tweek 0.1.0__py3-none-any.whl → 0.2.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 +2 -2
  2. tweek/_keygen.py +53 -0
  3. tweek/audit.py +288 -0
  4. tweek/cli.py +5303 -2396
  5. tweek/cli_model.py +380 -0
  6. tweek/config/families.yaml +609 -0
  7. tweek/config/manager.py +42 -5
  8. tweek/config/patterns.yaml +1510 -8
  9. tweek/config/tiers.yaml +161 -11
  10. tweek/diagnostics.py +71 -2
  11. tweek/hooks/break_glass.py +163 -0
  12. tweek/hooks/feedback.py +223 -0
  13. tweek/hooks/overrides.py +531 -0
  14. tweek/hooks/post_tool_use.py +472 -0
  15. tweek/hooks/pre_tool_use.py +1024 -62
  16. tweek/integrations/openclaw.py +443 -0
  17. tweek/integrations/openclaw_server.py +385 -0
  18. tweek/licensing.py +14 -54
  19. tweek/logging/bundle.py +2 -2
  20. tweek/logging/security_log.py +56 -13
  21. tweek/mcp/approval.py +57 -16
  22. tweek/mcp/proxy.py +18 -0
  23. tweek/mcp/screening.py +5 -5
  24. tweek/mcp/server.py +4 -1
  25. tweek/memory/__init__.py +24 -0
  26. tweek/memory/queries.py +223 -0
  27. tweek/memory/safety.py +140 -0
  28. tweek/memory/schemas.py +80 -0
  29. tweek/memory/store.py +989 -0
  30. tweek/platform/__init__.py +4 -4
  31. tweek/plugins/__init__.py +40 -24
  32. tweek/plugins/base.py +1 -1
  33. tweek/plugins/detectors/__init__.py +3 -3
  34. tweek/plugins/detectors/{moltbot.py → openclaw.py} +30 -27
  35. tweek/plugins/git_discovery.py +16 -4
  36. tweek/plugins/git_registry.py +8 -2
  37. tweek/plugins/git_security.py +21 -9
  38. tweek/plugins/screening/__init__.py +10 -1
  39. tweek/plugins/screening/heuristic_scorer.py +477 -0
  40. tweek/plugins/screening/llm_reviewer.py +14 -6
  41. tweek/plugins/screening/local_model_reviewer.py +161 -0
  42. tweek/proxy/__init__.py +38 -37
  43. tweek/proxy/addon.py +22 -3
  44. tweek/proxy/interceptor.py +1 -0
  45. tweek/proxy/server.py +4 -2
  46. tweek/sandbox/__init__.py +11 -0
  47. tweek/sandbox/docker_bridge.py +143 -0
  48. tweek/sandbox/executor.py +9 -6
  49. tweek/sandbox/layers.py +97 -0
  50. tweek/sandbox/linux.py +1 -0
  51. tweek/sandbox/project.py +548 -0
  52. tweek/sandbox/registry.py +149 -0
  53. tweek/security/__init__.py +9 -0
  54. tweek/security/language.py +250 -0
  55. tweek/security/llm_reviewer.py +1146 -60
  56. tweek/security/local_model.py +331 -0
  57. tweek/security/local_reviewer.py +146 -0
  58. tweek/security/model_registry.py +371 -0
  59. tweek/security/rate_limiter.py +11 -6
  60. tweek/security/secret_scanner.py +70 -4
  61. tweek/security/session_analyzer.py +26 -2
  62. tweek/skill_template/SKILL.md +200 -0
  63. tweek/skill_template/__init__.py +0 -0
  64. tweek/skill_template/cli-reference.md +331 -0
  65. tweek/skill_template/overrides-reference.md +184 -0
  66. tweek/skill_template/scripts/__init__.py +0 -0
  67. tweek/skill_template/scripts/check_installed.py +170 -0
  68. tweek/skills/__init__.py +38 -0
  69. tweek/skills/config.py +150 -0
  70. tweek/skills/fingerprints.py +198 -0
  71. tweek/skills/guard.py +293 -0
  72. tweek/skills/isolation.py +469 -0
  73. tweek/skills/scanner.py +715 -0
  74. tweek/vault/__init__.py +0 -1
  75. tweek/vault/cross_platform.py +12 -1
  76. tweek/vault/keychain.py +87 -29
  77. tweek-0.2.0.dist-info/METADATA +281 -0
  78. tweek-0.2.0.dist-info/RECORD +121 -0
  79. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/entry_points.txt +8 -1
  80. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/licenses/LICENSE +80 -0
  81. tweek/integrations/moltbot.py +0 -243
  82. tweek-0.1.0.dist-info/METADATA +0 -335
  83. tweek-0.1.0.dist-info/RECORD +0 -85
  84. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/WHEEL +0 -0
  85. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,443 @@
