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_skills.py ADDED
@@ -0,0 +1,289 @@
1
+ """Skills isolation chamber CLI commands extracted from tweek.cli.
2
+
3
+ Provides the ``skills`` Click group with subgroups: chamber, jail,
4
+ and top-level subcommands: report, status, config.
5
+ """
6
+
7
+ import json
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+
15
+ from tweek.cli_helpers import console
16
+
17
+ # ============================================================
18
+ # SKILLS - Isolation Chamber Management
19
+ # ============================================================
20
+
21
+
22
+ @click.group()
23
+ def skills():
24
+ """Manage skill isolation chamber, jail, and security scanning."""
25
+ pass
26
+
27
+
28
+ @skills.group("chamber")
29
+ def skills_chamber():
30
+ """Manage the skill isolation chamber."""
31
+ pass
32
+
33
+
34
+ @skills_chamber.command("list")
35
+ def chamber_list():
36
+ """List skills currently in the isolation chamber."""
37
+ from tweek.skills.isolation import SkillIsolationChamber
38
+
39
+ chamber = SkillIsolationChamber()
40
+ items = chamber.list_chamber()
41
+
42
+ if not items:
43
+ console.print("[white]Chamber is empty.[/white]")
44
+ return
45
+
46
+ table = Table(title="Isolation Chamber")
47
+ table.add_column("Name", style="cyan")
48
+ table.add_column("Has SKILL.md", style="green")
49
+ table.add_column("Path", style="white")
50
+
51
+ for item in items:
52
+ has_md = "Yes" if item["has_skill_md"] else "[red]No[/red]"
53
+ table.add_row(item["name"], has_md, item["path"])
54
+
55
+ console.print(table)
56
+
57
+
58
+ @skills_chamber.command("import")
59
+ @click.argument("source")
60
+ @click.option("--name", default=None, help="Override skill name")
61
+ @click.option("--target", type=click.Choice(["global", "project"]), default="global",
62
+ help="Install target if scan passes")
63
+ def chamber_import(source: str, name: Optional[str], target: str):
64
+ """Import a skill into the isolation chamber and scan it.
65
+
66
+ SOURCE is a path to a skill directory or SKILL.md file.
67
+ """
68
+ from tweek.skills.isolation import SkillIsolationChamber
69
+
70
+ source_path = Path(source).resolve()
71
+ if not source_path.exists():
72
+ console.print(f"[red]Source not found: {source_path}[/red]")
73
+ return
74
+
75
+ chamber = SkillIsolationChamber()
76
+ report, msg = chamber.accept_and_scan(source_path, skill_name=name, target=target)
77
+
78
+ if report.verdict == "pass":
79
+ console.print(f"[green]PASS[/green] {msg}")
80
+ elif report.verdict == "fail":
81
+ console.print(f"[red]FAIL[/red] {msg}")
82
+ elif report.verdict == "manual_review":
83
+ console.print(f"[yellow]MANUAL REVIEW[/yellow] {msg}")
84
+ else:
85
+ console.print(msg)
86
+
87
+
88
+ @skills_chamber.command("scan")
89
+ @click.argument("name")
90
+ def chamber_scan(name: str):
91
+ """Manually trigger a scan on a skill in the chamber."""
92
+ from tweek.skills.isolation import SkillIsolationChamber
93
+
94
+ chamber = SkillIsolationChamber()
95
+ report, msg = chamber.scan_skill(name)
96
+ console.print(msg)
97
+
98
+
99
+ @skills_chamber.command("approve")
100
+ @click.argument("name")
101
+ @click.option("--target", type=click.Choice(["global", "project"]), default="global",
102
+ help="Install target directory")
103
+ def chamber_approve(name: str, target: str):
104
+ """Approve a skill in the chamber and install it."""
105
+ from tweek.skills.isolation import SkillIsolationChamber
106
+
107
+ chamber = SkillIsolationChamber()
108
+ ok, msg = chamber.approve_skill(name, target=target)
109
+ if ok:
110
+ console.print(f"[green]{msg}[/green]")
111
+ else:
112
+ console.print(f"[red]{msg}[/red]")
113
+
114
+
115
+ @skills_chamber.command("reject")
116
+ @click.argument("name")
117
+ def chamber_reject(name: str):
118
+ """Reject a skill in the chamber and move it to jail."""
119
+ from tweek.skills.isolation import SkillIsolationChamber
120
+
121
+ chamber = SkillIsolationChamber()
122
+ ok, msg = chamber.jail_skill(name)
123
+ if ok:
124
+ console.print(f"[yellow]{msg}[/yellow]")
125
+ else:
126
+ console.print(f"[red]{msg}[/red]")
127
+
128
+
129
+ @skills.group("jail")
130
+ def skills_jail():
131
+ """Manage quarantined (jailed) skills."""
132
+ pass
133
+
134
+
135
+ @skills_jail.command("list")
136
+ def jail_list():
137
+ """List skills currently in the jail."""
138
+ from tweek.skills.isolation import SkillIsolationChamber
139
+
140
+ chamber = SkillIsolationChamber()
141
+ items = chamber.list_jail()
142
+
143
+ if not items:
144
+ console.print("[white]Jail is empty.[/white]")
145
+ return
146
+
147
+ table = Table(title="Skill Jail")
148
+ table.add_column("Name", style="cyan")
149
+ table.add_column("Verdict", style="red")
150
+ table.add_column("Risk", style="yellow")
151
+ table.add_column("Critical", style="red")
152
+ table.add_column("High", style="yellow")
153
+
154
+ for item in items:
155
+ table.add_row(
156
+ item["name"],
157
+ item.get("verdict", "?"),
158
+ item.get("risk_level", "?"),
159
+ str(item.get("critical", "?")),
160
+ str(item.get("high", "?")),
161
+ )
162
+
163
+ console.print(table)
164
+
165
+
166
+ @skills_jail.command("rescan")
167
+ @click.argument("name")
168
+ def jail_rescan(name: str):
169
+ """Re-scan a jailed skill (useful after pattern updates)."""
170
+ from tweek.skills.isolation import SkillIsolationChamber
171
+
172
+ chamber = SkillIsolationChamber()
173
+ ok, msg = chamber.release_from_jail(name, force=False)
174
+ if ok:
175
+ console.print(f"[green]{msg}[/green]")
176
+ else:
177
+ console.print(f"[red]{msg}[/red]")
178
+
179
+
180
+ @skills_jail.command("release")
181
+ @click.argument("name")
182
+ @click.confirmation_option(prompt="Force-release bypasses security scanning. Are you sure?")
183
+ def jail_release(name: str):
184
+ """Force-release a skill from jail (dangerous -- bypasses scanning)."""
185
+ from tweek.skills.isolation import SkillIsolationChamber
186
+
187
+ chamber = SkillIsolationChamber()
188
+ ok, msg = chamber.release_from_jail(name, force=True)
189
+ if ok:
190
+ console.print(f"[yellow]{msg}[/yellow]")
191
+ else:
192
+ console.print(f"[red]{msg}[/red]")
193
+
194
+
195
+ @skills_jail.command("purge")
196
+ @click.confirmation_option(prompt="Delete all jailed skills permanently?")
197
+ def jail_purge():
198
+ """Delete all jailed skills permanently."""
199
+ from tweek.skills.isolation import SkillIsolationChamber
200
+
201
+ chamber = SkillIsolationChamber()
202
+ count, msg = chamber.purge_jail()
203
+ console.print(msg)
204
+
205
+
206
+ @skills.command("report")
207
+ @click.argument("name")
208
+ def skills_report(name: str):
209
+ """View the latest scan report for a skill."""
210
+ from tweek.skills.isolation import SkillIsolationChamber
211
+
212
+ chamber = SkillIsolationChamber()
213
+ report_data = chamber.get_report(name)
214
+
215
+ if not report_data:
216
+ console.print(f"[white]No report found for '{name}'.[/white]")
217
+ return
218
+
219
+ console.print(Panel(
220
+ json.dumps(report_data, indent=2),
221
+ title=f"Scan Report: {name}",
222
+ border_style="cyan",
223
+ ))
224
+
225
+
226
+ @skills.command("status")
227
+ def skills_status():
228
+ """Show overview of chamber, jail, and installed skills."""
229
+ from tweek.skills.isolation import SkillIsolationChamber
230
+ from tweek.skills import CLAUDE_GLOBAL_SKILLS
231
+
232
+ chamber = SkillIsolationChamber()
233
+ chamber_items = chamber.list_chamber()
234
+ jail_items = chamber.list_jail()
235
+
236
+ # Count installed skills
237
+ installed_count = 0
238
+ if CLAUDE_GLOBAL_SKILLS.exists():
239
+ installed_count = sum(
240
+ 1 for d in CLAUDE_GLOBAL_SKILLS.iterdir()
241
+ if d.is_dir() and (d / "SKILL.md").exists()
242
+ )
243
+
244
+ table = Table(title="Skill Isolation Status")
245
+ table.add_column("Location", style="cyan")
246
+ table.add_column("Count", style="green")
247
+
248
+ table.add_row("Installed (global)", str(installed_count))
249
+ table.add_row("In Chamber", str(len(chamber_items)))
250
+ table.add_row("In Jail", str(len(jail_items)))
251
+
252
+ console.print(table)
253
+
254
+ if chamber_items:
255
+ console.print(f"\n[yellow]Chamber:[/yellow] {', '.join(i['name'] for i in chamber_items)}")
256
+ if jail_items:
257
+ console.print(f"[red]Jail:[/red] {', '.join(i['name'] for i in jail_items)}")
258
+
259
+
260
+ @skills.command("config")
261
+ @click.option("--mode", type=click.Choice(["auto", "manual"]), default=None,
262
+ help="Set isolation mode")
263
+ def skills_config(mode: Optional[str]):
264
+ """Show or update isolation chamber configuration."""
265
+ from tweek.skills.config import load_isolation_config, save_isolation_config
266
+
267
+ config = load_isolation_config()
268
+
269
+ if mode:
270
+ config.mode = mode
271
+ save_isolation_config(config)
272
+ console.print(f"[green]Isolation mode set to: {mode}[/green]")
273
+ return
274
+
275
+ table = Table(title="Isolation Chamber Config")
276
+ table.add_column("Setting", style="cyan")
277
+ table.add_column("Value", style="green")
278
+
279
+ table.add_row("Enabled", str(config.enabled))
280
+ table.add_row("Mode", config.mode)
281
+ table.add_row("Scan Timeout", f"{config.scan_timeout_seconds}s")
282
+ table.add_row("LLM Review", str(config.llm_review_enabled))
283
+ table.add_row("Max Skill Size", f"{config.max_skill_size_bytes:,} bytes")
284
+ table.add_row("Max File Count", str(config.max_file_count))
285
+ table.add_row("Fail on Critical", str(config.fail_on_critical))
286
+ table.add_row("Fail on HIGH Count", str(config.fail_on_high_count))
287
+ table.add_row("Review on HIGH Count", str(config.review_on_high_count))
288
+
289
+ console.print(table)