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,395 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek Git Plugin Discovery
4
+
5
+ Scans ~/.tweek/plugins/ for git-installed plugins, validates them
6
+ through the security pipeline, and dynamically imports them into
7
+ the plugin registry.
8
+
9
+ Discovery Flow:
10
+ 1. Scan ~/.tweek/plugins/*/tweek_plugin.json for manifests
11
+ 2. Validate each manifest
12
+ 3. Run security verification (checksums, AST analysis)
13
+ 4. Dynamic import via importlib
14
+ 5. Verify base class inheritance
15
+ 6. Register in the plugin registry
16
+
17
+ Uses isolated module names (tweek_git_plugins.{name}) to avoid conflicts
18
+ with bundled plugins.
19
+ """
20
+
21
+ import importlib
22
+ import importlib.util
23
+ import json
24
+ import logging
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Any, Dict, List, Optional, Tuple, Type
28
+
29
+ from tweek.plugins.git_registry import (
30
+ PLUGINS_DIR,
31
+ PluginRegistryClient,
32
+ RegistryEntry,
33
+ )
34
+ from tweek.plugins.git_security import (
35
+ PluginSecurityError,
36
+ validate_manifest,
37
+ validate_plugin_full,
38
+ verify_base_class,
39
+ )
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+ # Namespace prefix for dynamically imported git plugins
44
+ GIT_PLUGIN_MODULE_PREFIX = "tweek_git_plugins"
45
+
46
+
47
+ class PluginDiscoveryError(Exception):
48
+ """Raised when plugin discovery fails."""
49
+ pass
50
+
51
+
52
+ class DiscoveredPlugin:
53
+ """Information about a discovered git plugin."""
54
+
55
+ def __init__(
56
+ self,
57
+ name: str,
58
+ version: str,
59
+ category: str,
60
+ plugin_class: Type,
61
+ plugin_dir: Path,
62
+ manifest: dict,
63
+ ):
64
+ self.name = name
65
+ self.version = version
66
+ self.category = category
67
+ self.plugin_class = plugin_class
68
+ self.plugin_dir = plugin_dir
69
+ self.manifest = manifest
70
+
71
+ def __repr__(self) -> str:
72
+ return f"DiscoveredPlugin(name={self.name!r}, version={self.version!r}, category={self.category!r})"
73
+
74
+
75
+ def discover_git_plugins(
76
+ registry_client: Optional[PluginRegistryClient] = None,
77
+ plugins_dir: Optional[Path] = None,
78
+ skip_security: bool = False,
79
+ ) -> List[DiscoveredPlugin]:
80
+ """
81
+ Scan for and validate git-installed plugins.
82
+
83
+ Args:
84
+ registry_client: Registry client for checksum lookups
85
+ plugins_dir: Override plugins directory (default: ~/.tweek/plugins/)
86
+ skip_security: Skip security validation (for development only)
87
+
88
+ Returns:
89
+ List of successfully discovered and validated plugins
90
+ """
91
+ plugins_dir = plugins_dir or PLUGINS_DIR
92
+ discovered = []
93
+
94
+ if not plugins_dir.exists():
95
+ logger.debug(f"Plugins directory does not exist: {plugins_dir}")
96
+ return discovered
97
+
98
+ for plugin_dir in sorted(plugins_dir.iterdir()):
99
+ if not plugin_dir.is_dir():
100
+ continue
101
+
102
+ # Skip hidden directories
103
+ if plugin_dir.name.startswith("."):
104
+ continue
105
+
106
+ manifest_path = plugin_dir / "tweek_plugin.json"
107
+ if not manifest_path.exists():
108
+ logger.debug(f"No manifest in {plugin_dir.name}, skipping")
109
+ continue
110
+
111
+ try:
112
+ plugin = _discover_single_plugin(
113
+ plugin_dir=plugin_dir,
114
+ registry_client=registry_client,
115
+ skip_security=skip_security,
116
+ )
117
+ if plugin:
118
+ discovered.append(plugin)
119
+ logger.info(
120
+ f"Discovered git plugin: {plugin.name} v{plugin.version} "
121
+ f"({plugin.category})"
122
+ )
123
+ except (PluginSecurityError, PluginDiscoveryError) as e:
124
+ logger.warning(f"Failed to load plugin from {plugin_dir.name}: {e}")
125
+ except Exception as e:
126
+ logger.error(f"Unexpected error loading {plugin_dir.name}: {e}")
127
+
128
+ logger.info(f"Discovered {len(discovered)} git plugin(s)")
129
+ return discovered
130
+
131
+
132
+ def _discover_single_plugin(
133
+ plugin_dir: Path,
134
+ registry_client: Optional[PluginRegistryClient] = None,
135
+ skip_security: bool = False,
136
+ ) -> Optional[DiscoveredPlugin]:
137
+ """
138
+ Discover and validate a single plugin from a directory.
139
+
140
+ Args:
141
+ plugin_dir: Path to the plugin directory
142
+ registry_client: Registry client for checksum lookups
143
+ skip_security: Skip security validation
144
+
145
+ Returns:
146
+ DiscoveredPlugin if successful, None otherwise
147
+
148
+ Raises:
149
+ PluginSecurityError: If security validation fails
150
+ PluginDiscoveryError: If plugin cannot be loaded
151
+ """
152
+ # Step 1: Validate manifest
153
+ manifest_path = plugin_dir / "tweek_plugin.json"
154
+ valid, manifest, issues = validate_manifest(manifest_path)
155
+ if not valid:
156
+ raise PluginDiscoveryError(
157
+ f"Invalid manifest: {'; '.join(issues)}"
158
+ )
159
+
160
+ name = manifest["name"]
161
+ version = manifest["version"]
162
+ category = manifest["category"]
163
+ entry_point = manifest["entry_point"]
164
+
165
+ # Step 2: Check version compatibility
166
+ min_tweek = manifest.get("min_tweek_version")
167
+ max_tweek = manifest.get("max_tweek_version")
168
+ if not _check_version_compat(min_tweek, max_tweek):
169
+ raise PluginDiscoveryError(
170
+ f"Plugin {name} v{version} is not compatible with this version of Tweek. "
171
+ f"Requires: {min_tweek or '*'} - {max_tweek or '*'}"
172
+ )
173
+
174
+ # Step 3: Security validation
175
+ if not skip_security:
176
+ registry_checksums = None
177
+ if registry_client:
178
+ entry = registry_client.get_plugin(name)
179
+ if entry:
180
+ registry_checksums = entry.get_checksums(version)
181
+
182
+ is_safe, security_issues = validate_plugin_full(
183
+ plugin_dir,
184
+ manifest,
185
+ registry_checksums=registry_checksums,
186
+ skip_signature=False,
187
+ )
188
+
189
+ if not is_safe:
190
+ raise PluginSecurityError(
191
+ f"Plugin {name} failed security validation: {'; '.join(security_issues)}"
192
+ )
193
+
194
+ # Step 4: Dynamic import
195
+ plugin_class = _import_plugin_class(plugin_dir, entry_point, name)
196
+
197
+ # Step 5: Verify base class
198
+ valid, error = verify_base_class(plugin_class, category)
199
+ if not valid:
200
+ raise PluginDiscoveryError(f"Base class verification failed: {error}")
201
+
202
+ return DiscoveredPlugin(
203
+ name=name,
204
+ version=version,
205
+ category=category,
206
+ plugin_class=plugin_class,
207
+ plugin_dir=plugin_dir,
208
+ manifest=manifest,
209
+ )
210
+
211
+
212
+ def _import_plugin_class(
213
+ plugin_dir: Path,
214
+ entry_point: str,
215
+ plugin_name: str,
216
+ ) -> Type:
217
+ """
218
+ Dynamically import a plugin class from its entry point.
219
+
220
+ Entry point format: "module:ClassName"
221
+ e.g., "plugin:CursorDetector" imports CursorDetector from plugin.py
222
+
223
+ Uses isolated module names to avoid conflicts with bundled plugins:
224
+ tweek_git_plugins.{plugin_name}.{module}
225
+
226
+ Args:
227
+ plugin_dir: Path to the plugin directory
228
+ entry_point: Entry point string ("module:ClassName")
229
+ plugin_name: Plugin name for module namespace
230
+
231
+ Returns:
232
+ The plugin class
233
+
234
+ Raises:
235
+ PluginDiscoveryError: If import fails
236
+ """
237
+ if ":" not in entry_point:
238
+ raise PluginDiscoveryError(
239
+ f"Invalid entry_point format '{entry_point}'. "
240
+ f"Must be 'module:ClassName'"
241
+ )
242
+
243
+ module_name, class_name = entry_point.split(":", 1)
244
+
245
+ # Construct file path
246
+ module_file = plugin_dir / f"{module_name}.py"
247
+ if not module_file.exists():
248
+ raise PluginDiscoveryError(
249
+ f"Entry point module '{module_name}.py' not found in {plugin_dir}"
250
+ )
251
+
252
+ # Create isolated module name
253
+ full_module_name = f"{GIT_PLUGIN_MODULE_PREFIX}.{plugin_name}.{module_name}"
254
+
255
+ try:
256
+ # Load module from file
257
+ spec = importlib.util.spec_from_file_location(
258
+ full_module_name,
259
+ str(module_file),
260
+ )
261
+ if spec is None:
262
+ raise PluginDiscoveryError(
263
+ f"Could not create module spec for {module_file}"
264
+ )
265
+
266
+ module = importlib.util.module_from_spec(spec)
267
+
268
+ # Add plugin directory to the module's path for relative imports
269
+ if plugin_dir not in sys.path:
270
+ sys.path.insert(0, str(plugin_dir))
271
+
272
+ # Register the module in sys.modules before executing
273
+ sys.modules[full_module_name] = module
274
+
275
+ spec.loader.exec_module(module)
276
+
277
+ # Get the class
278
+ if not hasattr(module, class_name):
279
+ raise PluginDiscoveryError(
280
+ f"Class '{class_name}' not found in module '{module_name}'. "
281
+ f"Available: {[n for n in dir(module) if not n.startswith('_')]}"
282
+ )
283
+
284
+ plugin_class = getattr(module, class_name)
285
+
286
+ if not isinstance(plugin_class, type):
287
+ raise PluginDiscoveryError(
288
+ f"'{class_name}' in '{module_name}' is not a class"
289
+ )
290
+
291
+ return plugin_class
292
+
293
+ except ImportError as e:
294
+ raise PluginDiscoveryError(
295
+ f"Failed to import plugin: {e}"
296
+ ) from e
297
+ except Exception as e:
298
+ if isinstance(e, PluginDiscoveryError):
299
+ raise
300
+ raise PluginDiscoveryError(
301
+ f"Error loading plugin module: {e}"
302
+ ) from e
303
+ finally:
304
+ # Clean up sys.path (only remove if we added it)
305
+ try:
306
+ sys.path.remove(str(plugin_dir))
307
+ except ValueError:
308
+ pass
309
+
310
+
311
+ def _check_version_compat(
312
+ min_version: Optional[str],
313
+ max_version: Optional[str],
314
+ ) -> bool:
315
+ """
316
+ Check if the current Tweek version is compatible with plugin requirements.
317
+
318
+ Args:
319
+ min_version: Minimum Tweek version required (or None for no minimum)
320
+ max_version: Maximum Tweek version allowed (or None for no maximum)
321
+
322
+ Returns:
323
+ True if compatible
324
+ """
325
+ # Get current Tweek version
326
+ try:
327
+ from tweek import __version__ as tweek_version
328
+ except ImportError:
329
+ # If we can't determine version, assume compatible
330
+ return True
331
+
332
+ if not tweek_version:
333
+ return True
334
+
335
+ try:
336
+ current = _parse_version(tweek_version)
337
+ except ValueError:
338
+ return True
339
+
340
+ if min_version:
341
+ try:
342
+ minimum = _parse_version(min_version)
343
+ if current < minimum:
344
+ return False
345
+ except ValueError:
346
+ pass
347
+
348
+ if max_version:
349
+ try:
350
+ maximum = _parse_version(max_version)
351
+ if current > maximum:
352
+ return False
353
+ except ValueError:
354
+ pass
355
+
356
+ return True
357
+
358
+
359
+ def _parse_version(version_str: str) -> Tuple[int, ...]:
360
+ """Parse a version string into a comparable tuple."""
361
+ parts = version_str.strip().split(".")
362
+ return tuple(int(p) for p in parts)
363
+
364
+
365
+ def get_plugin_info(plugin_dir: Path) -> Optional[Dict[str, Any]]:
366
+ """
367
+ Get information about a single git plugin directory.
368
+
369
+ Args:
370
+ plugin_dir: Path to the plugin directory
371
+
372
+ Returns:
373
+ Dict with plugin info, or None if invalid
374
+ """
375
+ manifest_path = plugin_dir / "tweek_plugin.json"
376
+ if not manifest_path.exists():
377
+ return None
378
+
379
+ try:
380
+ with open(manifest_path) as f:
381
+ manifest = json.load(f)
382
+
383
+ return {
384
+ "name": manifest.get("name", plugin_dir.name),
385
+ "version": manifest.get("version", "unknown"),
386
+ "category": manifest.get("category", "unknown"),
387
+ "description": manifest.get("description", ""),
388
+ "author": manifest.get("author", ""),
389
+ "requires_license_tier": manifest.get("requires_license_tier", "free"),
390
+ "tags": manifest.get("tags", []),
391
+ "path": str(plugin_dir),
392
+ "source": "git",
393
+ }
394
+ except (json.JSONDecodeError, IOError):
395
+ return None