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,338 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek Plugin Lockfile Management
|
|
4
|
+
|
|
5
|
+
Manages version pinning for installed plugins via lockfiles:
|
|
6
|
+
- User lockfile: ~/.tweek/plugins.lock.json
|
|
7
|
+
- Project lockfile: .tweek/plugins.lock.json (takes precedence)
|
|
8
|
+
|
|
9
|
+
Lockfiles pin exact versions + commit SHAs + checksums for
|
|
10
|
+
reproducible installations across team environments.
|
|
11
|
+
|
|
12
|
+
Format:
|
|
13
|
+
{
|
|
14
|
+
"schema_version": 1,
|
|
15
|
+
"generated_at": "2026-01-29T00:00:00Z",
|
|
16
|
+
"generated_by": "tweek 0.1.0",
|
|
17
|
+
"plugins": {
|
|
18
|
+
"tweek-plugin-cursor-detector": {
|
|
19
|
+
"version": "1.2.0",
|
|
20
|
+
"git_ref": "v1.2.0",
|
|
21
|
+
"commit_sha": "abc123...",
|
|
22
|
+
"checksums": {
|
|
23
|
+
"plugin.py": "sha256:..."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import logging
|
|
32
|
+
import subprocess
|
|
33
|
+
from datetime import datetime, timezone
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
36
|
+
|
|
37
|
+
from tweek.plugins.git_registry import PLUGINS_DIR, TWEEK_HOME
|
|
38
|
+
from tweek.plugins.git_security import generate_checksums
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
# Lockfile locations
|
|
43
|
+
USER_LOCKFILE = TWEEK_HOME / "plugins.lock.json"
|
|
44
|
+
PROJECT_LOCKFILE = Path(".tweek") / "plugins.lock.json"
|
|
45
|
+
|
|
46
|
+
# Current lockfile schema version
|
|
47
|
+
LOCKFILE_SCHEMA_VERSION = 1
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class LockfileError(Exception):
|
|
51
|
+
"""Raised when lockfile operations fail."""
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class PluginLock:
|
|
56
|
+
"""Represents a single plugin's locked state."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, data: dict):
|
|
59
|
+
self._data = data
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def version(self) -> str:
|
|
63
|
+
return self._data.get("version", "")
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def git_ref(self) -> str:
|
|
67
|
+
return self._data.get("git_ref", "")
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def commit_sha(self) -> str:
|
|
71
|
+
return self._data.get("commit_sha", "")
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def checksums(self) -> Dict[str, str]:
|
|
75
|
+
return self._data.get("checksums", {})
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict:
|
|
78
|
+
return dict(self._data)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PluginLockfile:
|
|
82
|
+
"""
|
|
83
|
+
Manages plugin version lockfiles.
|
|
84
|
+
|
|
85
|
+
Supports two lockfile locations:
|
|
86
|
+
- User lockfile: ~/.tweek/plugins.lock.json
|
|
87
|
+
- Project lockfile: .tweek/plugins.lock.json (takes precedence)
|
|
88
|
+
|
|
89
|
+
When a project lockfile exists, it overrides the user lockfile.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
user_lockfile: Optional[Path] = None,
|
|
95
|
+
project_lockfile: Optional[Path] = None,
|
|
96
|
+
plugins_dir: Optional[Path] = None,
|
|
97
|
+
):
|
|
98
|
+
self._user_lockfile = user_lockfile or USER_LOCKFILE
|
|
99
|
+
self._project_lockfile = project_lockfile or PROJECT_LOCKFILE
|
|
100
|
+
self._plugins_dir = plugins_dir or PLUGINS_DIR
|
|
101
|
+
self._locks: Optional[Dict[str, PluginLock]] = None
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def active_lockfile(self) -> Optional[Path]:
|
|
105
|
+
"""Get the active lockfile path (project takes precedence)."""
|
|
106
|
+
if self._project_lockfile.exists():
|
|
107
|
+
return self._project_lockfile
|
|
108
|
+
if self._user_lockfile.exists():
|
|
109
|
+
return self._user_lockfile
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def has_lockfile(self) -> bool:
|
|
114
|
+
"""Check if any lockfile exists."""
|
|
115
|
+
return self.active_lockfile is not None
|
|
116
|
+
|
|
117
|
+
def load(self) -> Dict[str, PluginLock]:
|
|
118
|
+
"""
|
|
119
|
+
Load the active lockfile.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Dict mapping plugin name to PluginLock
|
|
123
|
+
"""
|
|
124
|
+
lockfile = self.active_lockfile
|
|
125
|
+
if lockfile is None:
|
|
126
|
+
return {}
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
with open(lockfile) as f:
|
|
130
|
+
data = json.load(f)
|
|
131
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
132
|
+
raise LockfileError(f"Failed to read lockfile {lockfile}: {e}")
|
|
133
|
+
|
|
134
|
+
schema = data.get("schema_version", 0)
|
|
135
|
+
if schema != LOCKFILE_SCHEMA_VERSION:
|
|
136
|
+
raise LockfileError(
|
|
137
|
+
f"Unsupported lockfile schema version {schema}. "
|
|
138
|
+
f"Expected {LOCKFILE_SCHEMA_VERSION}. "
|
|
139
|
+
f"Regenerate with 'tweek plugins lock'."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
plugins_data = data.get("plugins", {})
|
|
143
|
+
self._locks = {
|
|
144
|
+
name: PluginLock(lock_data)
|
|
145
|
+
for name, lock_data in plugins_data.items()
|
|
146
|
+
}
|
|
147
|
+
return self._locks
|
|
148
|
+
|
|
149
|
+
def get_lock(self, name: str) -> Optional[PluginLock]:
|
|
150
|
+
"""
|
|
151
|
+
Get the lock for a specific plugin.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
name: Plugin name
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
PluginLock if locked, None otherwise
|
|
158
|
+
"""
|
|
159
|
+
if self._locks is None:
|
|
160
|
+
self.load()
|
|
161
|
+
return self._locks.get(name) if self._locks else None
|
|
162
|
+
|
|
163
|
+
def is_locked(self, name: str) -> bool:
|
|
164
|
+
"""Check if a plugin version is locked."""
|
|
165
|
+
return self.get_lock(name) is not None
|
|
166
|
+
|
|
167
|
+
def generate(
|
|
168
|
+
self,
|
|
169
|
+
target: str = "user",
|
|
170
|
+
specific_plugins: Optional[Dict[str, str]] = None,
|
|
171
|
+
) -> Path:
|
|
172
|
+
"""
|
|
173
|
+
Generate a lockfile from currently installed plugins.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
target: "user" for ~/.tweek/ or "project" for .tweek/
|
|
177
|
+
specific_plugins: Optional dict of {name: version} to lock.
|
|
178
|
+
If None, locks all installed plugins.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Path to the generated lockfile
|
|
182
|
+
"""
|
|
183
|
+
lockfile_path = self._user_lockfile if target == "user" else self._project_lockfile
|
|
184
|
+
|
|
185
|
+
# Build plugin locks
|
|
186
|
+
plugins_lock = {}
|
|
187
|
+
|
|
188
|
+
if specific_plugins:
|
|
189
|
+
# Lock specific plugins
|
|
190
|
+
for name, version in specific_plugins.items():
|
|
191
|
+
lock_data = self._build_lock_entry(name, version)
|
|
192
|
+
if lock_data:
|
|
193
|
+
plugins_lock[name] = lock_data
|
|
194
|
+
else:
|
|
195
|
+
# Lock all installed plugins
|
|
196
|
+
if self._plugins_dir.exists():
|
|
197
|
+
for plugin_dir in sorted(self._plugins_dir.iterdir()):
|
|
198
|
+
if not plugin_dir.is_dir():
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
manifest_path = plugin_dir / "tweek_plugin.json"
|
|
202
|
+
if not manifest_path.exists():
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
with open(manifest_path) as f:
|
|
207
|
+
manifest = json.load(f)
|
|
208
|
+
name = manifest.get("name", plugin_dir.name)
|
|
209
|
+
version = manifest.get("version", "unknown")
|
|
210
|
+
lock_data = self._build_lock_entry(name, version)
|
|
211
|
+
if lock_data:
|
|
212
|
+
plugins_lock[name] = lock_data
|
|
213
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
214
|
+
logger.warning(f"Skipping {plugin_dir.name}: {e}")
|
|
215
|
+
|
|
216
|
+
# Build lockfile
|
|
217
|
+
lockfile_data = {
|
|
218
|
+
"schema_version": LOCKFILE_SCHEMA_VERSION,
|
|
219
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
220
|
+
"generated_by": self._get_tweek_version(),
|
|
221
|
+
"plugins": plugins_lock,
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# Write lockfile
|
|
225
|
+
lockfile_path.parent.mkdir(parents=True, exist_ok=True)
|
|
226
|
+
with open(lockfile_path, "w") as f:
|
|
227
|
+
json.dump(lockfile_data, f, indent=2, sort_keys=True)
|
|
228
|
+
|
|
229
|
+
logger.info(f"Generated lockfile with {len(plugins_lock)} plugin(s): {lockfile_path}")
|
|
230
|
+
return lockfile_path
|
|
231
|
+
|
|
232
|
+
def check_compliance(self) -> Tuple[bool, List[str]]:
|
|
233
|
+
"""
|
|
234
|
+
Check if installed plugins match the lockfile.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
(all_compliant, list_of_violations)
|
|
238
|
+
"""
|
|
239
|
+
if not self.has_lockfile:
|
|
240
|
+
return True, []
|
|
241
|
+
|
|
242
|
+
violations = []
|
|
243
|
+
locks = self.load()
|
|
244
|
+
|
|
245
|
+
for name, lock in locks.items():
|
|
246
|
+
plugin_dir = self._plugins_dir / name
|
|
247
|
+
if not plugin_dir.exists():
|
|
248
|
+
violations.append(f"Locked plugin '{name}' is not installed")
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# Check version
|
|
252
|
+
manifest_path = plugin_dir / "tweek_plugin.json"
|
|
253
|
+
if not manifest_path.exists():
|
|
254
|
+
violations.append(f"Plugin '{name}' has no manifest")
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
with open(manifest_path) as f:
|
|
259
|
+
manifest = json.load(f)
|
|
260
|
+
except (json.JSONDecodeError, IOError):
|
|
261
|
+
violations.append(f"Plugin '{name}' has invalid manifest")
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
current_version = manifest.get("version", "")
|
|
265
|
+
if current_version != lock.version:
|
|
266
|
+
violations.append(
|
|
267
|
+
f"Plugin '{name}' version mismatch: "
|
|
268
|
+
f"installed={current_version}, locked={lock.version}"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Check commit SHA if available
|
|
272
|
+
if lock.commit_sha:
|
|
273
|
+
current_sha = self._get_git_commit(plugin_dir)
|
|
274
|
+
if current_sha and current_sha != lock.commit_sha:
|
|
275
|
+
violations.append(
|
|
276
|
+
f"Plugin '{name}' commit mismatch: "
|
|
277
|
+
f"installed={current_sha[:12]}, locked={lock.commit_sha[:12]}"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Check checksums
|
|
281
|
+
if lock.checksums:
|
|
282
|
+
current_checksums = generate_checksums(plugin_dir)
|
|
283
|
+
for filename, expected in lock.checksums.items():
|
|
284
|
+
actual = current_checksums.get(filename, "")
|
|
285
|
+
if actual and actual != expected:
|
|
286
|
+
violations.append(
|
|
287
|
+
f"Plugin '{name}' file '{filename}' has been modified"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
return len(violations) == 0, violations
|
|
291
|
+
|
|
292
|
+
def _build_lock_entry(self, name: str, version: str) -> Optional[dict]:
|
|
293
|
+
"""Build a lock entry for a plugin."""
|
|
294
|
+
plugin_dir = self._plugins_dir / name
|
|
295
|
+
if not plugin_dir.exists():
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
entry = {
|
|
299
|
+
"version": version,
|
|
300
|
+
"git_ref": f"v{version}",
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
# Get commit SHA
|
|
304
|
+
commit = self._get_git_commit(plugin_dir)
|
|
305
|
+
if commit:
|
|
306
|
+
entry["commit_sha"] = commit
|
|
307
|
+
|
|
308
|
+
# Generate checksums
|
|
309
|
+
checksums = generate_checksums(plugin_dir)
|
|
310
|
+
if checksums:
|
|
311
|
+
entry["checksums"] = checksums
|
|
312
|
+
|
|
313
|
+
return entry
|
|
314
|
+
|
|
315
|
+
def _get_git_commit(self, plugin_dir: Path) -> Optional[str]:
|
|
316
|
+
"""Get the current git commit SHA."""
|
|
317
|
+
try:
|
|
318
|
+
result = subprocess.run(
|
|
319
|
+
["git", "rev-parse", "HEAD"],
|
|
320
|
+
capture_output=True,
|
|
321
|
+
text=True,
|
|
322
|
+
timeout=5,
|
|
323
|
+
cwd=str(plugin_dir),
|
|
324
|
+
)
|
|
325
|
+
if result.returncode == 0:
|
|
326
|
+
return result.stdout.strip()
|
|
327
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
328
|
+
pass
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def _get_tweek_version() -> str:
|
|
333
|
+
"""Get the current Tweek version string."""
|
|
334
|
+
try:
|
|
335
|
+
from tweek import __version__
|
|
336
|
+
return f"tweek {__version__}"
|
|
337
|
+
except ImportError:
|
|
338
|
+
return "tweek unknown"
|