tweek 0.3.1__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 (61) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6605
  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/allowed_dirs.yaml +16 -17
  21. tweek/config/families.yaml +4 -1
  22. tweek/config/manager.py +17 -0
  23. tweek/config/patterns.yaml +29 -5
  24. tweek/config/templates/config.yaml.template +212 -0
  25. tweek/config/templates/env.template +45 -0
  26. tweek/config/templates/overrides.yaml.template +121 -0
  27. tweek/config/templates/tweek.yaml.template +20 -0
  28. tweek/config/templates.py +136 -0
  29. tweek/config/tiers.yaml +5 -4
  30. tweek/diagnostics.py +112 -32
  31. tweek/hooks/overrides.py +4 -0
  32. tweek/hooks/post_tool_use.py +46 -1
  33. tweek/hooks/pre_tool_use.py +149 -49
  34. tweek/integrations/openclaw.py +84 -0
  35. tweek/licensing.py +1 -1
  36. tweek/mcp/__init__.py +7 -9
  37. tweek/mcp/clients/chatgpt.py +2 -2
  38. tweek/mcp/clients/claude_desktop.py +2 -2
  39. tweek/mcp/clients/gemini.py +2 -2
  40. tweek/mcp/proxy.py +165 -1
  41. tweek/memory/provenance.py +438 -0
  42. tweek/memory/queries.py +2 -0
  43. tweek/memory/safety.py +23 -4
  44. tweek/memory/schemas.py +1 -0
  45. tweek/memory/store.py +101 -71
  46. tweek/plugins/screening/heuristic_scorer.py +1 -1
  47. tweek/security/integrity.py +77 -0
  48. tweek/security/llm_reviewer.py +162 -68
  49. tweek/security/local_reviewer.py +44 -2
  50. tweek/security/model_registry.py +73 -7
  51. tweek/skill_template/overrides-reference.md +1 -1
  52. tweek/skills/context.py +221 -0
  53. tweek/skills/scanner.py +2 -2
  54. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/METADATA +8 -7
  55. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/RECORD +60 -38
  56. tweek/mcp/server.py +0 -320
  57. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
  58. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
  59. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
  60. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
  61. {tweek-0.3.1.dist-info → tweek-0.4.0.dist-info}/top_level.txt +0 -0
