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_logs.py ADDED
@@ -0,0 +1,301 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek CLI — Logs command group.
4
+
5
+ Extracted from cli.py to keep the main CLI module manageable.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+ import click
13
+
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+
17
+ from tweek.cli_helpers import console, TWEEK_BANNER
18
+
19
+
20
+ # ============================================================
21
+ # LOGS COMMANDS
22
+ # ============================================================
23
+
24
+ @click.group()
25
+ def logs():
26
+ """View and manage security logs."""
27
+ pass
28
+
29
+
30
+ @logs.command("show",
31
+ epilog="""\b
32
+ Examples:
33
+ tweek logs show Show last 20 security events
34
+ tweek logs show -n 50 Show last 50 events
35
+ tweek logs show --type block Filter by event type
36
+ tweek logs show --blocked Show only blocked/flagged events
37
+ tweek logs show --stats Show security statistics summary
38
+ tweek logs show --stats --days 30 Statistics for the last 30 days
39
+ """
40
+ )
41
+ @click.option("--limit", "-n", default=20, help="Number of events to show")
42
+ @click.option("--type", "-t", "event_type", help="Filter by event type")
43
+ @click.option("--tool", help="Filter by tool name")
44
+ @click.option("--blocked", is_flag=True, help="Show only blocked/flagged events")
45
+ @click.option("--stats", is_flag=True, help="Show security statistics instead of events")
46
+ @click.option("--days", "-d", default=7, help="Number of days to analyze (with --stats)")
47
+ def logs_show(limit: int, event_type: str, tool: str, blocked: bool, stats: bool, days: int):
48
+ """Show recent security events."""
49
+ from tweek.logging.security_log import get_logger
50
+
51
+ console.print(TWEEK_BANNER, style="cyan")
52
+
53
+ logger = get_logger()
54
+
55
+ # Handle stats mode
56
+ if stats:
57
+ stat_data = logger.get_stats(days=days)
58
+
59
+ console.print(Panel.fit(
60
+ f"[cyan]Period:[/cyan] Last {days} days\n"
61
+ f"[cyan]Total Events:[/cyan] {stat_data['total_events']}",
62
+ title="Security Statistics"
63
+ ))
64
+
65
+ # Decisions breakdown
66
+ if stat_data['by_decision']:
67
+ table = Table(title="Decisions")
68
+ table.add_column("Decision", style="cyan")
69
+ table.add_column("Count", justify="right")
70
+
71
+ decision_styles = {"allow": "green", "block": "red", "ask": "yellow", "deny": "red"}
72
+ for decision, count in stat_data['by_decision'].items():
73
+ style = decision_styles.get(decision, "white")
74
+ table.add_row(f"[{style}]{decision}[/{style}]", str(count))
75
+
76
+ console.print(table)
77
+ console.print()
78
+
79
+ # Top triggered patterns
80
+ if stat_data['top_patterns']:
81
+ table = Table(title="Top Triggered Patterns")
82
+ table.add_column("Pattern", style="cyan")
83
+ table.add_column("Severity")
84
+ table.add_column("Count", justify="right")
85
+
86
+ severity_styles = {"critical": "red", "high": "yellow", "medium": "blue", "low": "white"}
87
+ for pattern in stat_data['top_patterns']:
88
+ sev = pattern['severity'] or "unknown"
89
+ style = severity_styles.get(sev, "white")
90
+ table.add_row(
91
+ pattern['name'] or "unknown",
92
+ f"[{style}]{sev}[/{style}]",
93
+ str(pattern['count'])
94
+ )
95
+
96
+ console.print(table)
97
+ console.print()
98
+
99
+ # By tool
100
+ if stat_data['by_tool']:
101
+ table = Table(title="Events by Tool")
102
+ table.add_column("Tool", style="green")
103
+ table.add_column("Count", justify="right")
104
+
105
+ for tool_name, count in stat_data['by_tool'].items():
106
+ table.add_row(tool_name, str(count))
107
+
108
+ console.print(table)
109
+ return
110
+
111
+ from tweek.logging.security_log import EventType
112
+
113
+ if blocked:
114
+ events = logger.get_blocked_commands(limit=limit)
115
+ title = "Recent Blocked/Flagged Commands"
116
+ else:
117
+ et = None
118
+ if event_type:
119
+ try:
120
+ et = EventType(event_type)
121
+ except ValueError:
122
+ console.print(f"[red]Unknown event type: {event_type}[/red]")
123
+ console.print(f"[white]Valid types: {', '.join(e.value for e in EventType)}[/white]")
124
+ return
125
+
126
+ events = logger.get_recent_events(limit=limit, event_type=et, tool_name=tool)
127
+ title = "Recent Security Events"
128
+
129
+ if not events:
130
+ console.print("[yellow]No events found[/yellow]")
131
+ return
132
+
133
+ table = Table(title=title)
134
+ table.add_column("Time", style="white")
135
+ table.add_column("Type", style="cyan")
136
+ table.add_column("Tool", style="green")
137
+ table.add_column("Tier")
138
+ table.add_column("Decision")
139
+ table.add_column("Pattern/Reason", max_width=30)
140
+
141
+ decision_styles = {
142
+ "allow": "green",
143
+ "block": "red",
144
+ "ask": "yellow",
145
+ "deny": "red",
146
+ }
147
+
148
+ for event in events:
149
+ timestamp = event.get("timestamp", "")
150
+ if timestamp:
151
+ # Format timestamp nicely
152
+ try:
153
+ dt = datetime.fromisoformat(timestamp)
154
+ timestamp = dt.strftime("%m/%d %H:%M:%S")
155
+ except (ValueError, TypeError):
156
+ pass
157
+
158
+ decision = event.get("decision", "")
159
+ decision_style = decision_styles.get(decision, "white")
160
+
161
+ reason = event.get("pattern_name") or event.get("decision_reason", "")
162
+ if len(str(reason)) > 30:
163
+ reason = str(reason)[:27] + "..."
164
+
165
+ table.add_row(
166
+ timestamp,
167
+ event.get("event_type", ""),
168
+ event.get("tool_name", ""),
169
+ event.get("tier", ""),
170
+ f"[{decision_style}]{decision}[/{decision_style}]" if decision else "",
171
+ str(reason)
172
+ )
173
+
174
+ console.print(table)
175
+ console.print(f"\n[white]Showing {len(events)} events. Use --limit to see more.[/white]")
176
+
177
+
178
+ @logs.command("export",
179
+ epilog="""\b
180
+ Examples:
181
+ tweek logs export Export all logs to tweek_security_log.csv
182
+ tweek logs export --days 7 Export only the last 7 days
183
+ tweek logs export -o audit.csv Export to a custom file path
184
+ tweek logs export --days 30 -o monthly.csv Last 30 days to custom file
185
+ """
186
+ )
187
+ @click.option("--days", "-d", type=int, help="Limit to last N days")
188
+ @click.option("--output", "-o", default="tweek_security_log.csv", help="Output file path")
189
+ def logs_export(days: int, output: str):
190
+ """Export security logs to CSV."""
191
+ from tweek.logging.security_log import get_logger
192
+
193
+ logger = get_logger()
194
+ output_path = Path(output)
195
+
196
+ count = logger.export_csv(output_path, days=days)
197
+
198
+ if count > 0:
199
+ console.print(f"[green]\u2713[/green] Exported {count} events to {output_path}")
200
+ else:
201
+ console.print("[yellow]No events to export[/yellow]")
202
+
203
+
204
+ @logs.command("clear",
205
+ epilog="""\b
206
+ Examples:
207
+ tweek logs clear Clear all security logs (with prompt)
208
+ tweek logs clear --days 30 Clear logs older than 30 days
209
+ tweek logs clear --confirm Clear all logs without confirmation
210
+ """
211
+ )
212
+ @click.option("--days", "-d", type=int, help="Clear events older than N days")
213
+ @click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
214
+ def logs_clear(days: int, confirm: bool):
215
+ """Clear security logs."""
216
+ from tweek.logging.security_log import get_logger
217
+
218
+ if not confirm:
219
+ if days:
220
+ msg = f"Clear all events older than {days} days?"
221
+ else:
222
+ msg = "Clear ALL security logs?"
223
+
224
+ console.print(f"[yellow]{msg}[/yellow] ", end="")
225
+ if not click.confirm(""):
226
+ console.print("[white]Cancelled[/white]")
227
+ return
228
+
229
+ logger = get_logger()
230
+ deleted = logger.delete_events(days=days)
231
+
232
+ if deleted > 0:
233
+ if days:
234
+ console.print(f"[green]Cleared {deleted} event(s) older than {days} days[/green]")
235
+ else:
236
+ console.print(f"[green]Cleared {deleted} event(s)[/green]")
237
+ else:
238
+ console.print("[white]No events to clear[/white]")
239
+
240
+
241
+ @logs.command("bundle",
242
+ epilog="""\b
243
+ Examples:
244
+ tweek logs bundle Create diagnostic bundle
245
+ tweek logs bundle -o /tmp/diag.zip Specify output path
246
+ tweek logs bundle --days 7 Only last 7 days of events
247
+ tweek logs bundle --dry-run Show what would be collected
248
+ """
249
+ )
250
+ @click.option("--output", "-o", type=click.Path(), help="Output zip file path")
251
+ @click.option("--days", "-d", type=int, help="Only include events from last N days")
252
+ @click.option("--no-redact", is_flag=True, help="Skip redaction (for internal debugging)")
253
+ @click.option("--dry-run", is_flag=True, help="Show what would be collected")
254
+ def logs_bundle(output: str, days: int, no_redact: bool, dry_run: bool):
255
+ """Create a diagnostic bundle for support.
256
+
257
+ Collects security logs, configs (redacted), system info, and
258
+ doctor output into a zip file suitable for sending to Tweek support.
259
+
260
+ Sensitive data (API keys, passwords, tokens) is automatically
261
+ redacted before inclusion.
262
+ """
263
+ from tweek.logging.bundle import BundleCollector
264
+
265
+ collector = BundleCollector(redact=not no_redact, days=days)
266
+
267
+ if dry_run:
268
+ report = collector.get_dry_run_report()
269
+ console.print("[bold]Diagnostic Bundle - Dry Run[/bold]\n")
270
+ for item in report:
271
+ status = item.get("status", "unknown")
272
+ name = item.get("file", "?")
273
+ size = item.get("size")
274
+ size_str = f" ({size:,} bytes)" if size else ""
275
+ if "not found" in status:
276
+ console.print(f" [white] SKIP {name} ({status})[/white]")
277
+ else:
278
+ console.print(f" [green] ADD {name}{size_str}[/green]")
279
+ console.print()
280
+ console.print("[white]No files will be collected in dry-run mode.[/white]")
281
+ return
282
+
283
+ # Determine output path
284
+ if not output:
285
+ ts = datetime.now().strftime("%Y-%m-%d_%H%M%S")
286
+ output = f"tweek_diagnostic_bundle_{ts}.zip"
287
+
288
+ output_path = Path(output)
289
+
290
+ console.print("[bold]Creating diagnostic bundle...[/bold]")
291
+
292
+ try:
293
+ result = collector.create_bundle(output_path)
294
+ size = result.stat().st_size
295
+ console.print(f"\n[green]Bundle created: {result}[/green]")
296
+ console.print(f"[white]Size: {size:,} bytes[/white]")
297
+ if not no_redact:
298
+ console.print("[white]Sensitive data has been redacted.[/white]")
299
+ console.print(f"\n[bold]Send this file to Tweek support for analysis.[/bold]")
300
+ except Exception as e:
301
+ console.print(f"[red]Failed to create bundle: {e}[/red]")
tweek/cli_mcp.py ADDED
@@ -0,0 +1,148 @@
1
+ """MCP Proxy CLI commands extracted from tweek.cli.
2
+
3
+ Provides the ``mcp`` Click group with subcommands: proxy, approve, decide.
4
+ """
5
+
6
+ import asyncio
7
+
8
+ import click
9
+
10
+ from tweek.cli_helpers import console
11
+
12
+ # =============================================================================
13
+ # MCP GATEWAY COMMANDS
14
+ # =============================================================================
15
+
16
+
17
+ @click.group()
18
+ def mcp():
19
+ """MCP Security Proxy for desktop LLM applications.
20
+
21
+ Provides security-screened MCP proxy with built-in vault and status tools.
22
+ Supports Claude Desktop, ChatGPT Desktop, and Gemini CLI.
23
+ """
24
+ pass
25
+
26
+ @mcp.command("proxy",
27
+ epilog="""\b
28
+ Examples:
29
+ tweek mcp proxy Start MCP proxy on stdio transport
30
+ """
31
+ )
32
+ def mcp_proxy():
33
+ """Start MCP proxy server (stdio transport).
34
+
35
+ Connects to upstream MCP servers configured in config.yaml,
36
+ screens all tool calls through Tweek's security pipeline,
37
+ and queues flagged operations for human approval.
38
+
39
+ Configure upstreams in ~/.tweek/config.yaml:
40
+ mcp:
41
+ proxy:
42
+ upstreams:
43
+ filesystem:
44
+ command: "npx"
45
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
46
+
47
+ Example Claude Desktop config:
48
+ {"mcpServers": {"tweek-proxy": {"command": "tweek", "args": ["mcp", "proxy"]}}}
49
+ """
50
+ try:
51
+ from tweek.mcp.proxy import run_proxy, MCP_AVAILABLE
52
+
53
+ if not MCP_AVAILABLE:
54
+ console.print("[red]MCP SDK not installed.[/red]")
55
+ console.print("Install with: pip install 'tweek[mcp]' or pip install mcp")
56
+ return
57
+
58
+ # Load config
59
+ try:
60
+ from tweek.config.manager import ConfigManager
61
+ cfg = ConfigManager()
62
+ config = cfg.get_full_config()
63
+ except Exception:
64
+ config = {}
65
+
66
+ asyncio.run(run_proxy(config=config))
67
+
68
+ except KeyboardInterrupt:
69
+ pass
70
+ except Exception as e:
71
+ console.print(f"[red]MCP proxy error: {e}[/red]")
72
+
73
+
74
+ @mcp.command("approve",
75
+ epilog="""\b
76
+ Examples:
77
+ tweek mcp approve Start approval daemon (interactive)
78
+ tweek mcp approve --list List pending requests and exit
79
+ tweek mcp approve -p 5 Poll every 5 seconds
80
+ """
81
+ )
82
+ @click.option("--poll-interval", "-p", default=2.0, type=float,
83
+ help="Seconds between polls for new requests")
84
+ @click.option("--list", "list_pending", is_flag=True, help="List pending requests and exit")
85
+ def mcp_approve(poll_interval, list_pending):
86
+ """Start the approval daemon for MCP proxy requests.
87
+
88
+ Shows pending requests and allows approve/deny decisions.
89
+ Press Ctrl+C to exit.
90
+
91
+ Run this in a separate terminal while 'tweek mcp proxy' is serving.
92
+ Use --list to show pending requests without starting the daemon.
93
+ """
94
+ if list_pending:
95
+ try:
96
+ from tweek.mcp.approval import ApprovalQueue
97
+ from tweek.mcp.approval_cli import display_pending
98
+ queue = ApprovalQueue()
99
+ display_pending(queue)
100
+ except Exception as e:
101
+ console.print(f"[red]Error: {e}[/red]")
102
+ return
103
+
104
+ try:
105
+ from tweek.mcp.approval import ApprovalQueue
106
+ from tweek.mcp.approval_cli import run_approval_daemon
107
+
108
+ queue = ApprovalQueue()
109
+ run_approval_daemon(queue, poll_interval=poll_interval)
110
+
111
+ except KeyboardInterrupt:
112
+ pass
113
+ except Exception as e:
114
+ console.print(f"[red]Approval daemon error: {e}[/red]")
115
+
116
+
117
+ @mcp.command("decide",
118
+ epilog="""\b
119
+ Examples:
120
+ tweek mcp decide abc12345 approve Approve a request
121
+ tweek mcp decide abc12345 deny Deny a request
122
+ tweek mcp decide abc12345 deny -n "Not authorized" Deny with notes
123
+ """
124
+ )
125
+ @click.argument("request_id")
126
+ @click.argument("decision", type=click.Choice(["approve", "deny"]))
127
+ @click.option("--notes", "-n", help="Decision notes")
128
+ def mcp_decide(request_id, decision, notes):
129
+ """Approve or deny a specific approval request.
130
+
131
+ REQUEST_ID can be the full UUID or the first 8 characters.
132
+ """
133
+ try:
134
+ from tweek.mcp.approval import ApprovalQueue
135
+ from tweek.mcp.approval_cli import decide_request
136
+
137
+ queue = ApprovalQueue()
138
+ success = decide_request(queue, request_id, decision, notes=notes)
139
+
140
+ if success:
141
+ verb = "Approved" if decision == "approve" else "Denied"
142
+ style = "green" if decision == "approve" else "red"
143
+ console.print(f"[{style}]{verb} request {request_id}[/{style}]")
144
+ else:
145
+ console.print(f"[yellow]Could not {decision} request {request_id}[/yellow]")
146
+
147
+ except Exception as e:
148
+ console.print(f"[red]Error: {e}[/red]")