tweek 0.1.0__py3-none-any.whl → 0.2.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 (85) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/_keygen.py +53 -0
  3. tweek/audit.py +288 -0
  4. tweek/cli.py +5303 -2396
  5. tweek/cli_model.py +380 -0
  6. tweek/config/families.yaml +609 -0
  7. tweek/config/manager.py +42 -5
  8. tweek/config/patterns.yaml +1510 -8
  9. tweek/config/tiers.yaml +161 -11
  10. tweek/diagnostics.py +71 -2
  11. tweek/hooks/break_glass.py +163 -0
  12. tweek/hooks/feedback.py +223 -0
  13. tweek/hooks/overrides.py +531 -0
  14. tweek/hooks/post_tool_use.py +472 -0
  15. tweek/hooks/pre_tool_use.py +1024 -62
  16. tweek/integrations/openclaw.py +443 -0
  17. tweek/integrations/openclaw_server.py +385 -0
  18. tweek/licensing.py +14 -54
  19. tweek/logging/bundle.py +2 -2
  20. tweek/logging/security_log.py +56 -13
  21. tweek/mcp/approval.py +57 -16
  22. tweek/mcp/proxy.py +18 -0
  23. tweek/mcp/screening.py +5 -5
  24. tweek/mcp/server.py +4 -1
  25. tweek/memory/__init__.py +24 -0
  26. tweek/memory/queries.py +223 -0
  27. tweek/memory/safety.py +140 -0
  28. tweek/memory/schemas.py +80 -0
  29. tweek/memory/store.py +989 -0
  30. tweek/platform/__init__.py +4 -4
  31. tweek/plugins/__init__.py +40 -24
  32. tweek/plugins/base.py +1 -1
  33. tweek/plugins/detectors/__init__.py +3 -3
  34. tweek/plugins/detectors/{moltbot.py → openclaw.py} +30 -27
  35. tweek/plugins/git_discovery.py +16 -4
  36. tweek/plugins/git_registry.py +8 -2
  37. tweek/plugins/git_security.py +21 -9
  38. tweek/plugins/screening/__init__.py +10 -1
  39. tweek/plugins/screening/heuristic_scorer.py +477 -0
  40. tweek/plugins/screening/llm_reviewer.py +14 -6
  41. tweek/plugins/screening/local_model_reviewer.py +161 -0
  42. tweek/proxy/__init__.py +38 -37
  43. tweek/proxy/addon.py +22 -3
  44. tweek/proxy/interceptor.py +1 -0
  45. tweek/proxy/server.py +4 -2
  46. tweek/sandbox/__init__.py +11 -0
  47. tweek/sandbox/docker_bridge.py +143 -0
  48. tweek/sandbox/executor.py +9 -6
  49. tweek/sandbox/layers.py +97 -0
  50. tweek/sandbox/linux.py +1 -0
  51. tweek/sandbox/project.py +548 -0
  52. tweek/sandbox/registry.py +149 -0
  53. tweek/security/__init__.py +9 -0
  54. tweek/security/language.py +250 -0
  55. tweek/security/llm_reviewer.py +1146 -60
  56. tweek/security/local_model.py +331 -0
  57. tweek/security/local_reviewer.py +146 -0
  58. tweek/security/model_registry.py +371 -0
  59. tweek/security/rate_limiter.py +11 -6
  60. tweek/security/secret_scanner.py +70 -4
  61. tweek/security/session_analyzer.py +26 -2
  62. tweek/skill_template/SKILL.md +200 -0
  63. tweek/skill_template/__init__.py +0 -0
  64. tweek/skill_template/cli-reference.md +331 -0
  65. tweek/skill_template/overrides-reference.md +184 -0
  66. tweek/skill_template/scripts/__init__.py +0 -0
  67. tweek/skill_template/scripts/check_installed.py +170 -0
  68. tweek/skills/__init__.py +38 -0
  69. tweek/skills/config.py +150 -0
  70. tweek/skills/fingerprints.py +198 -0
  71. tweek/skills/guard.py +293 -0
  72. tweek/skills/isolation.py +469 -0
  73. tweek/skills/scanner.py +715 -0
  74. tweek/vault/__init__.py +0 -1
  75. tweek/vault/cross_platform.py +12 -1
  76. tweek/vault/keychain.py +87 -29
  77. tweek-0.2.0.dist-info/METADATA +281 -0
  78. tweek-0.2.0.dist-info/RECORD +121 -0
  79. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/entry_points.txt +8 -1
  80. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/licenses/LICENSE +80 -0
  81. tweek/integrations/moltbot.py +0 -243
  82. tweek-0.1.0.dist-info/METADATA +0 -335
  83. tweek-0.1.0.dist-info/RECORD +0 -85
  84. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/WHEEL +0 -0
  85. {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/top_level.txt +0 -0
tweek/skills/guard.py ADDED
@@ -0,0 +1,293 @@
1
+ """
2
+ Tweek Skill Isolation Guard — Self-Protection
3
+
4
+ Prevents the AI agent from bypassing the isolation chamber by:
5
+ 1. Blocking writes to Claude's skill directories (force use of chamber)
6
+ 2. Blocking writes to chamber/jail directories
7
+ 3. Detecting shell commands that manipulate skill directories
8
+ 4. Detecting autonomous skill downloads
9
+
10
+ Follows the same pattern as tweek/hooks/overrides.py for protected config files.
11
+ """
12
+
13
+ import re
14
+ from pathlib import Path
15
+ from typing import Dict, Optional, Tuple
16
+
17
+ from pathlib import Path as _Path
18
+
19
+ from tweek.skills import (
20
+ CHAMBER_DIR,
21
+ CLAUDE_GLOBAL_SKILLS,
22
+ JAIL_DIR,
23
+ REPORTS_DIR,
24
+ SKILLS_DIR,
25
+ )
26
+
27
+ # OpenClaw skill directory
28
+ OPENCLAW_SKILLS_DIR = _Path.home() / ".openclaw" / "workspace" / "skills"
29
+
30
+ # Paths that AI cannot write to directly
31
+ PROTECTED_SKILL_PATHS = [
32
+ SKILLS_DIR, # ~/.tweek/skills/ (chamber, jail, reports)
33
+ CLAUDE_GLOBAL_SKILLS, # ~/.claude/skills/
34
+ OPENCLAW_SKILLS_DIR, # ~/.openclaw/workspace/skills/
35
+ ]
36
+
37
+ # Regex patterns for detecting skill-related shell commands
38
+ _SKILL_DIR_PATTERNS = [
39
+ # Moving/copying out of jail
40
+ re.compile(
41
+ r"(cp|mv|rsync|ln)\s+.*\.tweek/skills/(jail|chamber)",
42
+ re.IGNORECASE,
43
+ ),
44
+ # Moving/copying into Claude's skill directories
45
+ re.compile(
46
+ r"(cp|mv|rsync|ln)\s+.*\.claude/skills/",
47
+ re.IGNORECASE,
48
+ ),
49
+ # Moving/copying into OpenClaw's skill directories
50
+ re.compile(
51
+ r"(cp|mv|rsync|ln)\s+.*\.openclaw/workspace/skills/",
52
+ re.IGNORECASE,
53
+ ),
54
+ # Symlink attacks targeting skill directories
55
+ re.compile(
56
+ r"ln\s+(-sf?\s+)?.*\.claude/skills",
57
+ re.IGNORECASE,
58
+ ),
59
+ re.compile(
60
+ r"ln\s+(-sf?\s+)?.*\.tweek/skills",
61
+ re.IGNORECASE,
62
+ ),
63
+ re.compile(
64
+ r"ln\s+(-sf?\s+)?.*\.openclaw/workspace/skills",
65
+ re.IGNORECASE,
66
+ ),
67
+ # Direct creation of SKILL.md via shell
68
+ re.compile(
69
+ r"(echo|cat|tee|printf)\s+.*>\s*.*\.claude/skills/.*SKILL\.md",
70
+ re.IGNORECASE,
71
+ ),
72
+ re.compile(
73
+ r"(echo|cat|tee|printf)\s+.*>\s*.*\.openclaw/workspace/skills/.*SKILL\.md",
74
+ re.IGNORECASE,
75
+ ),
76
+ ]
77
+
78
+ # Patterns for detecting skill downloads
79
+ _DOWNLOAD_PATTERNS = [
80
+ re.compile(
81
+ r"(curl|wget)\s+[^\n]*https?://[^\s]+.*>\s*.*SKILL",
82
+ re.IGNORECASE,
83
+ ),
84
+ re.compile(
85
+ r"(curl|wget)\s+[^\n]*https?://[^\s]+.*>\s*.*\.claude/skills/",
86
+ re.IGNORECASE,
87
+ ),
88
+ re.compile(
89
+ r"(curl|wget)\s+[^\n]*https?://[^\s]+.*>\s*.*\.openclaw/workspace/skills/",
90
+ re.IGNORECASE,
91
+ ),
92
+ re.compile(
93
+ r"git\s+clone\s+[^\n]*skill",
94
+ re.IGNORECASE,
95
+ ),
96
+ re.compile(
97
+ r"(curl|wget)\s+[^\n]*SKILL\.md",
98
+ re.IGNORECASE,
99
+ ),
100
+ ]
101
+
102
+
103
+ def is_skill_install_attempt(tool_name: str, tool_input: Dict) -> bool:
104
+ """
105
+ Check if a Write/Edit tool call is attempting to install a skill directly,
106
+ bypassing the isolation chamber.
107
+
108
+ Args:
109
+ tool_name: The tool being invoked (Write, Edit)
110
+ tool_input: The tool's input parameters
111
+
112
+ Returns:
113
+ True if this appears to be a direct skill installation attempt
114
+ """
115
+ if tool_name not in ("Write", "Edit"):
116
+ return False
117
+
118
+ file_path = tool_input.get("file_path", "")
119
+ if not file_path:
120
+ return False
121
+
122
+ # Normalize the path
123
+ try:
124
+ resolved = Path(file_path).expanduser().resolve()
125
+ except (ValueError, OSError):
126
+ return False
127
+
128
+ # Check if targeting Claude's skill directories
129
+ claude_skills = CLAUDE_GLOBAL_SKILLS.resolve()
130
+ try:
131
+ resolved.relative_to(claude_skills)
132
+ # Writing anything into ~/.claude/skills/ is blocked
133
+ return True
134
+ except ValueError:
135
+ pass
136
+
137
+ # Check if targeting OpenClaw's skill directories
138
+ openclaw_skills = OPENCLAW_SKILLS_DIR.resolve()
139
+ try:
140
+ resolved.relative_to(openclaw_skills)
141
+ return True
142
+ except ValueError:
143
+ pass
144
+
145
+ # Check project-level skills
146
+ # Look for .claude/skills/ or .openclaw/workspace/skills/ pattern anywhere in the path
147
+ path_str = str(resolved)
148
+ if ".claude/skills/" in path_str and "SKILL.md" in path_str:
149
+ return True
150
+ if ".openclaw/workspace/skills/" in path_str and "SKILL.md" in path_str:
151
+ return True
152
+
153
+ return False
154
+
155
+
156
+ def is_chamber_protected_path(file_path: str) -> bool:
157
+ """
158
+ Check if a file path is within the isolation chamber's protected directories.
159
+
160
+ Args:
161
+ file_path: The file path being accessed
162
+
163
+ Returns:
164
+ True if the path is protected from AI modification
165
+ """
166
+ if not file_path:
167
+ return False
168
+
169
+ try:
170
+ resolved = Path(file_path).expanduser().resolve()
171
+ except (ValueError, OSError):
172
+ return False
173
+
174
+ for protected in PROTECTED_SKILL_PATHS:
175
+ try:
176
+ protected_resolved = protected.resolve()
177
+ resolved.relative_to(protected_resolved)
178
+ return True
179
+ except ValueError:
180
+ continue
181
+
182
+ return False
183
+
184
+
185
+ def bash_targets_chamber(command: str) -> bool:
186
+ """
187
+ Check if a Bash command targets the isolation chamber or Claude skill directories.
188
+
189
+ Args:
190
+ command: The shell command string
191
+
192
+ Returns:
193
+ True if the command manipulates skill directories
194
+ """
195
+ if not command:
196
+ return False
197
+
198
+ for pattern in _SKILL_DIR_PATTERNS:
199
+ if pattern.search(command):
200
+ return True
201
+
202
+ return False
203
+
204
+
205
+ def is_skill_download_attempt(command: str) -> Tuple[bool, str]:
206
+ """
207
+ Check if a Bash command is attempting to download skill content.
208
+
209
+ Returns (True, url_or_description) if a download is detected,
210
+ (False, "") otherwise. Downloads are not blocked but trigger an "ask"
211
+ decision so the user can confirm.
212
+
213
+ Args:
214
+ command: The shell command string
215
+
216
+ Returns:
217
+ (is_download, description)
218
+ """
219
+ if not command:
220
+ return False, ""
221
+
222
+ for pattern in _DOWNLOAD_PATTERNS:
223
+ match = pattern.search(command)
224
+ if match:
225
+ return True, match.group(0)[:200]
226
+
227
+ return False, ""
228
+
229
+
230
+ def get_skill_guard_reason(tool_name: str, tool_input: Dict) -> Optional[str]:
231
+ """
232
+ Get a human-readable reason if this tool call should be blocked by the guard.
233
+
234
+ Returns None if no guard rule applies, or a reason string if blocked.
235
+
236
+ Args:
237
+ tool_name: The tool being invoked
238
+ tool_input: The tool's input parameters
239
+
240
+ Returns:
241
+ Block reason string, or None if allowed
242
+ """
243
+ if tool_name in ("Write", "Edit"):
244
+ file_path = tool_input.get("file_path", "")
245
+
246
+ if is_skill_install_attempt(tool_name, tool_input):
247
+ return (
248
+ "TWEEK SKILL GUARD: Direct skill installation is blocked.\n"
249
+ "Skills must go through the isolation chamber for security scanning.\n"
250
+ "Use: tweek skills chamber import <path>"
251
+ )
252
+
253
+ if is_chamber_protected_path(file_path):
254
+ return (
255
+ "TWEEK SKILL GUARD: Isolation chamber directories are protected.\n"
256
+ "The chamber, jail, and reports directories cannot be modified by AI.\n"
257
+ "Use the 'tweek skills' CLI commands to manage skills."
258
+ )
259
+
260
+ elif tool_name == "Bash":
261
+ command = tool_input.get("command", "")
262
+
263
+ if bash_targets_chamber(command):
264
+ return (
265
+ "TWEEK SKILL GUARD: Shell commands targeting skill directories are blocked.\n"
266
+ "Use the 'tweek skills' CLI commands to manage skills."
267
+ )
268
+
269
+ return None
270
+
271
+
272
+ def get_skill_download_prompt(command: str) -> Optional[str]:
273
+ """
274
+ Get an "ask" prompt message if this command appears to download skill content.
275
+
276
+ Returns None if no download detected, or a prompt string for the user.
277
+
278
+ Args:
279
+ command: The shell command string
280
+
281
+ Returns:
282
+ Prompt message string, or None if not a download
283
+ """
284
+ is_download, desc = is_skill_download_attempt(command)
285
+ if not is_download:
286
+ return None
287
+
288
+ return (
289
+ f"TWEEK SKILL GUARD: Detected potential skill download.\n"
290
+ f"Command: {desc}\n"
291
+ f"Downloaded skills should go through the isolation chamber.\n"
292
+ f"Allow this download?"
293
+ )