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.
- tweek/__init__.py +2 -2
- tweek/_keygen.py +53 -0
- tweek/audit.py +288 -0
- tweek/cli.py +5303 -2396
- tweek/cli_model.py +380 -0
- tweek/config/families.yaml +609 -0
- tweek/config/manager.py +42 -5
- tweek/config/patterns.yaml +1510 -8
- tweek/config/tiers.yaml +161 -11
- tweek/diagnostics.py +71 -2
- tweek/hooks/break_glass.py +163 -0
- tweek/hooks/feedback.py +223 -0
- tweek/hooks/overrides.py +531 -0
- tweek/hooks/post_tool_use.py +472 -0
- tweek/hooks/pre_tool_use.py +1024 -62
- tweek/integrations/openclaw.py +443 -0
- tweek/integrations/openclaw_server.py +385 -0
- tweek/licensing.py +14 -54
- tweek/logging/bundle.py +2 -2
- tweek/logging/security_log.py +56 -13
- tweek/mcp/approval.py +57 -16
- tweek/mcp/proxy.py +18 -0
- tweek/mcp/screening.py +5 -5
- tweek/mcp/server.py +4 -1
- tweek/memory/__init__.py +24 -0
- tweek/memory/queries.py +223 -0
- tweek/memory/safety.py +140 -0
- tweek/memory/schemas.py +80 -0
- tweek/memory/store.py +989 -0
- tweek/platform/__init__.py +4 -4
- tweek/plugins/__init__.py +40 -24
- tweek/plugins/base.py +1 -1
- tweek/plugins/detectors/__init__.py +3 -3
- tweek/plugins/detectors/{moltbot.py → openclaw.py} +30 -27
- tweek/plugins/git_discovery.py +16 -4
- tweek/plugins/git_registry.py +8 -2
- tweek/plugins/git_security.py +21 -9
- tweek/plugins/screening/__init__.py +10 -1
- tweek/plugins/screening/heuristic_scorer.py +477 -0
- tweek/plugins/screening/llm_reviewer.py +14 -6
- tweek/plugins/screening/local_model_reviewer.py +161 -0
- tweek/proxy/__init__.py +38 -37
- tweek/proxy/addon.py +22 -3
- tweek/proxy/interceptor.py +1 -0
- tweek/proxy/server.py +4 -2
- tweek/sandbox/__init__.py +11 -0
- tweek/sandbox/docker_bridge.py +143 -0
- tweek/sandbox/executor.py +9 -6
- tweek/sandbox/layers.py +97 -0
- tweek/sandbox/linux.py +1 -0
- tweek/sandbox/project.py +548 -0
- tweek/sandbox/registry.py +149 -0
- tweek/security/__init__.py +9 -0
- tweek/security/language.py +250 -0
- tweek/security/llm_reviewer.py +1146 -60
- tweek/security/local_model.py +331 -0
- tweek/security/local_reviewer.py +146 -0
- tweek/security/model_registry.py +371 -0
- tweek/security/rate_limiter.py +11 -6
- tweek/security/secret_scanner.py +70 -4
- tweek/security/session_analyzer.py +26 -2
- tweek/skill_template/SKILL.md +200 -0
- tweek/skill_template/__init__.py +0 -0
- tweek/skill_template/cli-reference.md +331 -0
- tweek/skill_template/overrides-reference.md +184 -0
- tweek/skill_template/scripts/__init__.py +0 -0
- tweek/skill_template/scripts/check_installed.py +170 -0
- tweek/skills/__init__.py +38 -0
- tweek/skills/config.py +150 -0
- tweek/skills/fingerprints.py +198 -0
- tweek/skills/guard.py +293 -0
- tweek/skills/isolation.py +469 -0
- tweek/skills/scanner.py +715 -0
- tweek/vault/__init__.py +0 -1
- tweek/vault/cross_platform.py +12 -1
- tweek/vault/keychain.py +87 -29
- tweek-0.2.0.dist-info/METADATA +281 -0
- tweek-0.2.0.dist-info/RECORD +121 -0
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/entry_points.txt +8 -1
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/licenses/LICENSE +80 -0
- tweek/integrations/moltbot.py +0 -243
- tweek-0.1.0.dist-info/METADATA +0 -335
- tweek-0.1.0.dist-info/RECORD +0 -85
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/WHEEL +0 -0
- {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
|
+
)
|