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.
- tweek/__init__.py +16 -0
- tweek/cli.py +3390 -0
- tweek/cli_helpers.py +193 -0
- tweek/config/__init__.py +13 -0
- tweek/config/allowed_dirs.yaml +23 -0
- tweek/config/manager.py +1064 -0
- tweek/config/patterns.yaml +751 -0
- tweek/config/tiers.yaml +129 -0
- tweek/diagnostics.py +589 -0
- tweek/hooks/__init__.py +1 -0
- tweek/hooks/pre_tool_use.py +861 -0
- tweek/integrations/__init__.py +3 -0
- tweek/integrations/moltbot.py +243 -0
- tweek/licensing.py +398 -0
- tweek/logging/__init__.py +9 -0
- tweek/logging/bundle.py +350 -0
- tweek/logging/json_logger.py +150 -0
- tweek/logging/security_log.py +745 -0
- tweek/mcp/__init__.py +24 -0
- tweek/mcp/approval.py +456 -0
- tweek/mcp/approval_cli.py +356 -0
- tweek/mcp/clients/__init__.py +37 -0
- tweek/mcp/clients/chatgpt.py +112 -0
- tweek/mcp/clients/claude_desktop.py +203 -0
- tweek/mcp/clients/gemini.py +178 -0
- tweek/mcp/proxy.py +667 -0
- tweek/mcp/screening.py +175 -0
- tweek/mcp/server.py +317 -0
- tweek/platform/__init__.py +131 -0
- tweek/plugins/__init__.py +835 -0
- tweek/plugins/base.py +1080 -0
- tweek/plugins/compliance/__init__.py +30 -0
- tweek/plugins/compliance/gdpr.py +333 -0
- tweek/plugins/compliance/gov.py +324 -0
- tweek/plugins/compliance/hipaa.py +285 -0
- tweek/plugins/compliance/legal.py +322 -0
- tweek/plugins/compliance/pci.py +361 -0
- tweek/plugins/compliance/soc2.py +275 -0
- tweek/plugins/detectors/__init__.py +30 -0
- tweek/plugins/detectors/continue_dev.py +206 -0
- tweek/plugins/detectors/copilot.py +254 -0
- tweek/plugins/detectors/cursor.py +192 -0
- tweek/plugins/detectors/moltbot.py +205 -0
- tweek/plugins/detectors/windsurf.py +214 -0
- tweek/plugins/git_discovery.py +395 -0
- tweek/plugins/git_installer.py +491 -0
- tweek/plugins/git_lockfile.py +338 -0
- tweek/plugins/git_registry.py +503 -0
- tweek/plugins/git_security.py +482 -0
- tweek/plugins/providers/__init__.py +30 -0
- tweek/plugins/providers/anthropic.py +181 -0
- tweek/plugins/providers/azure_openai.py +289 -0
- tweek/plugins/providers/bedrock.py +248 -0
- tweek/plugins/providers/google.py +197 -0
- tweek/plugins/providers/openai.py +230 -0
- tweek/plugins/scope.py +130 -0
- tweek/plugins/screening/__init__.py +26 -0
- tweek/plugins/screening/llm_reviewer.py +149 -0
- tweek/plugins/screening/pattern_matcher.py +273 -0
- tweek/plugins/screening/rate_limiter.py +174 -0
- tweek/plugins/screening/session_analyzer.py +159 -0
- tweek/proxy/__init__.py +302 -0
- tweek/proxy/addon.py +223 -0
- tweek/proxy/interceptor.py +313 -0
- tweek/proxy/server.py +315 -0
- tweek/sandbox/__init__.py +71 -0
- tweek/sandbox/executor.py +382 -0
- tweek/sandbox/linux.py +278 -0
- tweek/sandbox/profile_generator.py +323 -0
- tweek/screening/__init__.py +13 -0
- tweek/screening/context.py +81 -0
- tweek/security/__init__.py +22 -0
- tweek/security/llm_reviewer.py +348 -0
- tweek/security/rate_limiter.py +682 -0
- tweek/security/secret_scanner.py +506 -0
- tweek/security/session_analyzer.py +600 -0
- tweek/vault/__init__.py +40 -0
- tweek/vault/cross_platform.py +251 -0
- tweek/vault/keychain.py +288 -0
- tweek-0.1.0.dist-info/METADATA +335 -0
- tweek-0.1.0.dist-info/RECORD +85 -0
- tweek-0.1.0.dist-info/WHEEL +5 -0
- tweek-0.1.0.dist-info/entry_points.txt +25 -0
- tweek-0.1.0.dist-info/licenses/LICENSE +190 -0
- 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
|