tweek/cli_proxy.py ADDED
@@ -0,0 +1,405 @@
1
+ """CLI commands for the Tweek LLM security proxy.
2
+
3
+ Provides the ``proxy`` Click group and its subcommands:
4
+ start, stop, trust, config, wrap, setup.
5
+
6
+ These were extracted from the monolithic cli.py to improve
7
+ maintainability. The group is registered on the main CLI
8
+ entry-point via ``main.add_command(proxy)``.
9
+ """
10
+
11
+ import os
12
+ from pathlib import Path
13
+
14
+ import click
15
+
16
+ from tweek.cli_helpers import console, print_error, print_success, print_warning, spinner
17
+
18
+
19
+ # ------------------------------------------------------------------
20
+ # Proxy command group
21
+ # ------------------------------------------------------------------
22
+
23
+ @click.group()
24
+ def proxy():
25
+ """LLM API security proxy for universal protection.
26
+
27
+ The proxy intercepts LLM API traffic and screens for dangerous tool calls.
28
+ Works with any application that calls Anthropic, OpenAI, or other LLM APIs.
29
+
30
+ \b
31
+ Install dependencies: pip install tweek[proxy]
32
+ Quick start:
33
+ tweek proxy start # Start the proxy
34
+ tweek proxy trust # Install CA certificate
35
+ tweek proxy wrap openclaw "npm start" # Wrap an app
36
+ """
37
+ pass
38
+
39
+
40
+ # ------------------------------------------------------------------
41
+ # tweek proxy start
42
+ # ------------------------------------------------------------------
43
+
44
+ @proxy.command(
45
+ "start",
46
+ epilog="""\b
47
+ Examples:
48
+ tweek proxy start Start proxy on default port (9877)
49
+ tweek proxy start --port 8080 Start proxy on custom port
50
+ tweek proxy start --foreground Run in foreground for debugging
51
+ tweek proxy start --log-only Log traffic without blocking
52
+ """,
53
+ )
54
+ @click.option("--port", "-p", default=9877, help="Port for proxy to listen on")
55
+ @click.option("--web-port", type=int, help="Port for web interface (disabled by default)")
56
+ @click.option("--foreground", "-f", is_flag=True, help="Run in foreground (for debugging)")
57
+ @click.option("--log-only", is_flag=True, help="Log only, don't block dangerous requests")
58
+ def proxy_start(port: int, web_port: int, foreground: bool, log_only: bool):
59
+ """Start the Tweek LLM security proxy."""
60
+ from tweek.proxy import PROXY_AVAILABLE, PROXY_MISSING_DEPS
61
+
62
+ if not PROXY_AVAILABLE:
63
+ console.print("[red]\u2717[/red] Proxy dependencies not installed.")
64
+ console.print(" [white]Hint: Install with: pip install tweek[proxy][/white]")
65
+ console.print(" [white]This adds mitmproxy for HTTP(S) interception.[/white]")
66
+ return
67
+
68
+ from tweek.proxy.server import start_proxy
69
+
70
+ console.print(f"[cyan]Starting Tweek proxy on port {port}...[/cyan]")
71
+
72
+ success, message = start_proxy(
73
+ port=port,
74
+ web_port=web_port,
75
+ log_only=log_only,
76
+ foreground=foreground,
77
+ )
78
+
79
+ if success:
80
+ console.print(f"[green]\u2713[/green] {message}")
81
+ console.print()
82
+ console.print("[bold]To use the proxy:[/bold]")
83
+ console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
84
+ console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
85
+ console.print()
86
+ console.print("[white]Or use 'tweek proxy wrap' to create a wrapper script[/white]")
87
+ else:
88
+ console.print(f"[red]\u2717[/red] {message}")
89
+
90
+
91
+ # ------------------------------------------------------------------
92
+ # tweek proxy stop
93
+ # ------------------------------------------------------------------
94
+
95
+ @proxy.command(
96
+ "stop",
97
+ epilog="""\b
98
+ Examples:
99
+ tweek proxy stop Stop the running proxy server
100
+ """,
101
+ )
102
+ def proxy_stop():
103
+ """Stop the Tweek LLM security proxy."""
104
+ from tweek.proxy import PROXY_AVAILABLE
105
+
106
+ if not PROXY_AVAILABLE:
107
+ console.print("[red]\u2717[/red] Proxy dependencies not installed.")
108
+ return
109
+
110
+ from tweek.proxy.server import stop_proxy
111
+
112
+ success, message = stop_proxy()
113
+
114
+ if success:
115
+ console.print(f"[green]\u2713[/green] {message}")
116
+ else:
117
+ console.print(f"[yellow]![/yellow] {message}")
118
+
119
+
120
+ # ------------------------------------------------------------------
121
+ # tweek proxy trust
122
+ # ------------------------------------------------------------------
123
+
124
+ @proxy.command(
125
+ "trust",
126
+ epilog="""\b
127
+ Examples:
128
+ tweek proxy trust Install CA certificate for HTTPS interception
129
+ """,
130
+ )
131
+ def proxy_trust():
132
+ """Install the proxy CA certificate in system trust store.
133
+
134
+ This is required for HTTPS interception to work. The certificate
135
+ is generated locally and only used for local proxy traffic.
136
+ """
137
+ from tweek.proxy import PROXY_AVAILABLE
138
+
139
+ if not PROXY_AVAILABLE:
140
+ console.print("[red]\u2717[/red] Proxy dependencies not installed.")
141
+ console.print("[white]Run: pip install tweek\\[proxy][/white]")
142
+ return
143
+
144
+ from tweek.proxy.server import get_proxy_info, install_ca_certificate
145
+
146
+ info = get_proxy_info()
147
+
148
+ console.print("[bold]Tweek Proxy Certificate Installation[/bold]")
149
+ console.print()
150
+ console.print("This will install a local CA certificate to enable HTTPS interception.")
151
+ console.print("The certificate is generated on YOUR machine and never transmitted.")
152
+ console.print()
153
+ console.print(f"[white]Certificate location: {info['ca_cert']}[/white]")
154
+ console.print()
155
+
156
+ if not click.confirm("Install certificate? (requires admin password)"):
157
+ console.print("[white]Cancelled[/white]")
158
+ return
159
+
160
+ success, message = install_ca_certificate()
161
+
162
+ if success:
163
+ console.print(f"[green]\u2713[/green] {message}")
164
+ else:
165
+ console.print(f"[red]\u2717[/red] {message}")
166
+
167
+
168
+ # ------------------------------------------------------------------
169
+ # tweek proxy config
170
+ # ------------------------------------------------------------------
171
+
172
+ @proxy.command(
173
+ "config",
174
+ epilog="""\b
175
+ Examples:
176
+ tweek proxy config --enabled Enable proxy in configuration
177
+ tweek proxy config --disabled Disable proxy in configuration
178
+ tweek proxy config --enabled --port 8080 Enable proxy on custom port
179
+ """,
180
+ )
181
+ @click.option("--enabled", "set_enabled", is_flag=True, help="Enable proxy in configuration")
182
+ @click.option("--disabled", "set_disabled", is_flag=True, help="Disable proxy in configuration")
183
+ @click.option("--port", "-p", default=9877, help="Port for proxy")
184
+ def proxy_config(set_enabled, set_disabled, port):
185
+ """Configure proxy settings."""
186
+ if not set_enabled and not set_disabled:
187
+ console.print("[red]Specify --enabled or --disabled[/red]")
188
+ return
189
+
190
+ import yaml
191
+
192
+ config_path = Path.home() / ".tweek" / "config.yaml"
193
+ config_path.parent.mkdir(parents=True, exist_ok=True)
194
+
195
+ config = {}
196
+ if config_path.exists():
197
+ try:
198
+ with open(config_path) as f:
199
+ config = yaml.safe_load(f) or {}
200
+ except Exception:
201
+ pass
202
+
203
+ if set_enabled:
204
+ config["proxy"] = {
205
+ "enabled": True,
206
+ "port": port,
207
+ "block_mode": True,
208
+ "log_only": False,
209
+ }
210
+
211
+ with open(config_path, "w") as f:
212
+ yaml.dump(config, f, default_flow_style=False)
213
+
214
+ console.print(f"[green]\u2713[/green] Proxy mode enabled (port {port})")
215
+ console.print("[white]Run 'tweek proxy start' to start the proxy[/white]")
216
+
217
+ elif set_disabled:
218
+ if "proxy" in config:
219
+ config["proxy"]["enabled"] = False
220
+
221
+ with open(config_path, "w") as f:
222
+ yaml.dump(config, f, default_flow_style=False)
223
+
224
+ console.print("[green]\u2713[/green] Proxy mode disabled")
225
+
226
+
227
+ # ------------------------------------------------------------------
228
+ # tweek proxy wrap
229
+ # ------------------------------------------------------------------
230
+
231
+ @proxy.command(
232
+ "wrap",
233
+ epilog="""\b
234
+ Examples:
235
+ tweek proxy wrap openclaw "npm start" Wrap a Node.js app
236
+ tweek proxy wrap cursor "/Applications/Cursor.app/Contents/MacOS/Cursor"
237
+ tweek proxy wrap myapp "python serve.py" -o run.sh Custom output path
238
+ tweek proxy wrap myapp "npm start" --port 8080 Use custom proxy port
239
+ """,
240
+ )
241
+ @click.argument("app_name")
242
+ @click.argument("command")
243
+ @click.option("--output", "-o", help="Output script path (default: ./run-{app_name}-protected.sh)")
244
+ @click.option("--port", "-p", default=9877, help="Proxy port")
245
+ def proxy_wrap(app_name: str, command: str, output: str, port: int):
246
+ """Generate a wrapper script to run an app through the proxy."""
247
+ from tweek.proxy.server import generate_wrapper_script
248
+
249
+ if output:
250
+ output_path = Path(output)
251
+ else:
252
+ output_path = Path(f"./run-{app_name}-protected.sh")
253
+
254
+ script = generate_wrapper_script(command, port=port, output_path=output_path)
255
+
256
+ console.print(f"[green]\u2713[/green] Created wrapper script: {output_path}")
257
+ console.print()
258
+ console.print("[bold]Usage:[/bold]")
259
+ console.print(f" chmod +x {output_path}")
260
+ console.print(f" ./{output_path.name}")
261
+ console.print()
262
+ console.print("[white]The script will:[/white]")
263
+ console.print("[white] 1. Start Tweek proxy if not running[/white]")
264
+ console.print("[white] 2. Set proxy environment variables[/white]")
265
+ console.print(f"[white] 3. Run: {command}[/white]")
266
+
267
+
268
+ # ------------------------------------------------------------------
269
+ # tweek proxy setup
270
+ # ------------------------------------------------------------------
271
+
272
+ @proxy.command(
273
+ "setup",
274
+ epilog="""\b
275
+ Examples:
276
+ tweek proxy setup Launch interactive proxy setup wizard
277
+ """,
278
+ )
279
+ def proxy_setup():
280
+ """Interactive setup wizard for the HTTP proxy.
281
+
282
+ Walks through:
283
+ 1. Detecting LLM tools to protect
284
+ 2. Generating and trusting CA certificate
285
+ 3. Configuring shell environment variables
286
+ """
287
+ console.print()
288
+ console.print("[bold]HTTP Proxy Setup[/bold]")
289
+ console.print("\u2500" * 30)
290
+ console.print()
291
+
292
+ # Check dependencies
293
+ try:
294
+ from tweek.proxy import PROXY_AVAILABLE, PROXY_MISSING_DEPS
295
+ except ImportError:
296
+ print_error(
297
+ "Proxy module not available",
298
+ fix_hint="Install with: pip install tweek[proxy]",
299
+ )
300
+ return
301
+
302
+ if not PROXY_AVAILABLE:
303
+ print_error(
304
+ "Proxy dependencies not installed",
305
+ fix_hint="Install with: pip install tweek[proxy]",
306
+ )
307
+ return
308
+
309
+ # Step 1: Detect tools
310
+ console.print("[bold cyan]Step 1/3: Detect LLM Tools[/bold cyan]")
311
+ try:
312
+ from tweek.proxy import detect_supported_tools
313
+
314
+ with spinner("Scanning for LLM tools"):
315
+ tools = detect_supported_tools()
316
+
317
+ detected = [(name, info) for name, info in tools.items() if info]
318
+ if detected:
319
+ for name, info in detected:
320
+ print_success(f"Found {name.capitalize()}")
321
+ else:
322
+ print_warning("No LLM tools detected. You can still set up the proxy manually.")
323
+ except Exception as e:
324
+ print_warning(f"Could not detect tools: {e}")
325
+ console.print()
326
+
327
+ # Step 2: CA Certificate
328
+ console.print("[bold cyan]Step 2/3: CA Certificate[/bold cyan]")
329
+ setup_cert = click.confirm("Generate and trust Tweek CA certificate?", default=True)
330
+ if setup_cert:
331
+ try:
332
+ from tweek.proxy.cert import generate_ca, trust_ca
333
+
334
+ with spinner("Generating CA certificate"):
335
+ generate_ca()
336
+ print_success("CA certificate generated")
337
+
338
+ with spinner("Installing to system trust store"):
339
+ trust_ca()
340
+ print_success("Certificate trusted")
341
+ except ImportError:
342
+ print_warning("Certificate module not available. Run: tweek proxy trust")
343
+ except Exception as e:
344
+ print_warning(f"Could not set up certificate: {e}")
345
+ console.print(" [white]You can do this later with: tweek proxy trust[/white]")
346
+ else:
347
+ console.print(" [white]Skipped. Run 'tweek proxy trust' later.[/white]")
348
+ console.print()
349
+
350
+ # Step 3: Shell environment
351
+ console.print("[bold cyan]Step 3/3: Environment Variables[/bold cyan]")
352
+ port = click.prompt("Proxy port", default=9877, type=int)
353
+
354
+ shell_rc = _detect_shell_rc()
355
+ if shell_rc:
356
+ console.print(f" Detected shell config: {shell_rc}")
357
+ console.print(f" Will add:")
358
+ console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
359
+ console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
360
+ console.print()
361
+
362
+ apply_env = click.confirm(f"Add to {shell_rc}?", default=True)
363
+ if apply_env:
364
+ try:
365
+ rc_path = Path(shell_rc).expanduser()
366
+ with open(rc_path, "a") as f:
367
+ f.write(f"\n# Tweek proxy environment\n")
368
+ f.write(f"export HTTP_PROXY=http://127.0.0.1:{port}\n")
369
+ f.write(f"export HTTPS_PROXY=http://127.0.0.1:{port}\n")
370
+ print_success(f"Added to {shell_rc}")
371
+ console.print(f" [white]Restart your shell or run: source {shell_rc}[/white]")
372
+ except Exception as e:
373
+ print_warning(f"Could not write to {shell_rc}: {e}")
374
+ else:
375
+ console.print(" [white]Skipped. Set HTTP_PROXY and HTTPS_PROXY manually.[/white]")
376
+ else:
377
+ console.print(" [white]Could not detect shell config file.[/white]")
378
+ console.print(f" Add these to your shell profile:")
379
+ console.print(f" export HTTP_PROXY=http://127.0.0.1:{port}")
380
+ console.print(f" export HTTPS_PROXY=http://127.0.0.1:{port}")
381
+
382
+ console.print()
383
+ console.print("[bold green]Proxy configured![/bold green]")
384
+ console.print(" Start with: [cyan]tweek proxy start[/cyan]")
385
+ console.print()
386
+
387
+
388
+ # ------------------------------------------------------------------
389
+ # Helper (only used by proxy_setup)
390
+ # ------------------------------------------------------------------
391
+
392
+ def _detect_shell_rc() -> str:
393
+ """Detect the user's shell config file."""
394
+ shell = os.environ.get("SHELL", "")
395
+ home = Path.home()
396
+
397
+ if "zsh" in shell:
398
+ return "~/.zshrc"
399
+ elif "bash" in shell:
400
+ if (home / ".bash_profile").exists():
401
+ return "~/.bash_profile"
402
+ return "~/.bashrc"
403
+ elif "fish" in shell:
404
+ return "~/.config/fish/config.fish"
405
+ return ""
tweek/cli_security.py ADDED
@@ -0,0 +1,236 @@
1
+ """CLI commands for break-glass overrides and false-positive feedback.
2
+
3
+ Extracted from cli.py to keep the main CLI module manageable.
4
+ Groups:
5
+ override_group -- break-glass override create / list / clear
6
+ feedback_group -- false-positive reporting, stats, reset
7
+ """
8
+
9
+ import sys
10
+ from typing import Optional
11
+
12
+ import click
13
+ from rich.table import Table
14
+
15
+ from tweek.cli_helpers import console
16
+
17
+ # =========================================================================
18
+ # BREAK-GLASS OVERRIDE COMMANDS
19
+ # =========================================================================
20
+
21
+
22
+ @click.group("override")
23
+ def override_group():
24
+ """Break-glass override for hard-blocked patterns.
25
+
26
+ When graduated enforcement blocks a pattern with "deny" (critical +
27
+ deterministic), use these commands to create a temporary override.
28
+
29
+ Overrides downgrade "deny" to "ask" — you still see the prompt and
30
+ must explicitly approve. Every use is logged for audit.
31
+ """
32
+ pass
33
+
34
+
35
+ @override_group.command("create")
36
+ @click.option("--pattern", required=True, help="Pattern name to override (e.g., ssh_key_read)")
37
+ @click.option("--once", "mode", flag_value="once", default=True, help="Single-use override (consumed on first use)")
38
+ @click.option("--duration", "duration_minutes", type=int, default=None, help="Duration in minutes (overrides --once)")
39
+ @click.option("--reason", default="", help="Reason for the override (logged for audit)")
40
+ def override_create(pattern: str, mode: str, duration_minutes: Optional[int], reason: str):
41
+ """Create a break-glass override for a hard-blocked pattern."""
42
+ from tweek.hooks.break_glass import create_override
43
+
44
+ if duration_minutes:
45
+ mode = "duration"
46
+
47
+ override = create_override(pattern, mode=mode, duration_minutes=duration_minutes, reason=reason)
48
+
49
+ # Log the creation
50
+ try:
51
+ from tweek.logging.security_log import get_logger, EventType, SecurityEvent
52
+ logger = get_logger()
53
+ logger.log(SecurityEvent(
54
+ event_type=EventType.BREAK_GLASS,
55
+ tool_name="tweek_cli",
56
+ decision="override_created",
57
+ decision_reason=f"Break-glass override created for '{pattern}'",
58
+ metadata={
59
+ "pattern": pattern,
60
+ "mode": mode,
61
+ "duration_minutes": duration_minutes,
62
+ "reason": reason,
63
+ },
64
+ ))
65
+ except Exception:
66
+ pass
67
+
68
+ console.print(f"[bold green]Break-glass override created[/bold green]")
69
+ console.print(f" Pattern: [bold]{pattern}[/bold]")
70
+ console.print(f" Mode: {mode}")
71
+ if duration_minutes:
72
+ console.print(f" Expires: {override.get('expires_at', 'N/A')}")
73
+ if reason:
74
+ console.print(f" Reason: {reason}")
75
+ console.print()
76
+ console.print("[white]Next time this pattern triggers, you'll see an 'ask' prompt instead of a hard block.[/white]")
77
+
78
+
79
+ @override_group.command("list")
80
+ def override_list():
81
+ """List all break-glass overrides (active and historical)."""
82
+ from tweek.hooks.break_glass import list_overrides, list_active_overrides
83
+
84
+ all_overrides = list_overrides()
85
+ active = list_active_overrides()
86
+ active_patterns = {o["pattern"] for o in active}
87
+
88
+ if not all_overrides:
89
+ console.print("[white]No break-glass overrides found.[/white]")
90
+ return
91
+
92
+ table = Table(title="Break-Glass Overrides")
93
+ table.add_column("Pattern", style="bold")
94
+ table.add_column("Mode")
95
+ table.add_column("Status")
96
+ table.add_column("Reason")
97
+ table.add_column("Created")
98
+
99
+ for o in all_overrides:
100
+ if o["pattern"] in active_patterns and not o.get("used"):
101
+ status = "[green]active[/green]"
102
+ elif o.get("used"):
103
+ status = "[white]consumed[/white]"
104
+ else:
105
+ status = "[white]expired[/white]"
106
+
107
+ table.add_row(
108
+ o["pattern"],
109
+ o["mode"],
110
+ status,
111
+ o.get("reason", ""),
112
+ o.get("created_at", "")[:19],
113
+ )
114
+
115
+ console.print(table)
116
+ console.print(f"\n[bold]{len(active)}[/bold] active override(s)")
117
+
118
+
119
+ @override_group.command("clear")
120
+ @click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
121
+ def override_clear(confirm: bool):
122
+ """Remove all break-glass overrides."""
123
+ from tweek.hooks.break_glass import clear_overrides
124
+
125
+ if not confirm:
126
+ if not sys.stdin.isatty():
127
+ console.print("[red]Use --confirm to clear overrides in non-interactive mode.[/red]")
128
+ return
129
+ if not click.confirm("Clear all break-glass overrides?"):
130
+ return
131
+
132
+ count = clear_overrides()
133
+ console.print(f"[bold]Cleared {count} override(s).[/bold]")
134
+
135
+
136
+ # =========================================================================
137
+ # FEEDBACK COMMANDS
138
+ # =========================================================================
139
+
140
+
141
+ @click.group("feedback")
142
+ def feedback_group():
143
+ """False-positive feedback and pattern performance tracking.
144
+
145
+ Report false positives, view per-pattern FP rates, and manage
146
+ automatic severity demotions for noisy patterns.
147
+ """
148
+ pass
149
+
150
+
151
+ @feedback_group.command("fp")
152
+ @click.argument("pattern_name")
153
+ @click.option("--context", default="", help="Description of the false positive context")
154
+ def feedback_fp(pattern_name: str, context: str):
155
+ """Report a false positive for a pattern."""
156
+ from tweek.hooks.feedback import report_false_positive
157
+
158
+ result = report_false_positive(pattern_name, context=context)
159
+
160
+ # Log the report
161
+ try:
162
+ from tweek.logging.security_log import get_logger, EventType, SecurityEvent
163
+ logger = get_logger()
164
+ logger.log(SecurityEvent(
165
+ event_type=EventType.FALSE_POSITIVE_REPORT,
166
+ tool_name="tweek_cli",
167
+ pattern_name=pattern_name,
168
+ decision="fp_reported",
169
+ decision_reason=f"False positive reported for '{pattern_name}'",
170
+ metadata={
171
+ "context": context,
172
+ "fp_rate": result.get("fp_rate"),
173
+ "total_triggers": result.get("total_triggers"),
174
+ "false_positives": result.get("false_positives"),
175
+ },
176
+ ))
177
+ except Exception:
178
+ pass
179
+
180
+ console.print(f"[bold green]False positive recorded[/bold green] for [bold]{pattern_name}[/bold]")
181
+ console.print(f" FP rate: {result.get('fp_rate', 0):.1%} ({result.get('false_positives', 0)}/{result.get('total_triggers', 0)})")
182
+
183
+ if result.get("auto_demoted"):
184
+ console.print(f" [yellow]Auto-demoted:[/yellow] {result.get('original_severity')} -> {result.get('current_severity')}")
185
+
186
+
187
+ @feedback_group.command("stats")
188
+ @click.option("--above-threshold", is_flag=True, help="Show only patterns exceeding 5% FP rate")
189
+ def feedback_stats(above_threshold: bool):
190
+ """Show false-positive rates per pattern."""
191
+ from tweek.hooks.feedback import get_stats
192
+
193
+ stats = get_stats()
194
+ if not stats:
195
+ console.print("[white]No feedback data recorded yet.[/white]")
196
+ return
197
+
198
+ table = Table(title="Pattern FP Statistics")
199
+ table.add_column("Pattern", style="bold")
200
+ table.add_column("Triggers", justify="right")
201
+ table.add_column("FPs", justify="right")
202
+ table.add_column("FP Rate", justify="right")
203
+ table.add_column("Demoted?")
204
+
205
+ for name, data in sorted(stats.items(), key=lambda x: x[1].get("fp_rate", 0), reverse=True):
206
+ fp_rate = data.get("fp_rate", 0)
207
+ if above_threshold and fp_rate < 0.05:
208
+ continue
209
+
210
+ rate_style = "red" if fp_rate >= 0.05 else "green"
211
+ demoted = "[yellow]yes[/yellow]" if data.get("auto_demoted") else "no"
212
+
213
+ table.add_row(
214
+ name,
215
+ str(data.get("total_triggers", 0)),
216
+ str(data.get("false_positives", 0)),
217
+ f"[{rate_style}]{fp_rate:.1%}[/{rate_style}]",
218
+ demoted,
219
+ )
220
+
221
+ console.print(table)
222
+
223
+
224
+ @feedback_group.command("reset")
225
+ @click.argument("pattern_name")
226
+ def feedback_reset(pattern_name: str):
227
+ """Reset FP tracking and undo auto-demotion for a pattern."""
228
+ from tweek.hooks.feedback import reset_pattern
229
+
230
+ result = reset_pattern(pattern_name)
231
+ if result:
232
+ console.print(f"[bold]Reset feedback data for '{pattern_name}'[/bold]")
233
+ if result.get("was_demoted"):
234
+ console.print(f" Restored severity: {result.get('original_severity')}")
235
+ else:
236
+ console.print(f"[white]No feedback data found for '{pattern_name}'.[/white]")