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_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}")
@@ -1,23 +1,22 @@
1
- # Tweek Directory Safety Configuration
1
+ # Tweek Directory Safety Configuration (bundled default)
2
2
  #
3
- # This file controls WHERE Tweek hooks are active.
4
- # This is a SAFETY FEATURE to prevent accidental activation in production.
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 (production mode)
8
- # allowed_directories: - List of directories where Tweek activates
8
+ # global_enabled: true - Activate Tweek everywhere (default for end users)
9
+ # allowed_directories: - Restrict Tweek to specific directories only
9
10
  #
10
- # IMPORTANT: Without this file, Tweek is DISABLED everywhere.
11
+ # Lookup order:
12
+ # 1. ~/.tweek/allowed_dirs.yaml (user override — not in repo)
13
+ # 2. This file (bundled default)
11
14
 
12
- # Set to true to enable Tweek globally (for production deployment)
13
- global_enabled: false
15
+ # Production default: Tweek is active everywhere after install
16
+ global_enabled: true
14
17
 
15
- # Directories where Tweek hooks will activate
16
- # Tweek will also activate in subdirectories of these paths
17
- allowed_directories:
18
- # Test environment only (safe for development)
19
- - ~/AI/tweek/test-environment
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
@@ -1,5 +1,5 @@
1
1
  # Tweek Pattern Family Definitions v1
2
- # Groups the 259 attack patterns by attack class for:
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.