tweek 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. tweek/__init__.py +16 -0
  2. tweek/cli.py +3390 -0
  3. tweek/cli_helpers.py +193 -0
  4. tweek/config/__init__.py +13 -0
  5. tweek/config/allowed_dirs.yaml +23 -0
  6. tweek/config/manager.py +1064 -0
  7. tweek/config/patterns.yaml +751 -0
  8. tweek/config/tiers.yaml +129 -0
  9. tweek/diagnostics.py +589 -0
  10. tweek/hooks/__init__.py +1 -0
  11. tweek/hooks/pre_tool_use.py +861 -0
  12. tweek/integrations/__init__.py +3 -0
  13. tweek/integrations/moltbot.py +243 -0
  14. tweek/licensing.py +398 -0
  15. tweek/logging/__init__.py +9 -0
  16. tweek/logging/bundle.py +350 -0
  17. tweek/logging/json_logger.py +150 -0
  18. tweek/logging/security_log.py +745 -0
  19. tweek/mcp/__init__.py +24 -0
  20. tweek/mcp/approval.py +456 -0
  21. tweek/mcp/approval_cli.py +356 -0
  22. tweek/mcp/clients/__init__.py +37 -0
  23. tweek/mcp/clients/chatgpt.py +112 -0
  24. tweek/mcp/clients/claude_desktop.py +203 -0
  25. tweek/mcp/clients/gemini.py +178 -0
  26. tweek/mcp/proxy.py +667 -0
  27. tweek/mcp/screening.py +175 -0
  28. tweek/mcp/server.py +317 -0
  29. tweek/platform/__init__.py +131 -0
  30. tweek/plugins/__init__.py +835 -0
  31. tweek/plugins/base.py +1080 -0
  32. tweek/plugins/compliance/__init__.py +30 -0
  33. tweek/plugins/compliance/gdpr.py +333 -0
  34. tweek/plugins/compliance/gov.py +324 -0
  35. tweek/plugins/compliance/hipaa.py +285 -0
  36. tweek/plugins/compliance/legal.py +322 -0
  37. tweek/plugins/compliance/pci.py +361 -0
  38. tweek/plugins/compliance/soc2.py +275 -0
  39. tweek/plugins/detectors/__init__.py +30 -0
  40. tweek/plugins/detectors/continue_dev.py +206 -0
  41. tweek/plugins/detectors/copilot.py +254 -0
  42. tweek/plugins/detectors/cursor.py +192 -0
  43. tweek/plugins/detectors/moltbot.py +205 -0
  44. tweek/plugins/detectors/windsurf.py +214 -0
  45. tweek/plugins/git_discovery.py +395 -0
  46. tweek/plugins/git_installer.py +491 -0
  47. tweek/plugins/git_lockfile.py +338 -0
  48. tweek/plugins/git_registry.py +503 -0
  49. tweek/plugins/git_security.py +482 -0
  50. tweek/plugins/providers/__init__.py +30 -0
  51. tweek/plugins/providers/anthropic.py +181 -0
  52. tweek/plugins/providers/azure_openai.py +289 -0
  53. tweek/plugins/providers/bedrock.py +248 -0
  54. tweek/plugins/providers/google.py +197 -0
  55. tweek/plugins/providers/openai.py +230 -0
  56. tweek/plugins/scope.py +130 -0
  57. tweek/plugins/screening/__init__.py +26 -0
  58. tweek/plugins/screening/llm_reviewer.py +149 -0
  59. tweek/plugins/screening/pattern_matcher.py +273 -0
  60. tweek/plugins/screening/rate_limiter.py +174 -0
  61. tweek/plugins/screening/session_analyzer.py +159 -0
  62. tweek/proxy/__init__.py +302 -0
  63. tweek/proxy/addon.py +223 -0
  64. tweek/proxy/interceptor.py +313 -0
  65. tweek/proxy/server.py +315 -0
  66. tweek/sandbox/__init__.py +71 -0
  67. tweek/sandbox/executor.py +382 -0
  68. tweek/sandbox/linux.py +278 -0
  69. tweek/sandbox/profile_generator.py +323 -0
  70. tweek/screening/__init__.py +13 -0
  71. tweek/screening/context.py +81 -0
  72. tweek/security/__init__.py +22 -0
  73. tweek/security/llm_reviewer.py +348 -0
  74. tweek/security/rate_limiter.py +682 -0
  75. tweek/security/secret_scanner.py +506 -0
  76. tweek/security/session_analyzer.py +600 -0
  77. tweek/vault/__init__.py +40 -0
  78. tweek/vault/cross_platform.py +251 -0
  79. tweek/vault/keychain.py +288 -0
  80. tweek-0.1.0.dist-info/METADATA +335 -0
  81. tweek-0.1.0.dist-info/RECORD +85 -0
  82. tweek-0.1.0.dist-info/WHEEL +5 -0
  83. tweek-0.1.0.dist-info/entry_points.txt +25 -0
  84. tweek-0.1.0.dist-info/licenses/LICENSE +190 -0
  85. tweek-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,835 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek Plugin System
