tweek 0.3.0__py3-none-any.whl → 0.4.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 (63) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6559
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/__init__.py +8 -0
  21. tweek/config/allowed_dirs.yaml +16 -17
  22. tweek/config/families.yaml +4 -1
  23. tweek/config/manager.py +49 -0
  24. tweek/config/models.py +307 -0
  25. tweek/config/patterns.yaml +29 -5
  26. tweek/config/templates/config.yaml.template +212 -0
  27. tweek/config/templates/env.template +45 -0
  28. tweek/config/templates/overrides.yaml.template +121 -0
  29. tweek/config/templates/tweek.yaml.template +20 -0
  30. tweek/config/templates.py +136 -0
  31. tweek/config/tiers.yaml +5 -4
  32. tweek/diagnostics.py +112 -32
  33. tweek/hooks/overrides.py +4 -0
  34. tweek/hooks/post_tool_use.py +46 -1
  35. tweek/hooks/pre_tool_use.py +149 -49
  36. tweek/integrations/openclaw.py +84 -0
  37. tweek/licensing.py +1 -1
  38. tweek/mcp/__init__.py +7 -9
  39. tweek/mcp/clients/chatgpt.py +2 -2
  40. tweek/mcp/clients/claude_desktop.py +2 -2
  41. tweek/mcp/clients/gemini.py +2 -2
  42. tweek/mcp/proxy.py +165 -1
  43. tweek/memory/provenance.py +438 -0
  44. tweek/memory/queries.py +2 -0
  45. tweek/memory/safety.py +23 -4
  46. tweek/memory/schemas.py +1 -0
  47. tweek/memory/store.py +101 -71
  48. tweek/plugins/screening/heuristic_scorer.py +1 -1
  49. tweek/security/integrity.py +77 -0
  50. tweek/security/llm_reviewer.py +162 -68
  51. tweek/security/local_reviewer.py +44 -2
  52. tweek/security/model_registry.py +73 -7
  53. tweek/skill_template/overrides-reference.md +1 -1
  54. tweek/skills/context.py +221 -0
  55. tweek/skills/scanner.py +2 -2
  56. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/METADATA +9 -7
  57. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/RECORD +62 -39
  58. tweek/mcp/server.py +0 -320
  59. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
  60. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
  61. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
  62. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
  63. {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
tweek/cli_configure.py ADDED
@@ -0,0 +1,413 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek CLI Configure Command
4
+
5
+ Post-install configuration for Tweek components that are not required
6
+ during the initial quick install. Each subcommand handles one aspect
7
+ of configuration independently.
8
+
9
+ Usage:
10
+ tweek configure Show available configuration options
11
+ tweek configure llm Configure LLM review provider
12
+ tweek configure preset Change security preset
13
+ tweek configure vault Scan .env files and migrate to vault
14
+ tweek configure proxy Set up proxy for OpenClaw or other tools
15
+ tweek configure mcp Protect MCP-capable AI tools
16
+ tweek configure sandbox Set up Linux sandbox (firejail)
17
+ tweek configure wizard Run the full interactive setup wizard
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import click
22
+
23
+ from tweek.cli_helpers import console, print_success, print_warning
24
+
25
+
26
+ @click.group()
27
+ def configure():
28
+ """Configure Tweek components after installation.
29
+
30
+ Run 'tweek configure' to see available options. Each subcommand
31
+ handles one aspect of configuration independently.
32
+ """
33
+ pass
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # tweek configure llm
38
+ # ---------------------------------------------------------------------------
39
+
40
+ @configure.command()
41
+ def llm():
42
+ """Configure the LLM review provider for semantic analysis.
43
+
44
+ Sets up the cloud or local LLM used for Layer 3 security screening.
45
+ Supports Anthropic, OpenAI, Google, xAI, or any OpenAI-compatible endpoint.
46
+ """
47
+ from pathlib import Path
48
+
49
+ tweek_dir = Path("~/.tweek").expanduser()
50
+ tweek_dir.mkdir(parents=True, exist_ok=True)
51
+
52
+ from tweek.cli_install import _configure_llm_provider
53
+ result = _configure_llm_provider(tweek_dir, interactive=True, quick=False)
54
+
55
+ if result.get("provider_display"):
56
+ console.print()
57
+ print_success(f"LLM provider: {result['provider_display']}")
58
+ if result.get("model_display"):
59
+ console.print(f" Model: {result['model_display']}")
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # tweek configure preset
64
+ # ---------------------------------------------------------------------------
65
+
66
+ @configure.command()
67
+ @click.argument("name", required=False,
68
+ type=click.Choice(["paranoid", "cautious", "balanced", "trusted"]))
69
+ def preset(name):
70
+ """Change the security preset.
71
+
72
+ Presets control tool security tiers and enforcement thresholds.
73
+
74
+ \b
75
+ Presets:
76
+ paranoid Maximum security, prompt on everything
77
+ balanced Smart defaults with provenance tracking (recommended)
78
+ cautious Prompt on risky operations
79
+ trusted Minimal prompts, trust AI decisions
80
+ """
81
+ from tweek.config.manager import ConfigManager
82
+
83
+ cfg = ConfigManager()
84
+
85
+ if name is None:
86
+ # Interactive selection
87
+ console.print("[bold]Security Presets[/bold]")
88
+ console.print()
89
+ console.print(" [cyan]1.[/cyan] paranoid \u2014 Maximum security, prompt on everything")
90
+ console.print(" [cyan]2.[/cyan] balanced \u2014 Smart defaults with provenance tracking [green](recommended)[/green]")
91
+ console.print(" [cyan]3.[/cyan] cautious \u2014 Prompt on risky operations")
92
+ console.print(" [cyan]4.[/cyan] trusted \u2014 Minimal prompts, trust AI decisions")
93
+ console.print()
94
+
95
+ choice = click.prompt("Select preset", type=click.IntRange(1, 4), default=2)
96
+ name = {1: "paranoid", 2: "balanced", 3: "cautious", 4: "trusted"}[choice]
97
+
98
+ cfg.apply_preset(name)
99
+ print_success(f"Applied {name} preset")
100
+ if name == "balanced":
101
+ console.print("[white] Clean sessions get fewer prompts; tainted sessions get extra scrutiny[/white]")
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # tweek configure vault
106
+ # ---------------------------------------------------------------------------
107
+
108
+ @configure.command()
109
+ @click.option("--dry-run", is_flag=True, help="Show what would be migrated without making changes")
110
+ def vault(dry_run):
111
+ """Scan for .env files and migrate credentials to the secure vault.
112
+
113
+ Finds .env files in common locations, identifies credential keys,
114
+ and offers to migrate them to the system keychain.
115
+ """
116
+ from tweek.cli_install import scan_for_env_files
117
+ from rich.table import Table
118
+
119
+ console.print("[cyan]Scanning for .env files with credentials...[/cyan]\n")
120
+
121
+ env_files = scan_for_env_files()
122
+
123
+ if not env_files:
124
+ console.print("[white]No .env files with credentials found.[/white]")
125
+ return
126
+
127
+ table = Table(title="Found .env Files")
128
+ table.add_column("#", style="white")
129
+ table.add_column("Path")
130
+ table.add_column("Credentials", justify="right")
131
+
132
+ for i, (path, keys) in enumerate(env_files, 1):
133
+ from pathlib import Path as P
134
+ try:
135
+ display_path = path.relative_to(P.cwd())
136
+ except ValueError:
137
+ display_path = path
138
+ table.add_row(str(i), str(display_path), str(len(keys)))
139
+
140
+ console.print(table)
141
+
142
+ if dry_run:
143
+ console.print("\n[white]Dry run \u2014 no changes made.[/white]")
144
+ for path, keys in env_files:
145
+ console.print(f"\n {path}:")
146
+ for key in keys:
147
+ console.print(f" \u2022 {key}")
148
+ return
149
+
150
+ # Interactive migration
151
+ console.print("\n[yellow]Migrate these credentials to secure storage?[/yellow]")
152
+ if not click.confirm("Proceed?"):
153
+ console.print("[white]Skipped.[/white]")
154
+ return
155
+
156
+ try:
157
+ from tweek.vault import get_vault, VAULT_AVAILABLE
158
+ if not VAULT_AVAILABLE:
159
+ print_warning("Vault not available. Install keyring: pip install keyring")
160
+ return
161
+ vault_store = get_vault()
162
+ except ImportError:
163
+ print_warning("Vault module not available.")
164
+ return
165
+
166
+ for path, keys in env_files:
167
+ from pathlib import Path as P
168
+ try:
169
+ display_path = path.relative_to(P.cwd())
170
+ except ValueError:
171
+ display_path = path
172
+
173
+ console.print(f"\n[cyan]{display_path}[/cyan]")
174
+
175
+ suggested_skill = path.parent.name
176
+ if suggested_skill in (".", "", "~"):
177
+ suggested_skill = "default"
178
+
179
+ skill = click.prompt(" Skill name", default=suggested_skill)
180
+
181
+ console.print(f" [white]Credentials to migrate:[/white]")
182
+ for key in keys:
183
+ console.print(f" \u2022 {key}")
184
+
185
+ if click.confirm(f" Migrate {len(keys)} credentials to '{skill}'?"):
186
+ try:
187
+ from tweek.vault import migrate_env_to_vault
188
+ results = migrate_env_to_vault(path, skill, vault_store, dry_run=False)
189
+ successful = sum(1 for _, s in results if s)
190
+ print_success(f"Migrated {successful}/{len(results)} credentials")
191
+ except Exception as e:
192
+ print_warning(f"Migration failed: {e}")
193
+ else:
194
+ console.print(" [white]Skipped[/white]")
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # tweek configure proxy
199
+ # ---------------------------------------------------------------------------
200
+
201
+ @configure.command()
202
+ @click.option("--force", is_flag=True, help="Force proxy to override existing configurations")
203
+ def proxy(force):
204
+ """Set up Tweek proxy for OpenClaw or other API interceptors.
205
+
206
+ Detects OpenClaw and other proxy configurations, then offers to
207
+ set up Tweek as a security layer.
208
+ """
209
+ try:
210
+ from tweek.proxy import detect_proxy_conflicts, get_openclaw_status
211
+ except ImportError:
212
+ print_warning("Proxy module not available.")
213
+ return
214
+
215
+ try:
216
+ openclaw_status = get_openclaw_status()
217
+ except Exception as e:
218
+ print_warning(f"Could not check OpenClaw status: {e}")
219
+ return
220
+
221
+ if openclaw_status["installed"]:
222
+ console.print("[green]\u2713[/green] OpenClaw detected")
223
+ if openclaw_status["gateway_active"]:
224
+ console.print(f" Gateway running on port {openclaw_status['port']}")
225
+ elif openclaw_status["running"]:
226
+ console.print(f" Process running (port {openclaw_status['port']})")
227
+ else:
228
+ console.print(" Installed but not currently running")
229
+ console.print()
230
+
231
+ if force or click.confirm("Configure Tweek to protect OpenClaw?", default=True):
232
+ _apply_proxy_config(force=True)
233
+ else:
234
+ console.print("[white]Skipped. Run 'tweek protect openclaw' later.[/white]")
235
+ else:
236
+ console.print("[white]OpenClaw not detected on this system.[/white]")
237
+
238
+ # Check for other proxy conflicts
239
+ try:
240
+ conflicts = detect_proxy_conflicts()
241
+ non_openclaw = [c for c in conflicts if c.tool_name != "openclaw"]
242
+ if non_openclaw:
243
+ console.print("\n[yellow]Other proxy conflicts:[/yellow]")
244
+ for conflict in non_openclaw:
245
+ console.print(f" \u2022 {conflict.description}")
246
+ except Exception:
247
+ pass
248
+
249
+
250
+ def _apply_proxy_config(force: bool = False) -> None:
251
+ """Write proxy configuration to ~/.tweek/config.yaml."""
252
+ import yaml
253
+ from pathlib import Path
254
+
255
+ try:
256
+ from tweek.proxy import TWEEK_DEFAULT_PORT
257
+ except ImportError:
258
+ TWEEK_DEFAULT_PORT = 8766
259
+
260
+ tweek_dir = Path("~/.tweek").expanduser()
261
+ tweek_dir.mkdir(parents=True, exist_ok=True)
262
+ config_path = tweek_dir / "config.yaml"
263
+
264
+ if config_path.exists():
265
+ with open(config_path) as f:
266
+ config = yaml.safe_load(f) or {}
267
+ else:
268
+ config = {}
269
+
270
+ config["proxy"] = config.get("proxy", {})
271
+ config["proxy"]["enabled"] = True
272
+ config["proxy"]["port"] = TWEEK_DEFAULT_PORT
273
+ config["proxy"]["override_openclaw"] = force
274
+ config["proxy"]["auto_start"] = False
275
+
276
+ with open(config_path, "w") as f:
277
+ yaml.dump(config, f, default_flow_style=False)
278
+
279
+ print_success("Proxy configuration saved")
280
+ console.print(f" [white]Config: {config_path}[/white]")
281
+ console.print(" [white]Run 'tweek proxy start' to begin intercepting API calls[/white]")
282
+
283
+
284
+ # ---------------------------------------------------------------------------
285
+ # tweek configure mcp
286
+ # ---------------------------------------------------------------------------
287
+
288
+ @configure.command()
289
+ def mcp():
290
+ """Protect MCP-capable AI tools (Claude Desktop, ChatGPT, Gemini CLI).
291
+
292
+ Scans for installed MCP clients and offers to add Tweek as an MCP
293
+ security server for each one.
294
+ """
295
+ from tweek.cli_helpers import _detect_all_tools
296
+
297
+ try:
298
+ all_tools = _detect_all_tools()
299
+ except Exception as e:
300
+ print_warning(f"Could not detect AI tools: {e}")
301
+ return
302
+
303
+ # Filter to MCP-capable tools
304
+ mcp_tool_ids = {"claude-desktop", "chatgpt", "gemini"}
305
+ mcp_tools = [
306
+ (tool_id, label, installed, protected)
307
+ for tool_id, label, installed, protected, _detail in all_tools
308
+ if tool_id in mcp_tool_ids
309
+ ]
310
+
311
+ if not mcp_tools:
312
+ console.print("[white]No MCP-capable AI tools detected.[/white]")
313
+ return
314
+
315
+ console.print("[bold]MCP-Capable AI Tools[/bold]\n")
316
+
317
+ any_unprotected = False
318
+ for tool_id, label, installed, protected in mcp_tools:
319
+ if not installed:
320
+ console.print(f" [white]\u25cb[/white] {label} \u2014 not installed")
321
+ elif protected:
322
+ console.print(f" [green]\u2713[/green] {label} \u2014 protected")
323
+ else:
324
+ console.print(f" [yellow]\u26a0[/yellow] {label} \u2014 not protected")
325
+ any_unprotected = True
326
+
327
+ if not any_unprotected:
328
+ console.print("\n[white]All detected MCP tools are already protected.[/white]")
329
+ return
330
+
331
+ console.print()
332
+ unprotected = [
333
+ (tool_id, label)
334
+ for tool_id, label, installed, protected in mcp_tools
335
+ if installed and not protected
336
+ ]
337
+
338
+ for tool_id, label in unprotected:
339
+ if click.confirm(f" Protect {label}?", default=True):
340
+ try:
341
+ from tweek.cli_protect import _protect_mcp_client
342
+ _protect_mcp_client(tool_id)
343
+ print_success(f"{label} protected")
344
+ except Exception as e:
345
+ print_warning(f"Could not configure {label}: {e}")
346
+ else:
347
+ console.print(f" [white]Skipped {label}[/white]")
348
+
349
+
350
+ # ---------------------------------------------------------------------------
351
+ # tweek configure sandbox
352
+ # ---------------------------------------------------------------------------
353
+
354
+ @configure.command()
355
+ def sandbox():
356
+ """Set up Linux sandbox (firejail) for command isolation.
357
+
358
+ Only applicable on Linux systems. Checks if firejail is installed
359
+ and offers to install it if not.
360
+ """
361
+ from tweek.platform import IS_LINUX, get_capabilities
362
+
363
+ if not IS_LINUX:
364
+ console.print("[white]Sandbox is only available on Linux.[/white]")
365
+ console.print(f"[white]Your platform: {get_capabilities().platform.value}[/white]")
366
+ return
367
+
368
+ caps = get_capabilities()
369
+ if caps.sandbox_available:
370
+ print_success(f"Sandbox already available: {caps.sandbox_tool}")
371
+ return
372
+
373
+ console.print("[yellow]Sandbox (firejail) not installed.[/yellow]")
374
+ console.print(f"[white]Install with: {caps.sandbox_install_hint}[/white]")
375
+
376
+ try:
377
+ from tweek.sandbox.linux import prompt_install_firejail
378
+ prompt_install_firejail(console)
379
+ except ImportError:
380
+ console.print("[white]Run the install command shown above manually.[/white]")
381
+
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # tweek configure wizard
385
+ # ---------------------------------------------------------------------------
386
+
387
+ @configure.command()
388
+ @click.pass_context
389
+ def wizard(ctx):
390
+ """Run the full interactive setup wizard.
391
+
392
+ This is the same interactive experience as 'tweek install' without
393
+ the --quick flag. Use it to reconfigure all Tweek settings at once.
394
+ """
395
+ from tweek.cli_install import install
396
+
397
+ console.print("[bold]Starting full configuration wizard...[/bold]")
398
+ console.print("[white]This will walk through all configuration options.[/white]\n")
399
+
400
+ # Invoke the install command in interactive mode
401
+ ctx.invoke(
402
+ install,
403
+ scope=None,
404
+ preset=None,
405
+ quick=False,
406
+ backup=True,
407
+ skip_env_scan=False,
408
+ interactive=True,
409
+ ai_defaults=False,
410
+ with_sandbox=False,
411
+ force_proxy=False,
412
+ skip_proxy_check=False,
413
+ )