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,491 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek Git Plugin Installer
4
+
5
+ Handles git-based plugin installation operations:
6
+ - Clone plugin repos from registry
7
+ - Update to latest or specific version
8
+ - Remove installed plugins
9
+ - Verify installation integrity
10
+
11
+ All git operations use subprocess.run with:
12
+ - capture_output=True (no terminal access)
13
+ - timeout=30 (prevent hangs)
14
+ - No shell=True (no injection risk)
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ import shutil
21
+ import subprocess
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional, Tuple
24
+
25
+ from tweek.plugins.git_registry import (
26
+ PLUGINS_DIR,
27
+ TWEEK_HOME,
28
+ PluginRegistryClient,
29
+ RegistryEntry,
30
+ RegistryError,
31
+ )
32
+ from tweek.plugins.git_security import (
33
+ PluginSecurityError,
34
+ generate_checksums,
35
+ validate_manifest,
36
+ validate_plugin_full,
37
+ )
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+ # Git operation timeout in seconds
42
+ GIT_TIMEOUT = 30
43
+
44
+
45
+ class InstallError(Exception):
46
+ """Raised when plugin installation fails."""
47
+ pass
48
+
49
+
50
+ class GitPluginInstaller:
51
+ """
52
+ Installs, updates, and removes git-based plugins.
53
+
54
+ Each plugin is cloned into ~/.tweek/plugins/{name}/
55
+ Updates are git fetch + checkout to specific tag.
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ registry_client: Optional[PluginRegistryClient] = None,
61
+ plugins_dir: Optional[Path] = None,
62
+ ):
63
+ self._registry = registry_client or PluginRegistryClient()
64
+ self._plugins_dir = plugins_dir or PLUGINS_DIR
65
+
66
+ @property
67
+ def plugins_dir(self) -> Path:
68
+ return self._plugins_dir
69
+
70
+ def install(
71
+ self,
72
+ name: str,
73
+ version: Optional[str] = None,
74
+ verify: bool = True,
75
+ ) -> Tuple[bool, str]:
76
+ """
77
+ Install a plugin from the registry.
78
+
79
+ Steps:
80
+ 1. Look up plugin in registry
81
+ 2. Verify it's approved (verified=True)
82
+ 3. Git clone --depth 1
83
+ 4. Checkout specific version tag
84
+ 5. Run security verification pipeline
85
+
86
+ Args:
87
+ name: Plugin name (e.g., "tweek-plugin-cursor-detector")
88
+ version: Specific version to install (default: latest)
89
+ verify: Run security verification after install
90
+
91
+ Returns:
92
+ (success, message)
93
+ """
94
+ # Look up in registry
95
+ entry = self._registry.get_plugin(name)
96
+ if entry is None:
97
+ return False, f"Plugin '{name}' not found in registry or not verified"
98
+
99
+ if entry.deprecated:
100
+ logger.warning(f"Plugin '{name}' is deprecated")
101
+
102
+ # Determine version
103
+ target_version = version or entry.latest_version
104
+ if not target_version:
105
+ return False, f"No version available for '{name}'"
106
+
107
+ # Check version exists in registry
108
+ version_info = entry.get_version_info(target_version)
109
+ if version_info is None:
110
+ available = ", ".join(entry.versions.keys())
111
+ return False, (
112
+ f"Version '{target_version}' not found for '{name}'. "
113
+ f"Available versions: {available}"
114
+ )
115
+
116
+ # Check if already installed
117
+ plugin_dir = self._plugins_dir / name
118
+ if plugin_dir.exists():
119
+ return False, (
120
+ f"Plugin '{name}' is already installed at {plugin_dir}. "
121
+ f"Use 'tweek plugins update {name}' to update."
122
+ )
123
+
124
+ # Ensure plugins directory exists
125
+ self._plugins_dir.mkdir(parents=True, exist_ok=True)
126
+
127
+ # Git clone
128
+ repo_url = entry.repo_url
129
+ git_ref = entry.get_git_ref(target_version)
130
+
131
+ try:
132
+ self._git_clone(repo_url, plugin_dir, git_ref)
133
+ except InstallError as e:
134
+ # Clean up on failure
135
+ if plugin_dir.exists():
136
+ shutil.rmtree(plugin_dir, ignore_errors=True)
137
+ return False, f"Git clone failed: {e}"
138
+
139
+ # Verify installation
140
+ if verify:
141
+ try:
142
+ checksums = entry.get_checksums(target_version)
143
+ self._verify_installed_plugin(plugin_dir, checksums)
144
+ except (PluginSecurityError, InstallError) as e:
145
+ # Security check failed - remove the plugin
146
+ shutil.rmtree(plugin_dir, ignore_errors=True)
147
+ return False, f"Security verification failed: {e}"
148
+
149
+ logger.info(f"Successfully installed {name} v{target_version}")
150
+ return True, f"Installed {name} v{target_version}"
151
+
152
+ def update(
153
+ self,
154
+ name: str,
155
+ version: Optional[str] = None,
156
+ verify: bool = True,
157
+ ) -> Tuple[bool, str]:
158
+ """
159
+ Update an installed plugin.
160
+
161
+ Steps:
162
+ 1. Verify plugin is installed
163
+ 2. Git fetch
164
+ 3. Checkout new version tag
165
+ 4. Re-run security verification
166
+
167
+ Args:
168
+ name: Plugin name
169
+ version: Specific version to update to (default: latest)
170
+ verify: Run security verification after update
171
+
172
+ Returns:
173
+ (success, message)
174
+ """
175
+ plugin_dir = self._plugins_dir / name
176
+ if not plugin_dir.exists():
177
+ return False, f"Plugin '{name}' is not installed"
178
+
179
+ # Look up in registry
180
+ entry = self._registry.get_plugin(name)
181
+ if entry is None:
182
+ return False, f"Plugin '{name}' not found in registry"
183
+
184
+ # Determine target version
185
+ target_version = version or entry.latest_version
186
+ if not target_version:
187
+ return False, f"No version available for '{name}'"
188
+
189
+ # Check version exists
190
+ version_info = entry.get_version_info(target_version)
191
+ if version_info is None:
192
+ return False, f"Version '{target_version}' not found for '{name}'"
193
+
194
+ git_ref = entry.get_git_ref(target_version)
195
+
196
+ # Read current version
197
+ current_version = self._get_installed_version(plugin_dir)
198
+ if current_version == target_version:
199
+ return True, f"Plugin '{name}' is already at version {target_version}"
200
+
201
+ try:
202
+ self._git_fetch_checkout(plugin_dir, git_ref)
203
+ except InstallError as e:
204
+ return False, f"Git update failed: {e}"
205
+
206
+ # Verify updated plugin
207
+ if verify:
208
+ try:
209
+ checksums = entry.get_checksums(target_version)
210
+ self._verify_installed_plugin(plugin_dir, checksums)
211
+ except (PluginSecurityError, InstallError) as e:
212
+ # Revert to previous version if possible
213
+ if current_version:
214
+ old_ref = entry.get_git_ref(current_version)
215
+ try:
216
+ self._git_fetch_checkout(plugin_dir, old_ref)
217
+ logger.warning(f"Reverted {name} to v{current_version} after verification failure")
218
+ except InstallError:
219
+ pass
220
+ return False, f"Security verification failed after update: {e}"
221
+
222
+ logger.info(f"Successfully updated {name} to v{target_version}")
223
+ return True, f"Updated {name} from v{current_version} to v{target_version}"
224
+
225
+ def remove(self, name: str) -> Tuple[bool, str]:
226
+ """
227
+ Remove an installed plugin.
228
+
229
+ Args:
230
+ name: Plugin name
231
+
232
+ Returns:
233
+ (success, message)
234
+ """
235
+ plugin_dir = self._plugins_dir / name
236
+ if not plugin_dir.exists():
237
+ return False, f"Plugin '{name}' is not installed"
238
+
239
+ try:
240
+ shutil.rmtree(plugin_dir)
241
+ logger.info(f"Removed plugin '{name}'")
242
+ return True, f"Removed plugin '{name}'"
243
+ except OSError as e:
244
+ return False, f"Failed to remove plugin '{name}': {e}"
245
+
246
+ def check_updates(self) -> List[Dict[str, str]]:
247
+ """
248
+ Check all installed plugins for available updates.
249
+
250
+ Returns:
251
+ List of dicts with keys: name, current_version, latest_version
252
+ """
253
+ updates = []
254
+
255
+ if not self._plugins_dir.exists():
256
+ return updates
257
+
258
+ for plugin_dir in self._plugins_dir.iterdir():
259
+ if not plugin_dir.is_dir():
260
+ continue
261
+
262
+ name = plugin_dir.name
263
+ current = self._get_installed_version(plugin_dir)
264
+ if not current:
265
+ continue
266
+
267
+ latest = self._registry.get_update_available(name, current)
268
+ if latest:
269
+ updates.append({
270
+ "name": name,
271
+ "current_version": current,
272
+ "latest_version": latest,
273
+ })
274
+
275
+ return updates
276
+
277
+ def list_installed(self) -> List[Dict[str, str]]:
278
+ """
279
+ List all installed git plugins.
280
+
281
+ Returns:
282
+ List of dicts with keys: name, version, category, path
283
+ """
284
+ installed = []
285
+
286
+ if not self._plugins_dir.exists():
287
+ return installed
288
+
289
+ for plugin_dir in self._plugins_dir.iterdir():
290
+ if not plugin_dir.is_dir():
291
+ continue
292
+
293
+ manifest_path = plugin_dir / "tweek_plugin.json"
294
+ if not manifest_path.exists():
295
+ continue
296
+
297
+ try:
298
+ with open(manifest_path) as f:
299
+ manifest = json.load(f)
300
+
301
+ installed.append({
302
+ "name": manifest.get("name", plugin_dir.name),
303
+ "version": manifest.get("version", "unknown"),
304
+ "category": manifest.get("category", "unknown"),
305
+ "path": str(plugin_dir),
306
+ })
307
+ except (json.JSONDecodeError, IOError):
308
+ installed.append({
309
+ "name": plugin_dir.name,
310
+ "version": "unknown",
311
+ "category": "unknown",
312
+ "path": str(plugin_dir),
313
+ })
314
+
315
+ return installed
316
+
317
+ def verify_plugin(self, name: str) -> Tuple[bool, List[str]]:
318
+ """
319
+ Verify the integrity of an installed plugin.
320
+
321
+ Args:
322
+ name: Plugin name
323
+
324
+ Returns:
325
+ (is_valid, list_of_issues)
326
+ """
327
+ plugin_dir = self._plugins_dir / name
328
+ if not plugin_dir.exists():
329
+ return False, [f"Plugin '{name}' is not installed"]
330
+
331
+ manifest_path = plugin_dir / "tweek_plugin.json"
332
+ valid, manifest, issues = validate_manifest(manifest_path)
333
+ if not valid:
334
+ return False, issues
335
+
336
+ # Get checksums from registry
337
+ entry = self._registry.get_plugin(name)
338
+ checksums = {}
339
+ if entry:
340
+ version = manifest.get("version", "")
341
+ checksums = entry.get_checksums(version)
342
+
343
+ return validate_plugin_full(
344
+ plugin_dir,
345
+ manifest,
346
+ registry_checksums=checksums,
347
+ skip_signature=False,
348
+ )
349
+
350
+ def verify_all(self) -> Dict[str, Tuple[bool, List[str]]]:
351
+ """
352
+ Verify integrity of all installed plugins.
353
+
354
+ Returns:
355
+ Dict mapping plugin name to (is_valid, issues)
356
+ """
357
+ results = {}
358
+
359
+ if not self._plugins_dir.exists():
360
+ return results
361
+
362
+ for plugin_dir in self._plugins_dir.iterdir():
363
+ if not plugin_dir.is_dir():
364
+ continue
365
+ name = plugin_dir.name
366
+ results[name] = self.verify_plugin(name)
367
+
368
+ return results
369
+
370
+ # =========================================================================
371
+ # INTERNAL HELPERS
372
+ # =========================================================================
373
+
374
+ def _git_clone(self, repo_url: str, target_dir: Path, git_ref: str) -> None:
375
+ """
376
+ Clone a git repository.
377
+
378
+ Uses --depth 1 for shallow clone and checks out specific ref.
379
+ """
380
+ # Clone with depth 1
381
+ result = subprocess.run(
382
+ ["git", "clone", "--depth", "1", "--branch", git_ref, repo_url, str(target_dir)],
383
+ capture_output=True,
384
+ text=True,
385
+ timeout=GIT_TIMEOUT,
386
+ )
387
+
388
+ if result.returncode != 0:
389
+ # Try cloning without --branch (ref might not be a branch/tag)
390
+ result2 = subprocess.run(
391
+ ["git", "clone", "--depth", "1", repo_url, str(target_dir)],
392
+ capture_output=True,
393
+ text=True,
394
+ timeout=GIT_TIMEOUT,
395
+ )
396
+
397
+ if result2.returncode != 0:
398
+ raise InstallError(
399
+ f"git clone failed: {result2.stderr.strip() or result.stderr.strip()}"
400
+ )
401
+
402
+ # Fetch and checkout the specific ref
403
+ self._git_fetch_checkout(target_dir, git_ref)
404
+
405
+ def _git_fetch_checkout(self, plugin_dir: Path, git_ref: str) -> None:
406
+ """
407
+ Fetch latest changes and checkout a specific ref.
408
+ """
409
+ # Fetch all tags
410
+ result = subprocess.run(
411
+ ["git", "fetch", "--tags", "--depth", "1"],
412
+ capture_output=True,
413
+ text=True,
414
+ timeout=GIT_TIMEOUT,
415
+ cwd=str(plugin_dir),
416
+ )
417
+
418
+ if result.returncode != 0:
419
+ raise InstallError(f"git fetch failed: {result.stderr.strip()}")
420
+
421
+ # Checkout the ref
422
+ result = subprocess.run(
423
+ ["git", "checkout", git_ref],
424
+ capture_output=True,
425
+ text=True,
426
+ timeout=GIT_TIMEOUT,
427
+ cwd=str(plugin_dir),
428
+ )
429
+
430
+ if result.returncode != 0:
431
+ raise InstallError(f"git checkout {git_ref} failed: {result.stderr.strip()}")
432
+
433
+ def _get_installed_version(self, plugin_dir: Path) -> Optional[str]:
434
+ """Get the version of an installed plugin from its manifest."""
435
+ manifest_path = plugin_dir / "tweek_plugin.json"
436
+ if not manifest_path.exists():
437
+ return None
438
+
439
+ try:
440
+ with open(manifest_path) as f:
441
+ manifest = json.load(f)
442
+ return manifest.get("version")
443
+ except (json.JSONDecodeError, IOError):
444
+ return None
445
+
446
+ def _verify_installed_plugin(
447
+ self,
448
+ plugin_dir: Path,
449
+ registry_checksums: Optional[Dict[str, str]] = None,
450
+ ) -> None:
451
+ """
452
+ Run security verification on an installed plugin.
453
+
454
+ Raises:
455
+ PluginSecurityError: If verification fails
456
+ InstallError: If manifest is invalid
457
+ """
458
+ manifest_path = plugin_dir / "tweek_plugin.json"
459
+ valid, manifest, issues = validate_manifest(manifest_path)
460
+ if not valid:
461
+ raise InstallError(
462
+ f"Invalid manifest: {'; '.join(issues)}"
463
+ )
464
+
465
+ is_safe, security_issues = validate_plugin_full(
466
+ plugin_dir,
467
+ manifest,
468
+ registry_checksums=registry_checksums,
469
+ skip_signature=False,
470
+ )
471
+
472
+ if not is_safe:
473
+ raise PluginSecurityError(
474
+ f"Plugin failed security validation: {'; '.join(security_issues)}"
475
+ )
476
+
477
+ def _get_git_commit(self, plugin_dir: Path) -> Optional[str]:
478
+ """Get the current git commit SHA of an installed plugin."""
479
+ try:
480
+ result = subprocess.run(
481
+ ["git", "rev-parse", "HEAD"],
482
+ capture_output=True,
483
+ text=True,
484
+ timeout=5,
485
+ cwd=str(plugin_dir),
486
+ )
487
+ if result.returncode == 0:
488
+ return result.stdout.strip()
489
+ except (subprocess.TimeoutExpired, FileNotFoundError):
490
+ pass
491
+ return None