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.
- tweek/__init__.py +2 -2
- tweek/audit.py +2 -2
- tweek/cli.py +78 -6559
- tweek/cli_config.py +643 -0
- tweek/cli_configure.py +413 -0
- tweek/cli_core.py +718 -0
- tweek/cli_dry_run.py +390 -0
- tweek/cli_helpers.py +316 -0
- tweek/cli_install.py +1666 -0
- tweek/cli_logs.py +301 -0
- tweek/cli_mcp.py +148 -0
- tweek/cli_memory.py +343 -0
- tweek/cli_plugins.py +748 -0
- tweek/cli_protect.py +564 -0
- tweek/cli_proxy.py +405 -0
- tweek/cli_security.py +236 -0
- tweek/cli_skills.py +289 -0
- tweek/cli_uninstall.py +551 -0
- tweek/cli_vault.py +313 -0
- tweek/config/__init__.py +8 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +49 -0
- tweek/config/models.py +307 -0
- tweek/config/patterns.yaml +29 -5
- tweek/config/templates/config.yaml.template +212 -0
- tweek/config/templates/env.template +45 -0
- tweek/config/templates/overrides.yaml.template +121 -0
- tweek/config/templates/tweek.yaml.template +20 -0
- tweek/config/templates.py +136 -0
- tweek/config/tiers.yaml +5 -4
- tweek/diagnostics.py +112 -32
- tweek/hooks/overrides.py +4 -0
- tweek/hooks/post_tool_use.py +46 -1
- tweek/hooks/pre_tool_use.py +149 -49
- tweek/integrations/openclaw.py +84 -0
- tweek/licensing.py +1 -1
- tweek/mcp/__init__.py +7 -9
- tweek/mcp/clients/chatgpt.py +2 -2
- tweek/mcp/clients/claude_desktop.py +2 -2
- tweek/mcp/clients/gemini.py +2 -2
- tweek/mcp/proxy.py +165 -1
- tweek/memory/provenance.py +438 -0
- tweek/memory/queries.py +2 -0
- tweek/memory/safety.py +23 -4
- tweek/memory/schemas.py +1 -0
- tweek/memory/store.py +101 -71
- tweek/plugins/screening/heuristic_scorer.py +1 -1
- tweek/security/integrity.py +77 -0
- tweek/security/llm_reviewer.py +162 -68
- tweek/security/local_reviewer.py +44 -2
- tweek/security/model_registry.py +73 -7
- tweek/skill_template/overrides-reference.md +1 -1
- tweek/skills/context.py +221 -0
- tweek/skills/scanner.py +2 -2
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/METADATA +9 -7
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/RECORD +62 -39
- tweek/mcp/server.py +0 -320
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/WHEEL +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.4.0.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.0.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)
|