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_vault.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek CLI — Vault and License command groups.
|
|
4
|
+
|
|
5
|
+
Extracted from cli.py to keep the main CLI module manageable.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
from tweek.cli_helpers import console, TWEEK_BANNER
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ============================================================
|
|
20
|
+
# VAULT COMMANDS
|
|
21
|
+
# ============================================================
|
|
22
|
+
|
|
23
|
+
@click.group()
|
|
24
|
+
def vault():
|
|
25
|
+
"""Manage credentials in secure storage (Keychain on macOS, Secret Service on Linux)."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@vault.command("store",
|
|
30
|
+
epilog="""\b
|
|
31
|
+
Examples:
|
|
32
|
+
tweek vault store myskill API_KEY Prompt for value securely
|
|
33
|
+
tweek vault store myskill API_KEY sk-abc123 Store an API key (visible in history!)
|
|
34
|
+
"""
|
|
35
|
+
)
|
|
36
|
+
@click.argument("skill")
|
|
37
|
+
@click.argument("key")
|
|
38
|
+
@click.argument("value", required=False, default=None)
|
|
39
|
+
def vault_store(skill: str, key: str, value: Optional[str]):
|
|
40
|
+
"""Store a credential securely for a skill."""
|
|
41
|
+
from tweek.vault import get_vault, VAULT_AVAILABLE
|
|
42
|
+
from tweek.platform import get_capabilities
|
|
43
|
+
|
|
44
|
+
if not VAULT_AVAILABLE:
|
|
45
|
+
console.print("[red]\u2717[/red] Vault not available.")
|
|
46
|
+
console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
|
|
47
|
+
console.print(" [white]On macOS, keyring uses Keychain. On Linux, install gnome-keyring or kwallet.[/white]")
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
caps = get_capabilities()
|
|
51
|
+
|
|
52
|
+
# If value not provided as argument, prompt securely (avoids shell history exposure)
|
|
53
|
+
if value is None:
|
|
54
|
+
value = click.prompt(f"Enter value for {key}", hide_input=True)
|
|
55
|
+
if not value:
|
|
56
|
+
console.print("[red]No value provided.[/red]")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
vault_instance = get_vault()
|
|
61
|
+
if vault_instance.store(skill, key, value):
|
|
62
|
+
console.print(f"[green]\u2713[/green] Stored {key} for skill '{skill}'")
|
|
63
|
+
console.print(f"[white]Backend: {caps.vault_backend}[/white]")
|
|
64
|
+
else:
|
|
65
|
+
console.print(f"[red]\u2717[/red] Failed to store credential")
|
|
66
|
+
console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
|
|
67
|
+
except Exception as e:
|
|
68
|
+
console.print(f"[red]\u2717[/red] Failed to store credential: {e}")
|
|
69
|
+
console.print(" [white]Hint: Check your keyring backend is unlocked and accessible[/white]")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@vault.command("get",
|
|
73
|
+
epilog="""\b
|
|
74
|
+
Examples:
|
|
75
|
+
tweek vault get myskill API_KEY Retrieve a stored credential
|
|
76
|
+
tweek vault get deploy AWS_SECRET Retrieve a deployment secret
|
|
77
|
+
"""
|
|
78
|
+
)
|
|
79
|
+
@click.argument("skill")
|
|
80
|
+
@click.argument("key")
|
|
81
|
+
def vault_get(skill: str, key: str):
|
|
82
|
+
"""Retrieve a credential from secure storage."""
|
|
83
|
+
from tweek.vault import get_vault, VAULT_AVAILABLE
|
|
84
|
+
|
|
85
|
+
if not VAULT_AVAILABLE:
|
|
86
|
+
console.print("[red]\u2717[/red] Vault not available.")
|
|
87
|
+
console.print(" [white]Hint: Install keyring support: pip install keyring[/white]")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
vault_instance = get_vault()
|
|
91
|
+
value = vault_instance.get(skill, key)
|
|
92
|
+
|
|
93
|
+
if value is not None:
|
|
94
|
+
console.print(f"[yellow]GAH![/yellow] Credential access logged")
|
|
95
|
+
import sys as _sys
|
|
96
|
+
if not _sys.stdout.isatty():
|
|
97
|
+
console.print("[yellow]WARNING: stdout is piped — credential may be logged.[/yellow]", err=True)
|
|
98
|
+
console.print(value)
|
|
99
|
+
else:
|
|
100
|
+
console.print(f"[red]\u2717[/red] Credential not found: {key} for skill '{skill}'")
|
|
101
|
+
console.print(" [white]Hint: Store it with: tweek vault store {skill} {key} <value>[/white]".format(skill=skill, key=key))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@vault.command("migrate-env",
|
|
105
|
+
epilog="""\b
|
|
106
|
+
Examples:
|
|
107
|
+
tweek vault migrate-env --skill myapp Migrate .env to vault
|
|
108
|
+
tweek vault migrate-env --skill myapp --dry-run Preview without changes
|
|
109
|
+
tweek vault migrate-env --skill deploy --env-file .env.production Migrate specific file
|
|
110
|
+
"""
|
|
111
|
+
)
|
|
112
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be migrated without doing it")
|
|
113
|
+
@click.option("--env-file", default=".env", help="Path to .env file")
|
|
114
|
+
@click.option("--skill", required=True, help="Skill name to store credentials under")
|
|
115
|
+
def vault_migrate_env(dry_run: bool, env_file: str, skill: str):
|
|
116
|
+
"""Migrate credentials from .env file to secure storage."""
|
|
117
|
+
from tweek.vault import get_vault, migrate_env_to_vault, VAULT_AVAILABLE
|
|
118
|
+
|
|
119
|
+
if not VAULT_AVAILABLE:
|
|
120
|
+
console.print("[red]\u2717[/red] Vault not available. Install keyring: pip install keyring")
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
env_path = Path(env_file)
|
|
124
|
+
console.print(f"[cyan]Scanning {env_path} for credentials...[/cyan]")
|
|
125
|
+
|
|
126
|
+
if dry_run:
|
|
127
|
+
console.print("\n[yellow]DRY RUN - No changes will be made[/yellow]\n")
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
vault_instance = get_vault()
|
|
131
|
+
results = migrate_env_to_vault(env_path, skill, vault_instance, dry_run=dry_run)
|
|
132
|
+
|
|
133
|
+
if results:
|
|
134
|
+
console.print(f"\n[green]{'Would migrate' if dry_run else 'Migrated'}:[/green]")
|
|
135
|
+
for key, success in results:
|
|
136
|
+
status = "\u2713" if success else "\u2717"
|
|
137
|
+
console.print(f" {status} {key}")
|
|
138
|
+
successful = sum(1 for _, s in results if s)
|
|
139
|
+
total = len(results)
|
|
140
|
+
console.print(f"\n[green]\u2713[/green] {'Would migrate' if dry_run else 'Migrated'} {successful} credentials to skill '{skill}'")
|
|
141
|
+
|
|
142
|
+
if not dry_run and successful == total and env_path.exists():
|
|
143
|
+
console.print()
|
|
144
|
+
if click.confirm(f"Remove {env_path}? (credentials are now in the vault)"):
|
|
145
|
+
env_path.unlink()
|
|
146
|
+
console.print(f"[green]\u2713[/green] Removed {env_path}")
|
|
147
|
+
else:
|
|
148
|
+
console.print(f"[yellow]\u26a0[/yellow] {env_path} still contains plaintext credentials")
|
|
149
|
+
elif not dry_run and successful < total:
|
|
150
|
+
failed = total - successful
|
|
151
|
+
console.print(f"[yellow]\u26a0[/yellow] {failed} credential(s) failed to migrate \u2014 keeping {env_path}")
|
|
152
|
+
else:
|
|
153
|
+
console.print("[white]No credentials found to migrate[/white]")
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
console.print(f"[red]\u2717[/red] Migration failed: {e}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@vault.command("delete",
|
|
160
|
+
epilog="""\b
|
|
161
|
+
Examples:
|
|
162
|
+
tweek vault delete myskill API_KEY Delete a stored credential
|
|
163
|
+
tweek vault delete deploy AWS_SECRET Remove a deployment secret
|
|
164
|
+
"""
|
|
165
|
+
)
|
|
166
|
+
@click.argument("skill")
|
|
167
|
+
@click.argument("key")
|
|
168
|
+
def vault_delete(skill: str, key: str):
|
|
169
|
+
"""Delete a credential from secure storage."""
|
|
170
|
+
from tweek.vault import get_vault, VAULT_AVAILABLE
|
|
171
|
+
|
|
172
|
+
if not VAULT_AVAILABLE:
|
|
173
|
+
console.print("[red]\u2717[/red] Vault not available. Install keyring: pip install keyring")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
vault_instance = get_vault()
|
|
177
|
+
deleted = vault_instance.delete(skill, key)
|
|
178
|
+
|
|
179
|
+
if deleted:
|
|
180
|
+
console.print(f"[green]\u2713[/green] Deleted {key} from skill '{skill}'")
|
|
181
|
+
else:
|
|
182
|
+
console.print(f"[yellow]![/yellow] Credential not found: {key} for skill '{skill}'")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ============================================================
|
|
186
|
+
# LICENSE COMMANDS [experimental]
|
|
187
|
+
# ============================================================
|
|
188
|
+
|
|
189
|
+
@click.group("license")
|
|
190
|
+
def license_group():
|
|
191
|
+
"""Manage Tweek license and features. [experimental]"""
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@license_group.command("status",
|
|
196
|
+
epilog="""\b
|
|
197
|
+
Examples:
|
|
198
|
+
tweek license status Show license tier and features
|
|
199
|
+
"""
|
|
200
|
+
)
|
|
201
|
+
def license_status():
|
|
202
|
+
"""Show current license status and available features. [experimental]"""
|
|
203
|
+
console.print("[yellow]Note: License management is experimental. Pro/Enterprise tiers coming soon.[/yellow]")
|
|
204
|
+
|
|
205
|
+
from tweek.licensing import get_license, TIER_FEATURES, Tier
|
|
206
|
+
|
|
207
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
208
|
+
|
|
209
|
+
lic = get_license()
|
|
210
|
+
info = lic.info
|
|
211
|
+
|
|
212
|
+
# License info
|
|
213
|
+
tier_colors = {
|
|
214
|
+
Tier.FREE: "white",
|
|
215
|
+
Tier.PRO: "cyan",
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
tier_color = tier_colors.get(lic.tier, "white")
|
|
219
|
+
console.print(f"[bold]License Tier:[/bold] [{tier_color}]{lic.tier.value.upper()}[/{tier_color}]")
|
|
220
|
+
|
|
221
|
+
if info:
|
|
222
|
+
console.print(f"[white]Licensed to: {info.email}[/white]")
|
|
223
|
+
if info.expires_at:
|
|
224
|
+
from datetime import datetime
|
|
225
|
+
exp_date = datetime.fromtimestamp(info.expires_at).strftime("%Y-%m-%d")
|
|
226
|
+
if info.is_expired:
|
|
227
|
+
console.print(f"[red]Expired: {exp_date}[/red]")
|
|
228
|
+
else:
|
|
229
|
+
console.print(f"[white]Expires: {exp_date}[/white]")
|
|
230
|
+
else:
|
|
231
|
+
console.print("[white]Expires: Never[/white]")
|
|
232
|
+
console.print()
|
|
233
|
+
|
|
234
|
+
# Features table
|
|
235
|
+
table = Table(title="Feature Availability")
|
|
236
|
+
table.add_column("Feature", style="cyan")
|
|
237
|
+
table.add_column("Status")
|
|
238
|
+
table.add_column("Tier Required")
|
|
239
|
+
|
|
240
|
+
# Collect all features and their required tiers
|
|
241
|
+
feature_tiers = {}
|
|
242
|
+
for tier in [Tier.FREE, Tier.PRO]:
|
|
243
|
+
for feature in TIER_FEATURES.get(tier, []):
|
|
244
|
+
feature_tiers[feature] = tier
|
|
245
|
+
|
|
246
|
+
for feature, required_tier in feature_tiers.items():
|
|
247
|
+
has_it = lic.has_feature(feature)
|
|
248
|
+
status = "[green]\u2713[/green]" if has_it else "[white]\u25cb[/white]"
|
|
249
|
+
tier_display = required_tier.value.upper()
|
|
250
|
+
if required_tier == Tier.PRO:
|
|
251
|
+
tier_display = f"[cyan]{tier_display}[/cyan]"
|
|
252
|
+
|
|
253
|
+
table.add_row(feature, status, tier_display)
|
|
254
|
+
|
|
255
|
+
console.print(table)
|
|
256
|
+
|
|
257
|
+
if lic.tier == Tier.FREE:
|
|
258
|
+
console.print()
|
|
259
|
+
console.print("[green]All security features are included free and open source.[/green]")
|
|
260
|
+
console.print("[white]Pro (teams) and Enterprise (compliance) coming soon: gettweek.com[/white]")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
@license_group.command("activate",
|
|
264
|
+
epilog="""\b
|
|
265
|
+
Examples:
|
|
266
|
+
tweek license activate YOUR_KEY Activate a license key (Pro/Enterprise coming soon)
|
|
267
|
+
"""
|
|
268
|
+
)
|
|
269
|
+
@click.argument("license_key")
|
|
270
|
+
def license_activate(license_key: str):
|
|
271
|
+
"""Activate a license key. [experimental]"""
|
|
272
|
+
console.print("[yellow]Note: License management is experimental. Pro/Enterprise tiers coming soon.[/yellow]")
|
|
273
|
+
|
|
274
|
+
from tweek.licensing import get_license
|
|
275
|
+
|
|
276
|
+
lic = get_license()
|
|
277
|
+
success, message = lic.activate(license_key)
|
|
278
|
+
|
|
279
|
+
if success:
|
|
280
|
+
console.print(f"[green]\u2713[/green] {message}")
|
|
281
|
+
console.print()
|
|
282
|
+
console.print("[white]Run 'tweek license status' to see available features[/white]")
|
|
283
|
+
else:
|
|
284
|
+
console.print(f"[red]\u2717[/red] {message}")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@license_group.command("deactivate",
|
|
288
|
+
epilog="""\b
|
|
289
|
+
Examples:
|
|
290
|
+
tweek license deactivate Deactivate license (with prompt)
|
|
291
|
+
tweek license deactivate --confirm Deactivate without confirmation
|
|
292
|
+
"""
|
|
293
|
+
)
|
|
294
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
295
|
+
def license_deactivate(confirm: bool):
|
|
296
|
+
"""Remove current license and revert to FREE tier. [experimental]"""
|
|
297
|
+
console.print("[yellow]Note: License management is experimental. Pro/Enterprise tiers coming soon.[/yellow]")
|
|
298
|
+
|
|
299
|
+
from tweek.licensing import get_license
|
|
300
|
+
|
|
301
|
+
if not confirm:
|
|
302
|
+
console.print("[yellow]Deactivate license and revert to FREE tier?[/yellow] ", end="")
|
|
303
|
+
if not click.confirm(""):
|
|
304
|
+
console.print("[white]Cancelled[/white]")
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
lic = get_license()
|
|
308
|
+
success, message = lic.deactivate()
|
|
309
|
+
|
|
310
|
+
if success:
|
|
311
|
+
console.print(f"[green]\u2713[/green] {message}")
|
|
312
|
+
else:
|
|
313
|
+
console.print(f"[red]\u2717[/red] {message}")
|
tweek/config/__init__.py
CHANGED
|
@@ -10,4 +10,12 @@ PATTERNS_FILE = CONFIG_DIR / "patterns.yaml"
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"ConfigManager", "SecurityTier", "ConfigIssue", "ConfigChange",
|
|
12
12
|
"get_config", "CONFIG_DIR", "PATTERNS_FILE",
|
|
13
|
+
"TweekConfig", "PatternsConfig",
|
|
13
14
|
]
|
|
15
|
+
|
|
16
|
+
# Lazy imports for Pydantic models to avoid import cost when not needed
|
|
17
|
+
def __getattr__(name):
|
|
18
|
+
if name in ("TweekConfig", "PatternsConfig"):
|
|
19
|
+
from tweek.config.models import TweekConfig, PatternsConfig
|
|
20
|
+
return {"TweekConfig": TweekConfig, "PatternsConfig": PatternsConfig}[name]
|
|
21
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
tweek/config/allowed_dirs.yaml
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
# Tweek Directory Safety Configuration
|
|
1
|
+
# Tweek Directory Safety Configuration (bundled default)
|
|
2
2
|
#
|
|
3
|
-
# This file
|
|
4
|
-
#
|
|
3
|
+
# This file ships with the Tweek package and provides the production default.
|
|
4
|
+
# To override, create ~/.tweek/allowed_dirs.yaml (user-level config takes
|
|
5
|
+
# full precedence when it exists).
|
|
5
6
|
#
|
|
6
7
|
# Options:
|
|
7
|
-
# global_enabled: true - Activate Tweek everywhere (
|
|
8
|
-
# allowed_directories: -
|
|
8
|
+
# global_enabled: true - Activate Tweek everywhere (default for end users)
|
|
9
|
+
# allowed_directories: - Restrict Tweek to specific directories only
|
|
9
10
|
#
|
|
10
|
-
#
|
|
11
|
+
# Lookup order:
|
|
12
|
+
# 1. ~/.tweek/allowed_dirs.yaml (user override — not in repo)
|
|
13
|
+
# 2. This file (bundled default)
|
|
11
14
|
|
|
12
|
-
#
|
|
13
|
-
global_enabled:
|
|
15
|
+
# Production default: Tweek is active everywhere after install
|
|
16
|
+
global_enabled: true
|
|
14
17
|
|
|
15
|
-
#
|
|
16
|
-
# Tweek
|
|
17
|
-
allowed_directories:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# Add more directories as needed:
|
|
22
|
-
# - ~/projects/sensitive-project
|
|
23
|
-
# - /path/to/another/project
|
|
18
|
+
# When global_enabled is false, only activate in these directories.
|
|
19
|
+
# Tweek also activates in subdirectories of listed paths.
|
|
20
|
+
# allowed_directories:
|
|
21
|
+
# - ~/projects/my-project
|
|
22
|
+
# - /path/to/another/project
|
tweek/config/families.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Tweek Pattern Family Definitions v1
|
|
2
|
-
# Groups the
|
|
2
|
+
# Groups the 262 attack patterns by attack class for:
|
|
3
3
|
# 1. Heuristic scoring (near-miss detection via semantic signals)
|
|
4
4
|
# 2. Pattern management (enable/disable by family)
|
|
5
5
|
# 3. Reporting (family-level risk summaries)
|
|
@@ -257,6 +257,9 @@ families:
|
|
|
257
257
|
- 257 # self_describe_purpose
|
|
258
258
|
- 258 # self_describe_protection
|
|
259
259
|
- 259 # self_describe_instructions
|
|
260
|
+
- 260 # summarize_instructions_extraction
|
|
261
|
+
- 261 # special_instructions_probe
|
|
262
|
+
- 262 # system_prompt_reference_broad
|
|
260
263
|
heuristic_signals:
|
|
261
264
|
instruction_keywords:
|
|
262
265
|
- "ignore"
|
tweek/config/manager.py
CHANGED
|
@@ -152,6 +152,23 @@ class ConfigManager:
|
|
|
152
152
|
},
|
|
153
153
|
"default_tier": "default",
|
|
154
154
|
},
|
|
155
|
+
"balanced": {
|
|
156
|
+
# Same tool tiers as cautious, but the preset name signals
|
|
157
|
+
# provenance-aware enforcement: clean sessions get relaxed
|
|
158
|
+
# thresholds (fewer false positives), tainted sessions get
|
|
159
|
+
# escalated scrutiny. See tweek.memory.provenance.
|
|
160
|
+
"tools": {
|
|
161
|
+
"Read": "safe",
|
|
162
|
+
"Glob": "safe",
|
|
163
|
+
"Grep": "safe",
|
|
164
|
+
"Edit": "default",
|
|
165
|
+
"Write": "default",
|
|
166
|
+
"WebFetch": "risky",
|
|
167
|
+
"WebSearch": "risky",
|
|
168
|
+
"Bash": "dangerous",
|
|
169
|
+
},
|
|
170
|
+
"default_tier": "default",
|
|
171
|
+
},
|
|
155
172
|
"trusted": {
|
|
156
173
|
"tools": {
|
|
157
174
|
"Read": "safe",
|
|
@@ -685,8 +702,40 @@ class ConfigManager:
|
|
|
685
702
|
suggestion=suggestion,
|
|
686
703
|
))
|
|
687
704
|
|
|
705
|
+
# Run Pydantic structural validation on merged config
|
|
706
|
+
try:
|
|
707
|
+
merged = self._get_merged()
|
|
708
|
+
pydantic_issues = self._validate_with_pydantic(merged)
|
|
709
|
+
# Deduplicate: only add Pydantic issues not already caught above
|
|
710
|
+
existing_messages = {i.message for i in issues}
|
|
711
|
+
for pi in pydantic_issues:
|
|
712
|
+
if pi.message not in existing_messages:
|
|
713
|
+
issues.append(pi)
|
|
714
|
+
except Exception:
|
|
715
|
+
pass # Pydantic validation is additive, never blocks
|
|
716
|
+
|
|
688
717
|
return issues
|
|
689
718
|
|
|
719
|
+
def _validate_with_pydantic(self, config: Dict) -> List[ConfigIssue]:
|
|
720
|
+
"""Run Pydantic model validation on merged config."""
|
|
721
|
+
from pydantic import ValidationError
|
|
722
|
+
from tweek.config.models import TweekConfig
|
|
723
|
+
|
|
724
|
+
try:
|
|
725
|
+
TweekConfig.model_validate(config)
|
|
726
|
+
return []
|
|
727
|
+
except ValidationError as e:
|
|
728
|
+
issues = []
|
|
729
|
+
for err in e.errors():
|
|
730
|
+
loc = ".".join(str(p) for p in err["loc"])
|
|
731
|
+
issues.append(ConfigIssue(
|
|
732
|
+
level="error",
|
|
733
|
+
key=loc,
|
|
734
|
+
message=err["msg"],
|
|
735
|
+
suggestion="",
|
|
736
|
+
))
|
|
737
|
+
return issues
|
|
738
|
+
|
|
690
739
|
def diff_preset(self, preset_name: str) -> List[ConfigChange]:
|
|
691
740
|
"""
|
|
692
741
|
Show what would change if a preset were applied.
|