1
+ """
2
+ Tweek OpenClaw Integration - Gateway security plugin setup.
3
+
4
+ Detects OpenClaw Gateway, installs the Tweek security plugin, and
5
+ configures skill scanning, tool screening, and output scanning for
6
+ the OpenClaw ecosystem.
7
+ """
8
+
9
+ import json
10
+ import subprocess
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+
16
+ # OpenClaw default paths and ports
17
+ OPENCLAW_DEFAULT_PORT = 18789
18
+ OPENCLAW_HOME = Path.home() / ".openclaw"
19
+ OPENCLAW_CONFIG = OPENCLAW_HOME / "openclaw.json"
20
+ OPENCLAW_SKILLS_DIR = OPENCLAW_HOME / "workspace" / "skills"
21
+ OPENCLAW_PLUGIN_NAME = "@tweek/openclaw-plugin"
22
+
23
+ # Scanning server port (separate from Tweek proxy port)
24
+ SCANNER_SERVER_PORT = 9878
25
+
26
+
27
+ @dataclass
28
+ class OpenClawSetupResult:
29
+ """Result of OpenClaw protection setup."""
30
+ success: bool = False
31
+ openclaw_detected: bool = False
32
+ openclaw_version: Optional[str] = None
33
+ gateway_port: Optional[int] = None
34
+ gateway_running: bool = False
35
+ scanner_port: int = SCANNER_SERVER_PORT
36
+ preset: str = "cautious"
37
+ config_path: Optional[str] = None
38
+ plugin_installed: bool = False
39
+ error: Optional[str] = None
40
+ warnings: list = field(default_factory=list)
41
+
42
+
43
+ def detect_openclaw_installation() -> dict:
44
+ """
45
+ Detect OpenClaw installation details.
46
+
47
+ Returns dict with:
48
+ installed: bool
49
+ version: str or None
50
+ config_path: Path or None
51
+ gateway_port: int
52
+ process_running: bool
53
+ gateway_active: bool
54
+ skills_dir: Path or None
55
+ """
56
+ info = {
57
+ "installed": False,
58
+ "version": None,
59
+ "config_path": None,
60
+ "gateway_port": OPENCLAW_DEFAULT_PORT,
61
+ "process_running": False,
62
+ "gateway_active": False,
63
+ "skills_dir": None,
64
+ }
65
+
66
+ # Check npm global installation
67
+ try:
68
+ proc = subprocess.run(
69
+ ["npm", "list", "-g", "openclaw", "--json"],
70
+ capture_output=True,
71
+ text=True,
72
+ timeout=10,
73
+ )
74
+ if proc.returncode == 0:
75
+ data = json.loads(proc.stdout)
76
+ deps = data.get("dependencies", {})
77
+ if "openclaw" in deps:
78
+ info["installed"] = True
79
+ info["version"] = deps["openclaw"].get("version")
80
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
81
+ pass
82
+
83
+ # Check which/where
84
+ if not info["installed"]:
85
+ try:
86
+ import os
87
+ cmd = ["which", "openclaw"] if os.name != "nt" else ["where", "openclaw"]
88
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=5)
89
+ if proc.returncode == 0 and proc.stdout.strip():
90
+ info["installed"] = True
91
+ except (subprocess.TimeoutExpired, FileNotFoundError):
92
+ pass
93
+
94
+ # Check for OpenClaw home directory
95
+ if OPENCLAW_HOME.exists():
96
+ info["installed"] = True
97
+
98
+ # Check for skills directory
99
+ if OPENCLAW_SKILLS_DIR.exists():
100
+ info["skills_dir"] = OPENCLAW_SKILLS_DIR
101
+
102
+ # Check for config file and extract port
103
+ if OPENCLAW_CONFIG.exists():
104
+ info["config_path"] = OPENCLAW_CONFIG
105
+ try:
106
+ with open(OPENCLAW_CONFIG) as f:
107
+ config = json.load(f)
108
+ port = config.get("gateway", {}).get("port")
109
+ if port:
110
+ info["gateway_port"] = port
111
+ except (json.JSONDecodeError, IOError):
112
+ pass
113
+
114
+ # Check for running process
115
+ try:
116
+ proc = subprocess.run(
117
+ ["pgrep", "-f", "openclaw"],
118
+ capture_output=True,
119
+ text=True,
120
+ timeout=5,
121
+ )
122
+ if proc.returncode == 0 and proc.stdout.strip():
123
+ info["process_running"] = True
124
+ except (subprocess.TimeoutExpired, FileNotFoundError):
125
+ pass
126
+
127
+ # Check if gateway port is active
128
+ import socket
129
+ try:
130
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
131
+ s.settimeout(1)
132
+ result = s.connect_ex(("127.0.0.1", info["gateway_port"]))
133
+ info["gateway_active"] = result == 0
134
+ except (socket.error, OSError):
135
+ pass
136
+
137
+ return info
138
+
139
+
140
+ def _check_plugin_installed() -> bool:
141
+ """Check if the Tweek OpenClaw plugin is already installed."""
142
+ try:
143
+ proc = subprocess.run(
144
+ ["openclaw", "plugins", "list", "--json"],
145
+ capture_output=True,
146
+ text=True,
147
+ timeout=10,
148
+ )
149
+ if proc.returncode == 0:
150
+ data = json.loads(proc.stdout)
151
+ plugins = data.get("plugins", [])
152
+ return any(
153
+ p.get("name") == OPENCLAW_PLUGIN_NAME
154
+ or p.get("name") == "tweek-security"
155
+ for p in plugins
156
+ )
157
+ except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
158
+ pass
159
+ return False
160
+
161
+
162
+ def _install_plugin() -> tuple:
163
+ """
164
+ Install the Tweek plugin into OpenClaw.
165
+
166
+ Returns:
167
+ (success: bool, error: str or None)
168
+ """
169
+ try:
170
+ proc = subprocess.run(
171
+ ["openclaw", "plugins", "install", OPENCLAW_PLUGIN_NAME],
172
+ capture_output=True,
173
+ text=True,
174
+ timeout=60,
175
+ )
176
+ if proc.returncode == 0:
177
+ return True, None
178
+ return False, f"Plugin install failed: {proc.stderr.strip()}"
179
+ except subprocess.TimeoutExpired:
180
+ return False, "Plugin install timed out"
181
+ except FileNotFoundError:
182
+ return False, "openclaw CLI not found"
183
+
184
+
185
+ def _write_openclaw_config(
186
+ gateway_port: int,
187
+ scanner_port: int,
188
+ preset: str,
189
+ ) -> tuple:
190
+ """
191
+ Write or update the Tweek plugin configuration in openclaw.json.
192
+
193
+ Returns:
194
+ (config_path: str or None, error: str or None)
195
+ """
196
+ config = {}
197
+ if OPENCLAW_CONFIG.exists():
198
+ try:
199
+ with open(OPENCLAW_CONFIG) as f:
200
+ config = json.load(f)
201
+ except (json.JSONDecodeError, IOError):
202
+ config = {}
203
+
204
+ # Build preset-specific settings
205
+ preset_configs = {
206
+ "trusted": {
207
+ "skillGuard": {
208
+ "enabled": True,
209
+ "mode": "fingerprint_only",
210
+ "blockDangerous": False,
211
+ "promptSuspicious": False,
212
+ },
213
+ "toolScreening": {
214
+ "enabled": True,
215
+ "llmReview": False,
216
+ },
217
+ "outputScanning": {
218
+ "enabled": False,
219
+ },
220
+ },
221
+ "cautious": {
222
+ "skillGuard": {
223
+ "enabled": True,
224
+ "mode": "auto",
225
+ "blockDangerous": True,
226
+ "promptSuspicious": True,
227
+ },
228
+ "toolScreening": {
229
+ "enabled": True,
230
+ "llmReview": True,
231
+ "tiers": {
232
+ "bash": "dangerous",
233
+ "file_write": "risky",
234
+ "web_fetch": "risky",
235
+ "mcp_tool": "default",
236
+ },
237
+ },
238
+ "outputScanning": {
239
+ "enabled": True,
240
+ "secretDetection": True,
241
+ "exfiltrationDetection": True,
242
+ },
243
+ },
244
+ "paranoid": {
245
+ "skillGuard": {
246
+ "enabled": True,
247
+ "mode": "manual",
248
+ "blockDangerous": True,
249
+ "promptSuspicious": True,
250
+ },
251
+ "toolScreening": {
252
+ "enabled": True,
253
+ "llmReview": True,
254
+ "tiers": {
255
+ "bash": "dangerous",
256
+ "file_write": "dangerous",
257
+ "web_fetch": "dangerous",
258
+ "mcp_tool": "risky",
259
+ },
260
+ },
261
+ "outputScanning": {
262
+ "enabled": True,
263
+ "secretDetection": True,
264
+ "exfiltrationDetection": True,
265
+ },
266
+ },
267
+ }
268
+
269
+ preset_config = preset_configs.get(preset, preset_configs["cautious"])
270
+
271
+ # Merge into existing config
272
+ plugins = config.setdefault("plugins", {})
273
+ entries = plugins.setdefault("entries", {})
274
+ entries["tweek"] = {
275
+ "enabled": True,
276
+ "config": {
277
+ "preset": preset,
278
+ "scannerPort": scanner_port,
279
+ **preset_config,
280
+ },
281
+ }
282
+
283
+ try:
284
+ OPENCLAW_HOME.mkdir(parents=True, exist_ok=True)
285
+ with open(OPENCLAW_CONFIG, "w") as f:
286
+ json.dump(config, f, indent=2)
287
+ return str(OPENCLAW_CONFIG), None
288
+ except IOError as e:
289
+ return None, f"Failed to write config: {e}"
290
+
291
+
292
+ def setup_openclaw_protection(
293
+ port: Optional[int] = None,
294
+ preset: str = "cautious",
295
+ skip_plugin_install: bool = False,
296
+ ) -> OpenClawSetupResult:
297
+ """
298
+ Configure Tweek to protect OpenClaw Gateway.
299
+
300
+ This is the main entry point for the Tweek-first installation path.
301
+ Detects OpenClaw, installs the plugin, writes configuration, and
302
+ prepares the scanning server.
303
+
304
+ Args:
305
+ port: Override OpenClaw gateway port (default: auto-detect)
306
+ preset: Security preset to apply (paranoid, cautious, trusted)
307
+ skip_plugin_install: Skip npm plugin install (for ClawHub-first path)
308
+
309
+ Returns:
310
+ OpenClawSetupResult with setup details
311
+ """
312
+ result = OpenClawSetupResult(preset=preset)
313
+
314
+ # 1. Detect OpenClaw
315
+ openclaw = detect_openclaw_installation()
316
+ result.openclaw_detected = openclaw["installed"]
317
+ result.openclaw_version = openclaw["version"]
318
+
319
+ if not openclaw["installed"]:
320
+ result.error = "OpenClaw not detected on this system"
321
+ return result
322
+
323
+ # 2. Resolve gateway port
324
+ if port is not None:
325
+ result.gateway_port = port
326
+ else:
327
+ result.gateway_port = openclaw["gateway_port"]
328
+
329
+ result.gateway_running = openclaw["gateway_active"]
330
+
331
+ # 3. Install plugin (unless skipped for ClawHub-first path)
332
+ if not skip_plugin_install:
333
+ if _check_plugin_installed():
334
+ result.plugin_installed = True
335
+ result.warnings.append("Tweek plugin already installed in OpenClaw")
336
+ else:
337
+ success, error = _install_plugin()
338
+ if success:
339
+ result.plugin_installed = True
340
+ else:
341
+ result.warnings.append(f"Plugin install: {error}")
342
+ result.warnings.append(
343
+ "You can install manually: "
344
+ f"openclaw plugins install {OPENCLAW_PLUGIN_NAME}"
345
+ )
346
+
347
+ # 4. Check scanner port availability
348
+ import socket
349
+ try:
350
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
351
+ s.settimeout(1)
352
+ if s.connect_ex(("127.0.0.1", SCANNER_SERVER_PORT)) == 0:
353
+ result.warnings.append(
354
+ f"Port {SCANNER_SERVER_PORT} is already in use. "
355
+ "Scanner server may need a different port."
356
+ )
357
+ except (socket.error, OSError):
358
+ pass
359
+
360
+ # 5. Write OpenClaw config
361
+ config_path, error = _write_openclaw_config(
362
+ gateway_port=result.gateway_port,
363
+ scanner_port=result.scanner_port,
364
+ preset=preset,
365
+ )
366
+ if error:
367
+ result.error = error
368
+ return result
369
+ result.config_path = config_path
370
+
371
+ # 6. Update Tweek's own config to know about OpenClaw
372
+ tweek_dir = Path.home() / ".tweek"
373
+ tweek_dir.mkdir(parents=True, exist_ok=True)
374
+ tweek_config_path = tweek_dir / "config.yaml"
375
+
376
+ try:
377
+ import yaml
378
+ except ImportError:
379
+ yaml = None
380
+
381
+ tweek_config = {}
382
+ if tweek_config_path.exists():
383
+ try:
384
+ if yaml:
385
+ with open(tweek_config_path) as f:
386
+ tweek_config = yaml.safe_load(f) or {}
387
+ else:
388
+ tweek_config = {}
389
+ except Exception:
390
+ tweek_config = {}
391
+
392
+ tweek_config["openclaw"] = {
393
+ "enabled": True,
394
+ "gateway_port": result.gateway_port,
395
+ "scanner_port": result.scanner_port,
396
+ "preset": preset,
397
+ "plugin_installed": result.plugin_installed,
398
+ }
399
+
400
+ try:
401
+ if yaml:
402
+ with open(tweek_config_path, "w") as f:
403
+ yaml.dump(tweek_config, f, default_flow_style=False)
404
+ else:
405
+ # Manual YAML writing as fallback
406
+ existing_lines = []
407
+ if tweek_config_path.exists():
408
+ existing_content = tweek_config_path.read_text()
409
+ # Remove any existing openclaw section
410
+ in_openclaw = False
411
+ for line in existing_content.splitlines():
412
+ if line.startswith("openclaw:"):
413
+ in_openclaw = True
414
+ continue
415
+ if in_openclaw and (line.startswith(" ") or not line.strip()):
416
+ continue
417
+ in_openclaw = False
418
+ existing_lines.append(line)
419
+
420
+ openclaw_lines = [
421
+ "openclaw:",
422
+ " enabled: true",
423
+ f" gateway_port: {result.gateway_port}",
424
+ f" scanner_port: {result.scanner_port}",
425
+ f" preset: {preset}",
426
+ f" plugin_installed: {'true' if result.plugin_installed else 'false'}",
427
+ ]
428
+
429
+ all_lines = existing_lines + openclaw_lines
430
+ tweek_config_path.write_text("\n".join(all_lines) + "\n")
431
+ except Exception as e:
432
+ result.warnings.append(f"Could not update Tweek config: {e}")
433
+
434
+ # 7. Apply security preset
435
+ try:
436
+ from tweek.config.manager import ConfigManager
437
+ cfg = ConfigManager()
438
+ cfg.apply_preset(preset)
439
+ except Exception as e:
440
+ result.warnings.append(f"Could not apply preset: {e}")
441
+
442
+ result.success = True
443
+ return result