4
+
5
+ Modular plugin architecture supporting:
6
+ - Domain compliance modules (Gov, HIPAA, PCI, Legal)
7
+ - LLM provider plugins (Anthropic, OpenAI, Google, Bedrock)
8
+ - Tool detector plugins (moltbot, Cursor, Continue)
9
+ - Screening method plugins (rate limiting, pattern matching, LLM review)
10
+
11
+ Plugin Discovery:
12
+ - Built-in plugins are registered automatically
13
+ - External plugins discovered via Python entry_points
14
+ - Plugins can be enabled/disabled via configuration
15
+
16
+ License Tiers:
17
+ - FREE: Core pattern matching, basic screening
18
+ - PRO: LLM review, session analysis, rate limiting
19
+ - ENTERPRISE: Compliance modules (Gov, HIPAA, PCI, Legal)
20
+ """
21
+
22
+ from dataclasses import dataclass, field
23
+ from enum import Enum
24
+ from typing import Optional, Dict, List, Any, Type, Callable
25
+ from importlib.metadata import entry_points
26
+ import logging
27
+ import threading
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ # Thread lock for singleton pattern
32
+ _registry_lock = threading.Lock()
33
+
34
+
35
+ class PluginSource(Enum):
36
+ """How a plugin was installed/discovered."""
37
+ BUILTIN = "builtin" # Bundled with Tweek
38
+ GIT = "git" # Installed from git repository
39
+ ENTRY_POINT = "entry_point" # Discovered via Python entry_points
40
+
41
+
42
+ class PluginCategory(Enum):
43
+ """Categories of plugins supported by Tweek."""
44
+ COMPLIANCE = "tweek.compliance" # Gov, HIPAA, PCI, Legal
45
+ LLM_PROVIDER = "tweek.llm_providers" # Anthropic, OpenAI, etc.
46
+ TOOL_DETECTOR = "tweek.tool_detectors" # moltbot, Cursor, etc.
47
+ SCREENING = "tweek.screening" # rate_limiter, pattern_matcher
48
+
49
+
50
+ class LicenseTier(Enum):
51
+ """License tiers for plugin access."""
52
+ FREE = "free"
53
+ PRO = "pro"
54
+ ENTERPRISE = "enterprise"
55
+
56
+
57
+ @dataclass
58
+ class PluginMetadata:
59
+ """Metadata describing a plugin."""
60
+ name: str
61
+ version: str
62
+ category: PluginCategory
63
+ description: str
64
+ author: Optional[str] = None
65
+ requires_license: LicenseTier = LicenseTier.FREE
66
+ homepage: Optional[str] = None
67
+ tags: List[str] = field(default_factory=list)
68
+
69
+ def __post_init__(self):
70
+ # Ensure tags is a list
71
+ if self.tags is None:
72
+ self.tags = []
73
+
74
+
75
+ @dataclass
76
+ class PluginInfo:
77
+ """Runtime information about a loaded plugin."""
78
+ metadata: PluginMetadata
79
+ plugin_class: Type
80
+ instance: Optional[Any] = None
81
+ enabled: bool = True
82
+ load_error: Optional[str] = None
83
+ source: PluginSource = PluginSource.BUILTIN
84
+ install_path: Optional[str] = None
85
+ manifest: Optional[Dict[str, Any]] = None
86
+
87
+ @property
88
+ def name(self) -> str:
89
+ return self.metadata.name
90
+
91
+ @property
92
+ def category(self) -> PluginCategory:
93
+ return self.metadata.category
94
+
95
+
96
+ class PluginRegistry:
97
+ """
98
+ Central registry for all Tweek plugins.
99
+
100
+ Handles plugin discovery, registration, and lifecycle management.
101
+ Supports both built-in plugins and external plugins via entry_points.
102
+ """
103
+
104
+ def __init__(self):
105
+ self._plugins: Dict[PluginCategory, Dict[str, PluginInfo]] = {
106
+ category: {} for category in PluginCategory
107
+ }
108
+ self._config: Dict[str, Dict[str, Any]] = {}
109
+ self._license_checker: Optional[Callable[[LicenseTier], bool]] = None
110
+
111
+ def set_license_checker(self, checker: Callable[[LicenseTier], bool]) -> None:
112
+ """
113
+ Set a function to check if a license tier is available.
114
+
115
+ Args:
116
+ checker: Function that takes LicenseTier and returns True if available
117
+ """
118
+ self._license_checker = checker
119
+
120
+ def _check_license(self, required: LicenseTier) -> bool:
121
+ """Check if the required license tier is available."""
122
+ if self._license_checker is None:
123
+ # No checker set - allow FREE tier only
124
+ return required == LicenseTier.FREE
125
+
126
+ return self._license_checker(required)
127
+
128
+ def _log_plugin_event(self, operation: str, **kwargs):
129
+ """Log plugin event to security logger (never raises)."""
130
+ try:
131
+ from tweek.logging.security_log import get_logger as get_sec_logger, SecurityEvent, EventType
132
+ metadata = {"operation": operation, **kwargs}
133
+ get_sec_logger().log(SecurityEvent(
134
+ event_type=EventType.PLUGIN_EVENT,
135
+ tool_name="plugin_registry",
136
+ decision="allow",
137
+ metadata=metadata,
138
+ source="plugins",
139
+ ))
140
+ except Exception:
141
+ pass
142
+
143
+ def discover_plugins(self) -> int:
144
+ """
145
+ Discover plugins via Python entry_points.
146
+
147
+ Compatible with Python 3.9, 3.10, 3.11, and 3.12.
148
+
149
+ Returns:
150
+ Number of plugins discovered
151
+ """
152
+ discovered = 0
153
+
154
+ for category in PluginCategory:
155
+ eps = self._get_entry_points(category.value)
156
+
157
+ for ep in eps:
158
+ try:
159
+ plugin_class = ep.load()
160
+ self.register(ep.name, plugin_class, category)
161
+ discovered += 1
162
+ logger.debug(f"Discovered plugin: {ep.name} ({category.value})")
163
+ except Exception as e:
164
+ logger.warning(f"Failed to load plugin {ep.name}: {e}")
165
+ self._log_plugin_event(
166
+ "discover_failed", plugin=ep.name, category=category.value, error=str(e)
167
+ )
168
+
169
+ if discovered > 0:
170
+ self._log_plugin_event("discover_complete", discovered_count=discovered)
171
+
172
+ return discovered
173
+
174
+ def _get_entry_points(self, group: str) -> List:
175
+ """
176
+ Get entry points for a group, compatible with Python 3.9+.
177
+
178
+ Python 3.9: entry_points() returns dict-like SelectableGroups
179
+ Python 3.10+: entry_points(group=...) returns EntryPoints directly
180
+
181
+ Args:
182
+ group: The entry point group name
183
+
184
+ Returns:
185
+ List of entry points (may be empty)
186
+ """
187
+ import sys
188
+
189
+ try:
190
+ if sys.version_info >= (3, 10):
191
+ # Python 3.10+ - use keyword argument
192
+ return list(entry_points(group=group))
193
+ else:
194
+ # Python 3.9 - returns dict-like SelectableGroups
195
+ all_eps = entry_points()
196
+
197
+ # In Python 3.9, entry_points() returns SelectableGroups
198
+ # which is dict-like with group names as keys
199
+ if hasattr(all_eps, 'get'):
200
+ # Standard dict-like access
201
+ return list(all_eps.get(group, []))
202
+ elif hasattr(all_eps, 'select'):
203
+ # Some versions support select()
204
+ return list(all_eps.select(group=group))
205
+ else:
206
+ # Fallback: iterate and filter
207
+ return [ep for ep in all_eps if getattr(ep, 'group', None) == group]
208
+
209
+ except Exception as e:
210
+ logger.warning(f"Failed to get entry points for {group}: {e}")
211
+ return []
212
+
213
+ def register(
214
+ self,
215
+ name: str,
216
+ plugin_class: Type,
217
+ category: PluginCategory,
218
+ metadata: Optional[PluginMetadata] = None,
219
+ source: PluginSource = PluginSource.BUILTIN,
220
+ install_path: Optional[str] = None,
221
+ manifest: Optional[Dict[str, Any]] = None,
222
+ override: bool = False,
223
+ ) -> bool:
224
+ """
225
+ Register a plugin.
226
+
227
+ Args:
228
+ name: Plugin name (must be unique within category)
229
+ plugin_class: Plugin class (not instance)
230
+ category: Plugin category
231
+ metadata: Optional metadata (extracted from class if not provided)
232
+ source: How the plugin was installed (builtin, git, entry_point)
233
+ install_path: Filesystem path for git-installed plugins
234
+ manifest: Parsed manifest dict for git-installed plugins
235
+ override: If True, replace existing registration (for git overriding builtin)
236
+
237
+ Returns:
238
+ True if registered successfully
239
+ """
240
+ # Extract metadata from class if not provided
241
+ if metadata is None:
242
+ metadata = self._extract_metadata(name, plugin_class, category)
243
+
244
+ # Check if already registered
245
+ if name in self._plugins[category]:
246
+ if override:
247
+ logger.info(
248
+ f"Git plugin {name} overriding builtin in {category.value}"
249
+ )
250
+ else:
251
+ logger.warning(f"Plugin {name} already registered in {category.value}")
252
+ return False
253
+
254
+ # Create plugin info
255
+ info = PluginInfo(
256
+ metadata=metadata,
257
+ plugin_class=plugin_class,
258
+ enabled=True,
259
+ source=source,
260
+ install_path=install_path,
261
+ manifest=manifest,
262
+ )
263
+
264
+ self._plugins[category][name] = info
265
+ logger.debug(f"Registered plugin: {name} ({category.value}, source={source.value})")
266
+ self._log_plugin_event(
267
+ "register", plugin=name, category=category.value, source=source.value
268
+ )
269
+ return True
270
+
271
+ def _extract_metadata(
272
+ self,
273
+ name: str,
274
+ plugin_class: Type,
275
+ category: PluginCategory
276
+ ) -> PluginMetadata:
277
+ """Extract metadata from a plugin class."""
278
+ # Try to get metadata from class attributes
279
+ version = getattr(plugin_class, 'VERSION', '0.0.0')
280
+ description = getattr(plugin_class, 'DESCRIPTION', plugin_class.__doc__ or '')
281
+ author = getattr(plugin_class, 'AUTHOR', None)
282
+ requires_license = getattr(plugin_class, 'REQUIRES_LICENSE', LicenseTier.FREE)
283
+ tags = getattr(plugin_class, 'TAGS', [])
284
+
285
+ # Handle string license tier
286
+ if isinstance(requires_license, str):
287
+ try:
288
+ requires_license = LicenseTier(requires_license)
289
+ except ValueError:
290
+ requires_license = LicenseTier.FREE
291
+
292
+ return PluginMetadata(
293
+ name=name,
294
+ version=version,
295
+ category=category,
296
+ description=description.strip() if description else '',
297
+ author=author,
298
+ requires_license=requires_license,
299
+ tags=tags
300
+ )
301
+
302
+ def unregister(self, name: str, category: PluginCategory) -> bool:
303
+ """
304
+ Unregister a plugin.
305
+
306
+ Args:
307
+ name: Plugin name
308
+ category: Plugin category
309
+
310
+ Returns:
311
+ True if unregistered successfully
312
+ """
313
+ if name in self._plugins[category]:
314
+ del self._plugins[category][name]
315
+ return True
316
+ return False
317
+
318
+ def get(
319
+ self,
320
+ name: str,
321
+ category: PluginCategory,
322
+ instantiate: bool = True
323
+ ) -> Optional[Any]:
324
+ """
325
+ Get a plugin instance.
326
+
327
+ Args:
328
+ name: Plugin name
329
+ category: Plugin category
330
+ instantiate: If True, return instance; if False, return class
331
+
332
+ Returns:
333
+ Plugin instance/class if found and enabled, None otherwise
334
+ """
335
+ info = self._plugins[category].get(name)
336
+ if info is None:
337
+ return None
338
+
339
+ if not info.enabled:
340
+ return None
341
+
342
+ # Check license
343
+ if not self._check_license(info.metadata.requires_license):
344
+ logger.debug(
345
+ f"Plugin {name} requires {info.metadata.requires_license.value} license"
346
+ )
347
+ return None
348
+
349
+ if not instantiate:
350
+ return info.plugin_class
351
+
352
+ # Lazy instantiation
353
+ if info.instance is None:
354
+ try:
355
+ config = self._config.get(name, {})
356
+ info.instance = info.plugin_class(config) if config else info.plugin_class()
357
+ except Exception as e:
358
+ info.load_error = str(e)
359
+ logger.error(f"Failed to instantiate plugin {name}: {e}")
360
+ self._log_plugin_event(
361
+ "instantiate_failed", plugin=name, category=category.value, error=str(e)
362
+ )
363
+ return None
364
+
365
+ return info.instance
366
+
367
+ def get_info(self, name: str, category: PluginCategory) -> Optional[PluginInfo]:
368
+ """Get plugin info without instantiating."""
369
+ return self._plugins[category].get(name)
370
+
371
+ def get_all(
372
+ self,
373
+ category: PluginCategory,
374
+ enabled_only: bool = True,
375
+ licensed_only: bool = True
376
+ ) -> List[Any]:
377
+ """
378
+ Get all plugins in a category.
379
+
380
+ Args:
381
+ category: Plugin category
382
+ enabled_only: Only return enabled plugins
383
+ licensed_only: Only return plugins user has license for
384
+
385
+ Returns:
386
+ List of plugin instances
387
+ """
388
+ plugins = []
389
+
390
+ for name, info in self._plugins[category].items():
391
+ if enabled_only and not info.enabled:
392
+ continue
393
+
394
+ if licensed_only and not self._check_license(info.metadata.requires_license):
395
+ continue
396
+
397
+ instance = self.get(name, category)
398
+ if instance is not None:
399
+ plugins.append(instance)
400
+
401
+ return plugins
402
+
403
+ def list_plugins(
404
+ self,
405
+ category: Optional[PluginCategory] = None
406
+ ) -> List[PluginInfo]:
407
+ """
408
+ List all registered plugins.
409
+
410
+ Args:
411
+ category: Optional category filter
412
+
413
+ Returns:
414
+ List of PluginInfo objects
415
+ """
416
+ plugins = []
417
+
418
+ categories = [category] if category else list(PluginCategory)
419
+
420
+ for cat in categories:
421
+ plugins.extend(self._plugins[cat].values())
422
+
423
+ return plugins
424
+
425
+ def enable(self, name: str, category: PluginCategory) -> bool:
426
+ """Enable a plugin."""
427
+ info = self._plugins[category].get(name)
428
+ if info:
429
+ info.enabled = True
430
+ return True
431
+ return False
432
+
433
+ def disable(self, name: str, category: PluginCategory) -> bool:
434
+ """Disable a plugin."""
435
+ info = self._plugins[category].get(name)
436
+ if info:
437
+ info.enabled = False
438
+ return True
439
+ return False
440
+
441
+ def is_enabled(self, name: str, category: PluginCategory) -> bool:
442
+ """Check if a plugin is enabled."""
443
+ info = self._plugins[category].get(name)
444
+ return info.enabled if info else False
445
+
446
+ def configure(self, name: str, config: Dict[str, Any], invalidate: bool = True) -> None:
447
+ """
448
+ Set configuration for a plugin.
449
+
450
+ Args:
451
+ name: Plugin name
452
+ config: Configuration dictionary
453
+ invalidate: If True, invalidate existing instance to force recreation
454
+ with new config (recommended for config changes)
455
+ """
456
+ self._config[name] = config
457
+
458
+ # Handle existing instances
459
+ for category in PluginCategory:
460
+ info = self._plugins[category].get(name)
461
+ if info and info.instance:
462
+ if invalidate:
463
+ # Invalidate instance so it gets recreated with new config
464
+ # on next access via get()
465
+ info.instance = None
466
+ info.load_error = None
467
+ elif hasattr(info.instance, 'configure'):
468
+ # Try hot-reconfiguration (for backward compatibility)
469
+ info.instance.configure(config)
470
+
471
+ def get_config(self, name: str) -> Dict[str, Any]:
472
+ """Get configuration for a plugin."""
473
+ return self._config.get(name, {})
474
+
475
+ def load_config(self, config: Dict[str, Any]) -> None:
476
+ """
477
+ Load plugin configuration from a dictionary.
478
+
479
+ Expected format:
480
+ {
481
+ "plugins": {
482
+ "compliance": {
483
+ "gov": {"enabled": true, "actions": {...}},
484
+ "hipaa": {"enabled": false}
485
+ },
486
+ "screening": {
487
+ "rate_limiter": {"enabled": true, "burst_threshold": 15}
488
+ }
489
+ }
490
+ }
491
+ """
492
+ plugins_config = config.get("plugins", {})
493
+
494
+ # Map category names to enums
495
+ category_map = {
496
+ "compliance": PluginCategory.COMPLIANCE,
497
+ "providers": PluginCategory.LLM_PROVIDER,
498
+ "detectors": PluginCategory.TOOL_DETECTOR,
499
+ "screening": PluginCategory.SCREENING,
500
+ }
501
+
502
+ for cat_name, cat_config in plugins_config.items():
503
+ category = category_map.get(cat_name)
504
+ if category is None:
505
+ continue
506
+
507
+ # Handle "modules" sub-key or direct plugin configs
508
+ modules = cat_config.get("modules", cat_config)
509
+ if not isinstance(modules, dict):
510
+ continue
511
+
512
+ for plugin_name, plugin_config in modules.items():
513
+ if not isinstance(plugin_config, dict):
514
+ continue
515
+
516
+ # Enable/disable
517
+ if "enabled" in plugin_config:
518
+ if plugin_config["enabled"]:
519
+ self.enable(plugin_name, category)
520
+ else:
521
+ self.disable(plugin_name, category)
522
+
523
+ # Store config
524
+ self.configure(plugin_name, plugin_config)
525
+
526
+ def get_stats(self) -> Dict[str, Any]:
527
+ """Get statistics about registered plugins."""
528
+ stats = {
529
+ "total": 0,
530
+ "enabled": 0,
531
+ "by_category": {},
532
+ "by_license": {tier.value: 0 for tier in LicenseTier}
533
+ }
534
+
535
+ for category in PluginCategory:
536
+ cat_stats = {"total": 0, "enabled": 0}
537
+
538
+ for info in self._plugins[category].values():
539
+ stats["total"] += 1
540
+ cat_stats["total"] += 1
541
+ stats["by_license"][info.metadata.requires_license.value] += 1
542
+
543
+ if info.enabled:
544
+ stats["enabled"] += 1
545
+ cat_stats["enabled"] += 1
546
+
547
+ stats["by_category"][category.value] = cat_stats
548
+
549
+ return stats
550
+
551
+
552
+ # Global registry instance
553
+ _registry: Optional[PluginRegistry] = None
554
+
555
+
556
+ def get_registry() -> PluginRegistry:
557
+ """
558
+ Get the global plugin registry.
559
+
560
+ Thread-safe singleton pattern using double-checked locking.
561
+ """
562
+ global _registry
563
+ if _registry is None:
564
+ with _registry_lock:
565
+ # Double-check after acquiring lock
566
+ if _registry is None:
567
+ _registry = PluginRegistry()
568
+ return _registry
569
+
570
+
571
+ def init_plugins(config: Optional[Dict[str, Any]] = None) -> PluginRegistry:
572
+ """
573
+ Initialize the plugin system.
574
+
575
+ Loads plugins in order:
576
+ 1. Built-in plugins (bundled with Tweek)
577
+ 2. Git-installed plugins (~/.tweek/plugins/)
578
+ 3. Entry point plugins (from installed packages)
579
+
580
+ Git plugins with the same name as a builtin will override the builtin.
581
+
582
+ Args:
583
+ config: Optional configuration dictionary
584
+
585
+ Returns:
586
+ The initialized plugin registry
587
+ """
588
+ registry = get_registry()
589
+
590
+ # Log startup
591
+ try:
592
+ from tweek.logging.security_log import get_logger as get_sec_logger, SecurityEvent, EventType
593
+ get_sec_logger().log(SecurityEvent(
594
+ event_type=EventType.STARTUP,
595
+ tool_name="plugin_system",
596
+ decision="allow",
597
+ metadata={"operation": "init_plugins"},
598
+ source="plugins",
599
+ ))
600
+ except Exception:
601
+ pass
602
+
603
+ # Register built-in plugins
604
+ _register_builtin_plugins(registry)
605
+
606
+ # Discover git-installed plugins
607
+ _discover_git_plugins(registry)
608
+
609
+ # Discover external plugins via entry_points
610
+ registry.discover_plugins()
611
+
612
+ # Load configuration
613
+ if config:
614
+ registry.load_config(config)
615
+
616
+ # Set up license checker
617
+ try:
618
+ from tweek.licensing import get_license, Tier
619
+
620
+ def check_license(required: LicenseTier) -> bool:
621
+ """
622
+ Check if the user's license tier meets the required tier.
623
+
624
+ Tier hierarchy: FREE < PRO < ENTERPRISE
625
+ Higher tiers include all lower tier features.
626
+ """
627
+ lic = get_license()
628
+ tier_order = [Tier.FREE, Tier.PRO, Tier.ENTERPRISE]
629
+
630
+ # Map plugin LicenseTier to licensing Tier
631
+ tier_map = {
632
+ LicenseTier.FREE: Tier.FREE,
633
+ LicenseTier.PRO: Tier.PRO,
634
+ LicenseTier.ENTERPRISE: Tier.ENTERPRISE,
635
+ }
636
+
637
+ required_tier = tier_map.get(required, Tier.FREE)
638
+ user_tier = lic.tier
639
+
640
+ # User has access if their tier is >= required tier
641
+ return tier_order.index(user_tier) >= tier_order.index(required_tier)
642
+
643
+ registry.set_license_checker(check_license)
644
+ except ImportError:
645
+ pass
646
+
647
+ return registry
648
+
649
+
650
+ def _register_builtin_plugins(registry: PluginRegistry) -> None:
651
+ """Register all built-in plugins."""
652
+ # Compliance plugins
653
+ try:
654
+ from tweek.plugins.compliance import (
655
+ GovCompliancePlugin,
656
+ HIPAACompliancePlugin,
657
+ PCICompliancePlugin,
658
+ LegalCompliancePlugin,
659
+ SOC2CompliancePlugin,
660
+ GDPRCompliancePlugin,
661
+ )
662
+ registry.register("gov", GovCompliancePlugin, PluginCategory.COMPLIANCE)
663
+ registry.register("hipaa", HIPAACompliancePlugin, PluginCategory.COMPLIANCE)
664
+ registry.register("pci", PCICompliancePlugin, PluginCategory.COMPLIANCE)
665
+ registry.register("legal", LegalCompliancePlugin, PluginCategory.COMPLIANCE)
666
+ registry.register("soc2", SOC2CompliancePlugin, PluginCategory.COMPLIANCE)
667
+ registry.register("gdpr", GDPRCompliancePlugin, PluginCategory.COMPLIANCE)
668
+ except ImportError as e:
669
+ logger.debug(f"Compliance plugins not available: {e}")
670
+
671
+ # Provider plugins
672
+ try:
673
+ from tweek.plugins.providers import (
674
+ AnthropicProvider,
675
+ OpenAIProvider,
676
+ AzureOpenAIProvider,
677
+ GoogleProvider,
678
+ BedrockProvider,
679
+ )
680
+ registry.register("anthropic", AnthropicProvider, PluginCategory.LLM_PROVIDER)
681
+ registry.register("openai", OpenAIProvider, PluginCategory.LLM_PROVIDER)
682
+ registry.register("azure_openai", AzureOpenAIProvider, PluginCategory.LLM_PROVIDER)
683
+ registry.register("google", GoogleProvider, PluginCategory.LLM_PROVIDER)
684
+ registry.register("bedrock", BedrockProvider, PluginCategory.LLM_PROVIDER)
685
+ except ImportError as e:
686
+ logger.debug(f"Provider plugins not available: {e}")
687
+
688
+ # Detector plugins
689
+ try:
690
+ from tweek.plugins.detectors import (
691
+ MoltbotDetector,
692
+ CursorDetector,
693
+ ContinueDetector,
694
+ CopilotDetector,
695
+ WindsurfDetector,
696
+ )
697
+ registry.register("moltbot", MoltbotDetector, PluginCategory.TOOL_DETECTOR)
698
+ registry.register("cursor", CursorDetector, PluginCategory.TOOL_DETECTOR)
699
+ registry.register("continue", ContinueDetector, PluginCategory.TOOL_DETECTOR)
700
+ registry.register("copilot", CopilotDetector, PluginCategory.TOOL_DETECTOR)
701
+ registry.register("windsurf", WindsurfDetector, PluginCategory.TOOL_DETECTOR)
702
+ except ImportError as e:
703
+ logger.debug(f"Detector plugins not available: {e}")
704
+
705
+ # Screening plugins
706
+ try:
707
+ from tweek.plugins.screening import (
708
+ RateLimiterPlugin,
709
+ PatternMatcherPlugin,
710
+ LLMReviewerPlugin,
711
+ SessionAnalyzerPlugin,
712
+ )
713
+ registry.register("rate_limiter", RateLimiterPlugin, PluginCategory.SCREENING)
714
+ registry.register("pattern_matcher", PatternMatcherPlugin, PluginCategory.SCREENING)
715
+ registry.register("llm_reviewer", LLMReviewerPlugin, PluginCategory.SCREENING)
716
+ registry.register("session_analyzer", SessionAnalyzerPlugin, PluginCategory.SCREENING)
717
+ except ImportError as e:
718
+ logger.debug(f"Screening plugins not available: {e}")
719
+
720
+
721
+ def _discover_git_plugins(registry: PluginRegistry) -> int:
722
+ """
723
+ Discover and register git-installed plugins from ~/.tweek/plugins/.
724
+
725
+ Git plugins override builtins with the same name, allowing users
726
+ to use newer versions of bundled plugins via git.
727
+
728
+ Returns:
729
+ Number of git plugins registered
730
+ """
731
+ try:
732
+ from tweek.plugins.git_discovery import discover_git_plugins
733
+ from tweek.plugins.git_registry import PluginRegistryClient
734
+ except ImportError:
735
+ logger.debug("Git plugin discovery modules not available")
736
+ return 0
737
+
738
+ # Map manifest categories to PluginCategory enum
739
+ category_map = {
740
+ "compliance": PluginCategory.COMPLIANCE,
741
+ "providers": PluginCategory.LLM_PROVIDER,
742
+ "detectors": PluginCategory.TOOL_DETECTOR,
743
+ "screening": PluginCategory.SCREENING,
744
+ }
745
+
746
+ registered = 0
747
+ try:
748
+ registry_client = PluginRegistryClient()
749
+ plugins = discover_git_plugins(registry_client=registry_client)
750
+
751
+ for plugin in plugins:
752
+ category = category_map.get(plugin.category)
753
+ if category is None:
754
+ logger.warning(
755
+ f"Git plugin {plugin.name} has unknown category: {plugin.category}"
756
+ )
757
+ continue
758
+
759
+ # Derive a short name from the full plugin name
760
+ # e.g., "tweek-plugin-cursor-detector" -> "cursor"
761
+ short_name = _derive_short_name(plugin.name, plugin.category)
762
+
763
+ # Git plugins override builtins
764
+ already_registered = short_name in registry._plugins.get(category, {})
765
+
766
+ success = registry.register(
767
+ name=short_name,
768
+ plugin_class=plugin.plugin_class,
769
+ category=category,
770
+ source=PluginSource.GIT,
771
+ install_path=str(plugin.plugin_dir),
772
+ manifest=plugin.manifest,
773
+ override=already_registered,
774
+ )
775
+
776
+ if success:
777
+ registered += 1
778
+
779
+ except Exception as e:
780
+ logger.warning(f"Git plugin discovery failed: {e}")
781
+ try:
782
+ from tweek.logging.security_log import get_logger as get_sec_logger, SecurityEvent, EventType
783
+ get_sec_logger().log(SecurityEvent(
784
+ event_type=EventType.PLUGIN_EVENT,
785
+ tool_name="plugin_registry",
786
+ decision="error",
787
+ decision_reason=f"Git plugin discovery failed: {e}",
788
+ source="plugins",
789
+ ))
790
+ except Exception:
791
+ pass
792
+
793
+ if registered > 0:
794
+ logger.info(f"Registered {registered} git plugin(s)")
795
+
796
+ return registered
797
+
798
+
799
+ def _derive_short_name(full_name: str, category: str) -> str:
800
+ """
801
+ Derive a short plugin name from the full registry name.
802
+
803
+ Examples:
804
+ "tweek-plugin-cursor-detector" -> "cursor"
805
+ "tweek-plugin-hipaa" -> "hipaa"
806
+ "tweek-plugin-openai-provider" -> "openai"
807
+ """
808
+ # Remove common prefixes
809
+ name = full_name
810
+ for prefix in ("tweek-plugin-", "tweek-"):
811
+ if name.startswith(prefix):
812
+ name = name[len(prefix):]
813
+ break
814
+
815
+ # Remove common suffixes
816
+ for suffix in ("-detector", "-provider", "-plugin", "-compliance", "-screening"):
817
+ if name.endswith(suffix):
818
+ name = name[:-len(suffix)]
819
+ break
820
+
821
+ # Replace hyphens with underscores
822
+ return name.replace("-", "_")
823
+
824
+
825
+ # Public API
826
+ __all__ = [
827
+ "PluginCategory",
828
+ "PluginSource",
829
+ "PluginMetadata",
830
+ "PluginInfo",
831
+ "PluginRegistry",
832
+ "LicenseTier",
833
+ "get_registry",
834
+ "init_plugins",
835
+ ]