tweek 0.3.0__py3-none-any.whl → 0.3.1__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 +1 -1
- tweek/audit.py +2 -2
- tweek/cli.py +63 -17
- tweek/config/__init__.py +8 -0
- tweek/config/manager.py +32 -0
- tweek/config/models.py +307 -0
- tweek/config/patterns.yaml +1 -1
- tweek/hooks/post_tool_use.py +1 -1
- tweek/licensing.py +1 -1
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/METADATA +3 -2
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/RECORD +16 -15
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/WHEEL +0 -0
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.0.dist-info → tweek-0.3.1.dist-info}/top_level.txt +0 -0
tweek/__init__.py
CHANGED
tweek/audit.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Tweek Skill Audit — Security analysis for skill files and tool descriptions.
|
|
3
3
|
|
|
4
4
|
Reads skill content, detects language, translates non-English content,
|
|
5
|
-
and runs the full
|
|
5
|
+
and runs the full 259-pattern regex analysis + LLM semantic review.
|
|
6
6
|
Designed for one-time evaluation of skills before installation.
|
|
7
7
|
"""
|
|
8
8
|
|
|
@@ -173,7 +173,7 @@ def audit_content(
|
|
|
173
173
|
except Exception:
|
|
174
174
|
pass
|
|
175
175
|
|
|
176
|
-
# Step 3: Pattern matching (all
|
|
176
|
+
# Step 3: Pattern matching (all 259 patterns against English content)
|
|
177
177
|
try:
|
|
178
178
|
from tweek.hooks.pre_tool_use import PatternMatcher
|
|
179
179
|
|
tweek/cli.py
CHANGED
|
@@ -1181,26 +1181,27 @@ def _print_install_summary(
|
|
|
1181
1181
|
@main.command(
|
|
1182
1182
|
epilog="""\b
|
|
1183
1183
|
Examples:
|
|
1184
|
-
tweek unprotect Interactive — choose what to
|
|
1184
|
+
tweek unprotect Interactive — choose what to unprotect
|
|
1185
1185
|
tweek unprotect claude-code Remove Claude Code hooks
|
|
1186
1186
|
tweek unprotect claude-code --global Remove global Claude Code hooks
|
|
1187
1187
|
tweek unprotect claude-desktop Remove from Claude Desktop
|
|
1188
|
-
tweek unprotect --all Remove ALL Tweek data system-wide
|
|
1189
1188
|
"""
|
|
1190
1189
|
)
|
|
1191
1190
|
@click.argument("tool", required=False, type=click.Choice(
|
|
1192
1191
|
["claude-code", "openclaw", "claude-desktop", "chatgpt", "gemini"]))
|
|
1193
|
-
@click.option("--all", "remove_all", is_flag=True, default=False,
|
|
1194
|
-
help="Remove ALL Tweek data: hooks, skills, config, patterns, logs, MCP integrations")
|
|
1195
1192
|
@click.option("--global", "unprotect_global", is_flag=True, default=False,
|
|
1196
1193
|
help="Remove from ~/.claude/ (global installation)")
|
|
1197
1194
|
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
1198
|
-
def unprotect(tool: str,
|
|
1195
|
+
def unprotect(tool: str, unprotect_global: bool, confirm: bool):
|
|
1199
1196
|
"""Remove Tweek protection from an AI tool.
|
|
1200
1197
|
|
|
1198
|
+
This removes hooks and MCP configuration for a specific tool
|
|
1199
|
+
but keeps Tweek installed on your system. Use `tweek uninstall`
|
|
1200
|
+
to fully remove Tweek.
|
|
1201
|
+
|
|
1201
1202
|
When run without arguments, launches an interactive wizard
|
|
1202
1203
|
that walks through each protected tool asking if you want
|
|
1203
|
-
to remove protection.
|
|
1204
|
+
to remove protection.
|
|
1204
1205
|
|
|
1205
1206
|
This command can only be run from an interactive terminal.
|
|
1206
1207
|
AI agents are blocked from running it.
|
|
@@ -1217,8 +1218,8 @@ def unprotect(tool: str, remove_all: bool, unprotect_global: bool, confirm: bool
|
|
|
1217
1218
|
console.print("[white]Open a terminal and run the command directly.[/white]")
|
|
1218
1219
|
raise SystemExit(1)
|
|
1219
1220
|
|
|
1220
|
-
# No tool
|
|
1221
|
-
if not tool
|
|
1221
|
+
# No tool: run interactive wizard
|
|
1222
|
+
if not tool:
|
|
1222
1223
|
_run_unprotect_wizard()
|
|
1223
1224
|
return
|
|
1224
1225
|
|
|
@@ -1228,17 +1229,11 @@ def unprotect(tool: str, remove_all: bool, unprotect_global: bool, confirm: bool
|
|
|
1228
1229
|
global_target = Path("~/.claude").expanduser()
|
|
1229
1230
|
project_target = Path.cwd() / ".claude"
|
|
1230
1231
|
|
|
1231
|
-
if remove_all:
|
|
1232
|
-
_uninstall_everything(global_target, project_target, tweek_dir, confirm)
|
|
1233
|
-
_show_package_removal_hint()
|
|
1234
|
-
return
|
|
1235
|
-
|
|
1236
1232
|
if tool == "claude-code":
|
|
1237
1233
|
if unprotect_global:
|
|
1238
1234
|
_uninstall_scope(global_target, tweek_dir, confirm, scope_label="global")
|
|
1239
1235
|
else:
|
|
1240
1236
|
_uninstall_scope(project_target, tweek_dir, confirm, scope_label="project")
|
|
1241
|
-
_show_package_removal_hint()
|
|
1242
1237
|
return
|
|
1243
1238
|
|
|
1244
1239
|
if tool in ("claude-desktop", "chatgpt", "gemini"):
|
|
@@ -1266,6 +1261,57 @@ def unprotect(tool: str, remove_all: bool, unprotect_global: bool, confirm: bool
|
|
|
1266
1261
|
console.print("[white]Manual step: remove tweek plugin from openclaw.json[/white]")
|
|
1267
1262
|
return
|
|
1268
1263
|
|
|
1264
|
+
|
|
1265
|
+
@main.command(
|
|
1266
|
+
epilog="""\b
|
|
1267
|
+
Examples:
|
|
1268
|
+
tweek uninstall Interactive full removal
|
|
1269
|
+
tweek uninstall --all Remove ALL Tweek data system-wide
|
|
1270
|
+
tweek uninstall --all --confirm Remove everything without prompts
|
|
1271
|
+
"""
|
|
1272
|
+
)
|
|
1273
|
+
@click.option("--all", "remove_all", is_flag=True, default=False,
|
|
1274
|
+
help="Remove ALL Tweek data: hooks, skills, config, patterns, logs, MCP integrations")
|
|
1275
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompts")
|
|
1276
|
+
def uninstall(remove_all: bool, confirm: bool):
|
|
1277
|
+
"""Fully remove Tweek from your system.
|
|
1278
|
+
|
|
1279
|
+
Removes all hooks, skills, configuration, data, and optionally
|
|
1280
|
+
the Tweek package itself. For removing protection from a single
|
|
1281
|
+
tool without uninstalling, use `tweek unprotect` instead.
|
|
1282
|
+
|
|
1283
|
+
This command can only be run from an interactive terminal.
|
|
1284
|
+
AI agents are blocked from running it.
|
|
1285
|
+
"""
|
|
1286
|
+
# ─────────────────────────────────────────────────────────────
|
|
1287
|
+
# HUMAN-ONLY GATE: Block non-interactive execution
|
|
1288
|
+
# ─────────────────────────────────────────────────────────────
|
|
1289
|
+
if not sys.stdin.isatty():
|
|
1290
|
+
console.print("[red]ERROR: tweek uninstall must be run from an interactive terminal.[/red]")
|
|
1291
|
+
console.print("[white]This command cannot be run by AI agents or automated scripts.[/white]")
|
|
1292
|
+
console.print("[white]Open a terminal and run the command directly.[/white]")
|
|
1293
|
+
raise SystemExit(1)
|
|
1294
|
+
|
|
1295
|
+
console.print(TWEEK_BANNER, style="cyan")
|
|
1296
|
+
|
|
1297
|
+
tweek_dir = Path("~/.tweek").expanduser()
|
|
1298
|
+
global_target = Path("~/.claude").expanduser()
|
|
1299
|
+
project_target = Path.cwd() / ".claude"
|
|
1300
|
+
|
|
1301
|
+
if not remove_all:
|
|
1302
|
+
# Interactive: ask what to remove
|
|
1303
|
+
console.print("[bold]What would you like to remove?[/bold]")
|
|
1304
|
+
console.print()
|
|
1305
|
+
console.print(" [bold]1.[/bold] Everything (all hooks, data, config, and package)")
|
|
1306
|
+
console.print(" [bold]2.[/bold] Cancel")
|
|
1307
|
+
console.print()
|
|
1308
|
+
choice = click.prompt("Select", type=click.IntRange(1, 2), default=2)
|
|
1309
|
+
if choice == 2:
|
|
1310
|
+
console.print("[white]Cancelled[/white]")
|
|
1311
|
+
return
|
|
1312
|
+
console.print()
|
|
1313
|
+
|
|
1314
|
+
_uninstall_everything(global_target, project_target, tweek_dir, confirm)
|
|
1269
1315
|
_show_package_removal_hint()
|
|
1270
1316
|
|
|
1271
1317
|
|
|
@@ -1991,7 +2037,7 @@ def update(check: bool):
|
|
|
1991
2037
|
Patterns are stored in ~/.tweek/patterns/ and can be updated
|
|
1992
2038
|
independently of the Tweek application.
|
|
1993
2039
|
|
|
1994
|
-
All
|
|
2040
|
+
All 259 patterns are included free. PRO tier adds LLM review,
|
|
1995
2041
|
session analysis, and rate limiting.
|
|
1996
2042
|
"""
|
|
1997
2043
|
import subprocess
|
|
@@ -2267,7 +2313,7 @@ def audit(path, translate, llm_review, json_out):
|
|
|
2267
2313
|
credential theft, data exfiltration, and other attack patterns.
|
|
2268
2314
|
|
|
2269
2315
|
Non-English content is detected and translated to English before
|
|
2270
|
-
running all
|
|
2316
|
+
running all 259 regex patterns. LLM semantic review provides
|
|
2271
2317
|
additional analysis for obfuscated attacks.
|
|
2272
2318
|
|
|
2273
2319
|
\b
|
|
@@ -2695,7 +2741,7 @@ def protect_openclaw(port, paranoid, preset):
|
|
|
2695
2741
|
|
|
2696
2742
|
# Show configuration
|
|
2697
2743
|
console.print(f" Scanner: port {result.scanner_port} -> wrapping OpenClaw gateway")
|
|
2698
|
-
console.print(f" Preset: {result.preset} (
|
|
2744
|
+
console.print(f" Preset: {result.preset} (259 patterns + rate limiting)")
|
|
2699
2745
|
|
|
2700
2746
|
# Check for API key
|
|
2701
2747
|
anthropic_key = os.environ.get("ANTHROPIC_API_KEY")
|
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/manager.py
CHANGED
|
@@ -685,8 +685,40 @@ class ConfigManager:
|
|
|
685
685
|
suggestion=suggestion,
|
|
686
686
|
))
|
|
687
687
|
|
|
688
|
+
# Run Pydantic structural validation on merged config
|
|
689
|
+
try:
|
|
690
|
+
merged = self._get_merged()
|
|
691
|
+
pydantic_issues = self._validate_with_pydantic(merged)
|
|
692
|
+
# Deduplicate: only add Pydantic issues not already caught above
|
|
693
|
+
existing_messages = {i.message for i in issues}
|
|
694
|
+
for pi in pydantic_issues:
|
|
695
|
+
if pi.message not in existing_messages:
|
|
696
|
+
issues.append(pi)
|
|
697
|
+
except Exception:
|
|
698
|
+
pass # Pydantic validation is additive, never blocks
|
|
699
|
+
|
|
688
700
|
return issues
|
|
689
701
|
|
|
702
|
+
def _validate_with_pydantic(self, config: Dict) -> List[ConfigIssue]:
|
|
703
|
+
"""Run Pydantic model validation on merged config."""
|
|
704
|
+
from pydantic import ValidationError
|
|
705
|
+
from tweek.config.models import TweekConfig
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
TweekConfig.model_validate(config)
|
|
709
|
+
return []
|
|
710
|
+
except ValidationError as e:
|
|
711
|
+
issues = []
|
|
712
|
+
for err in e.errors():
|
|
713
|
+
loc = ".".join(str(p) for p in err["loc"])
|
|
714
|
+
issues.append(ConfigIssue(
|
|
715
|
+
level="error",
|
|
716
|
+
key=loc,
|
|
717
|
+
message=err["msg"],
|
|
718
|
+
suggestion="",
|
|
719
|
+
))
|
|
720
|
+
return issues
|
|
721
|
+
|
|
690
722
|
def diff_preset(self, preset_name: str) -> List[ConfigChange]:
|
|
691
723
|
"""
|
|
692
724
|
Show what would change if a preset were applied.
|
tweek/config/models.py
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pydantic models for Tweek configuration validation.
|
|
3
|
+
|
|
4
|
+
These models define the schema for tiers.yaml (the main configuration file)
|
|
5
|
+
and patterns.yaml (attack pattern definitions). They provide:
|
|
6
|
+
- Type-safe configuration loading with automatic validation
|
|
7
|
+
- Human-readable error messages for invalid configuration
|
|
8
|
+
- JSON Schema export for documentation and IDE support
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# Enums
|
|
22
|
+
# ============================================================================
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SecurityTierValue(str, Enum):
|
|
26
|
+
"""Valid security tier values."""
|
|
27
|
+
SAFE = "safe"
|
|
28
|
+
DEFAULT = "default"
|
|
29
|
+
RISKY = "risky"
|
|
30
|
+
DANGEROUS = "dangerous"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class NonEnglishHandling(str, Enum):
|
|
34
|
+
"""How non-English content is handled during screening."""
|
|
35
|
+
ESCALATE = "escalate"
|
|
36
|
+
TRANSLATE = "translate"
|
|
37
|
+
BOTH = "both"
|
|
38
|
+
NONE = "none"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PatternSeverity(str, Enum):
|
|
42
|
+
"""Severity levels for attack patterns."""
|
|
43
|
+
CRITICAL = "critical"
|
|
44
|
+
HIGH = "high"
|
|
45
|
+
MEDIUM = "medium"
|
|
46
|
+
LOW = "low"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PatternConfidence(str, Enum):
|
|
50
|
+
"""Confidence levels for attack pattern matches."""
|
|
51
|
+
DETERMINISTIC = "deterministic"
|
|
52
|
+
HEURISTIC = "heuristic"
|
|
53
|
+
CONTEXTUAL = "contextual"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ============================================================================
|
|
57
|
+
# Configuration Section Models
|
|
58
|
+
# ============================================================================
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LLMReviewLocalConfig(BaseModel):
|
|
62
|
+
"""Configuration for local LLM server (Ollama, LM Studio)."""
|
|
63
|
+
enabled: bool = True
|
|
64
|
+
probe_timeout: float = Field(default=2.0, gt=0)
|
|
65
|
+
timeout_seconds: float = Field(default=30.0, gt=0)
|
|
66
|
+
ollama_host: Optional[str] = None
|
|
67
|
+
lm_studio_host: Optional[str] = None
|
|
68
|
+
preferred_models: List[str] = Field(default_factory=list)
|
|
69
|
+
validate_on_first_use: bool = True
|
|
70
|
+
min_validation_score: float = Field(default=0.6, ge=0.0, le=1.0)
|
|
71
|
+
|
|
72
|
+
model_config = {"extra": "allow"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LLMReviewFallbackConfig(BaseModel):
|
|
76
|
+
"""Configuration for LLM fallback chain."""
|
|
77
|
+
enabled: bool = True
|
|
78
|
+
order: List[str] = Field(default_factory=lambda: ["local", "anthropic", "openai"])
|
|
79
|
+
|
|
80
|
+
model_config = {"extra": "allow"}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LLMReviewConfig(BaseModel):
|
|
84
|
+
"""Configuration for LLM-based semantic review."""
|
|
85
|
+
enabled: bool = True
|
|
86
|
+
provider: str = "auto"
|
|
87
|
+
model: str = "auto"
|
|
88
|
+
base_url: Optional[str] = None
|
|
89
|
+
api_key_env: Optional[str] = None
|
|
90
|
+
timeout_seconds: float = Field(default=15.0, gt=0)
|
|
91
|
+
local: LLMReviewLocalConfig = Field(default_factory=LLMReviewLocalConfig)
|
|
92
|
+
fallback: LLMReviewFallbackConfig = Field(default_factory=LLMReviewFallbackConfig)
|
|
93
|
+
|
|
94
|
+
model_config = {"extra": "allow"}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class RateLimitingConfig(BaseModel):
|
|
98
|
+
"""Configuration for request rate limiting."""
|
|
99
|
+
enabled: bool = True
|
|
100
|
+
burst_window_seconds: int = Field(default=10, gt=0)
|
|
101
|
+
burst_threshold: int = Field(default=5, gt=0)
|
|
102
|
+
max_per_minute: int = Field(default=60, gt=0)
|
|
103
|
+
max_dangerous_per_minute: int = Field(default=10, gt=0)
|
|
104
|
+
max_same_command_per_minute: int = Field(default=5, gt=0)
|
|
105
|
+
|
|
106
|
+
model_config = {"extra": "allow"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SessionAnalysisConfig(BaseModel):
|
|
110
|
+
"""Configuration for session-level analysis."""
|
|
111
|
+
enabled: bool = True
|
|
112
|
+
lookback_minutes: int = Field(default=30, gt=0)
|
|
113
|
+
alert_on_risk_score: float = Field(default=0.7, ge=0.0, le=1.0)
|
|
114
|
+
|
|
115
|
+
model_config = {"extra": "allow"}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class HeuristicScorerConfig(BaseModel):
|
|
119
|
+
"""Configuration for heuristic scoring bridge."""
|
|
120
|
+
enabled: bool = True
|
|
121
|
+
threshold: float = Field(default=0.4, ge=0.0, le=1.0)
|
|
122
|
+
log_all_scores: bool = False
|
|
123
|
+
|
|
124
|
+
model_config = {"extra": "allow"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class LocalModelConfig(BaseModel):
|
|
128
|
+
"""Configuration for local ONNX model inference."""
|
|
129
|
+
enabled: bool = True
|
|
130
|
+
model: str = "auto"
|
|
131
|
+
escalate_to_llm: bool = True
|
|
132
|
+
escalate_min_confidence: float = Field(default=0.1, ge=0.0, le=1.0)
|
|
133
|
+
escalate_max_confidence: float = Field(default=0.9, ge=0.0, le=1.0)
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def check_escalation_bounds(self) -> "LocalModelConfig":
|
|
137
|
+
if self.escalate_min_confidence >= self.escalate_max_confidence:
|
|
138
|
+
raise ValueError(
|
|
139
|
+
f"escalate_min_confidence ({self.escalate_min_confidence}) "
|
|
140
|
+
f"must be less than escalate_max_confidence ({self.escalate_max_confidence})"
|
|
141
|
+
)
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
model_config = {"extra": "allow"}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TierDefinition(BaseModel):
|
|
148
|
+
"""Definition of a security tier."""
|
|
149
|
+
description: str
|
|
150
|
+
screening: List[str] = Field(default_factory=list)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class EscalationRule(BaseModel):
|
|
154
|
+
"""A content-based escalation rule."""
|
|
155
|
+
pattern: str
|
|
156
|
+
description: str
|
|
157
|
+
escalate_to: SecurityTierValue
|
|
158
|
+
|
|
159
|
+
@field_validator("pattern")
|
|
160
|
+
@classmethod
|
|
161
|
+
def validate_regex(cls, v: str) -> str:
|
|
162
|
+
try:
|
|
163
|
+
re.compile(v)
|
|
164
|
+
except re.error as e:
|
|
165
|
+
raise ValueError(f"Invalid regex pattern: {e}") from e
|
|
166
|
+
return v
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SensitiveDirectory(BaseModel):
|
|
170
|
+
"""A sensitive directory that triggers path boundary escalation."""
|
|
171
|
+
pattern: str
|
|
172
|
+
escalate_to: SecurityTierValue
|
|
173
|
+
description: str = ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class PathBoundaryConfig(BaseModel):
|
|
177
|
+
"""Configuration for path boundary escalation."""
|
|
178
|
+
enabled: bool = True
|
|
179
|
+
default_escalate_to: SecurityTierValue = SecurityTierValue.RISKY
|
|
180
|
+
sensitive_directories: List[SensitiveDirectory] = Field(default_factory=list)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class OpenClawConfig(BaseModel):
|
|
184
|
+
"""Configuration for OpenClaw integration."""
|
|
185
|
+
enabled: bool = False
|
|
186
|
+
gateway_port: int = Field(default=18789, gt=0, le=65535)
|
|
187
|
+
scanner_port: int = Field(default=9878, gt=0, le=65535)
|
|
188
|
+
plugin_installed: bool = False
|
|
189
|
+
preset: str = "cautious"
|
|
190
|
+
|
|
191
|
+
model_config = {"extra": "allow"}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ============================================================================
|
|
195
|
+
# Root Configuration Model
|
|
196
|
+
# ============================================================================
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TweekConfig(BaseModel):
|
|
200
|
+
"""
|
|
201
|
+
Root Pydantic model for Tweek configuration (tiers.yaml / config.yaml).
|
|
202
|
+
|
|
203
|
+
Validates the merged configuration from builtin, user, and project layers.
|
|
204
|
+
Uses extra="allow" at the root level to be forward-compatible with new
|
|
205
|
+
config keys added in future versions.
|
|
206
|
+
"""
|
|
207
|
+
version: Optional[int] = None
|
|
208
|
+
|
|
209
|
+
# Core tool/skill classification
|
|
210
|
+
tools: Dict[str, SecurityTierValue] = Field(default_factory=dict)
|
|
211
|
+
skills: Dict[str, SecurityTierValue] = Field(default_factory=dict)
|
|
212
|
+
default_tier: SecurityTierValue = SecurityTierValue.DEFAULT
|
|
213
|
+
|
|
214
|
+
# Tier definitions
|
|
215
|
+
tiers: Dict[str, TierDefinition] = Field(default_factory=dict)
|
|
216
|
+
|
|
217
|
+
# Screening configuration
|
|
218
|
+
llm_review: Optional[LLMReviewConfig] = None
|
|
219
|
+
rate_limiting: Optional[RateLimitingConfig] = None
|
|
220
|
+
session_analysis: Optional[SessionAnalysisConfig] = None
|
|
221
|
+
heuristic_scorer: Optional[HeuristicScorerConfig] = None
|
|
222
|
+
local_model: Optional[LocalModelConfig] = None
|
|
223
|
+
|
|
224
|
+
# Escalation rules
|
|
225
|
+
escalations: List[EscalationRule] = Field(default_factory=list)
|
|
226
|
+
|
|
227
|
+
# Path boundary
|
|
228
|
+
path_boundary: Optional[PathBoundaryConfig] = None
|
|
229
|
+
|
|
230
|
+
# Non-English handling
|
|
231
|
+
non_english_handling: NonEnglishHandling = NonEnglishHandling.ESCALATE
|
|
232
|
+
|
|
233
|
+
# Integration configs
|
|
234
|
+
proxy: Optional[Dict[str, Any]] = None
|
|
235
|
+
mcp: Optional[Dict[str, Any]] = None
|
|
236
|
+
sandbox: Optional[Dict[str, Any]] = None
|
|
237
|
+
isolation_chamber: Optional[Dict[str, Any]] = None
|
|
238
|
+
plugins: Optional[Dict[str, Any]] = None
|
|
239
|
+
openclaw: Optional[OpenClawConfig] = None
|
|
240
|
+
|
|
241
|
+
model_config = {"extra": "allow"}
|
|
242
|
+
|
|
243
|
+
@field_validator("tools", mode="before")
|
|
244
|
+
@classmethod
|
|
245
|
+
def coerce_tool_tiers(cls, v: Any) -> Any:
|
|
246
|
+
"""Accept string tier values and coerce to SecurityTierValue."""
|
|
247
|
+
if isinstance(v, dict):
|
|
248
|
+
return {k: SecurityTierValue(val) if isinstance(val, str) else val for k, val in v.items()}
|
|
249
|
+
return v
|
|
250
|
+
|
|
251
|
+
@field_validator("skills", mode="before")
|
|
252
|
+
@classmethod
|
|
253
|
+
def coerce_skill_tiers(cls, v: Any) -> Any:
|
|
254
|
+
"""Accept string tier values and coerce to SecurityTierValue."""
|
|
255
|
+
if isinstance(v, dict):
|
|
256
|
+
return {k: SecurityTierValue(val) if isinstance(val, str) else val for k, val in v.items()}
|
|
257
|
+
return v
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ============================================================================
|
|
261
|
+
# Pattern Schema Model
|
|
262
|
+
# ============================================================================
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class PatternDefinition(BaseModel):
|
|
266
|
+
"""A single attack pattern definition from patterns.yaml."""
|
|
267
|
+
id: int
|
|
268
|
+
name: str
|
|
269
|
+
description: str
|
|
270
|
+
regex: str
|
|
271
|
+
severity: PatternSeverity
|
|
272
|
+
confidence: PatternConfidence
|
|
273
|
+
family: Optional[str] = None
|
|
274
|
+
|
|
275
|
+
@field_validator("regex")
|
|
276
|
+
@classmethod
|
|
277
|
+
def validate_regex(cls, v: str) -> str:
|
|
278
|
+
try:
|
|
279
|
+
re.compile(v)
|
|
280
|
+
except re.error as e:
|
|
281
|
+
raise ValueError(f"Invalid regex in pattern: {e}") from e
|
|
282
|
+
return v
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class PatternsConfig(BaseModel):
|
|
286
|
+
"""Root model for patterns.yaml."""
|
|
287
|
+
version: int
|
|
288
|
+
pattern_count: int = 0
|
|
289
|
+
patterns: List[PatternDefinition] = Field(default_factory=list)
|
|
290
|
+
|
|
291
|
+
@model_validator(mode="after")
|
|
292
|
+
def check_pattern_count(self) -> "PatternsConfig":
|
|
293
|
+
actual = len(self.patterns)
|
|
294
|
+
if self.pattern_count > 0 and actual != self.pattern_count:
|
|
295
|
+
raise ValueError(
|
|
296
|
+
f"pattern_count ({self.pattern_count}) does not match "
|
|
297
|
+
f"actual number of patterns ({actual})"
|
|
298
|
+
)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
@model_validator(mode="after")
|
|
302
|
+
def check_unique_ids(self) -> "PatternsConfig":
|
|
303
|
+
ids = [p.id for p in self.patterns]
|
|
304
|
+
if len(ids) != len(set(ids)):
|
|
305
|
+
dupes = [i for i in ids if ids.count(i) > 1]
|
|
306
|
+
raise ValueError(f"Duplicate pattern IDs: {set(dupes)}")
|
|
307
|
+
return self
|
tweek/config/patterns.yaml
CHANGED
tweek/hooks/post_tool_use.py
CHANGED
|
@@ -11,7 +11,7 @@ web pages, documents, and other ingested content.
|
|
|
11
11
|
|
|
12
12
|
Screening Pipeline:
|
|
13
13
|
1. Language Detection — identify non-English content
|
|
14
|
-
2. Pattern Matching —
|
|
14
|
+
2. Pattern Matching — 259 regex patterns for known attack vectors
|
|
15
15
|
3. LLM Review — semantic analysis if non-English escalation triggers
|
|
16
16
|
|
|
17
17
|
Claude Code PostToolUse Protocol:
|
tweek/licensing.py
CHANGED
|
@@ -74,7 +74,7 @@ class LicenseInfo:
|
|
|
74
74
|
# Only compliance and team management features require a license.
|
|
75
75
|
TIER_FEATURES = {
|
|
76
76
|
Tier.FREE: [
|
|
77
|
-
"pattern_matching", # All
|
|
77
|
+
"pattern_matching", # All 259 patterns included free
|
|
78
78
|
"basic_logging",
|
|
79
79
|
"vault_storage",
|
|
80
80
|
"cli_commands",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tweek
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Defense-in-depth security for AI coding assistants - protect credentials, code, and system from prompt injection attacks
|
|
5
5
|
Author: Tommy Mancino
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -45,6 +45,7 @@ Provides-Extra: dev
|
|
|
45
45
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
46
46
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
47
47
|
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
|
|
48
|
+
Requires-Dist: hypothesis>=6.98.0; extra == "dev"
|
|
48
49
|
Requires-Dist: black>=23.0; extra == "dev"
|
|
49
50
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
50
51
|
Requires-Dist: twine>=4.0; extra == "dev"
|
|
@@ -165,7 +166,7 @@ Turn 3: cat ~/.ssh/id_rsa → BLOCKED: path_escalation anomaly
|
|
|
165
166
|
|
|
166
167
|
**Response injection** — Malicious instructions hidden in tool responses are caught at ingestion.
|
|
167
168
|
|
|
168
|
-
See the full [Attack Patterns Reference](docs/ATTACK_PATTERNS.md) for all 259 patterns across
|
|
169
|
+
See the full [Attack Patterns Reference](docs/ATTACK_PATTERNS.md) for all 259 patterns across 11 categories.
|
|
169
170
|
|
|
170
171
|
---
|
|
171
172
|
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
tweek/__init__.py,sha256=
|
|
1
|
+
tweek/__init__.py,sha256=NppVIy7NWIJrgFbWfAVveJtg-UdwqA9ux98hciWBeLM,360
|
|
2
2
|
tweek/_keygen.py,sha256=UapwIKNSwaRWdqHoJoF3hmKuiux6aIiFGe8WVskTbI8,1286
|
|
3
|
-
tweek/audit.py,sha256=
|
|
4
|
-
tweek/cli.py,sha256=
|
|
3
|
+
tweek/audit.py,sha256=OmCUagbx_fkCorcrZt2ebTtDm-rr4fRKkZpxZdvZens,8868
|
|
4
|
+
tweek/cli.py,sha256=Ad54k7bDws9Eg7z3oeEVR85Ni9DQz72JJVUFC1WV_zA,256803
|
|
5
5
|
tweek/cli_helpers.py,sha256=Q2NTOkyRTOIPNLMqY2jA5_tuzDPksAGwGXYPRK3bzoY,5538
|
|
6
6
|
tweek/cli_model.py,sha256=iMZStFqA0Nqyzm4rxSbhD4v-AqcO6h5NI72AR7cldoY,12853
|
|
7
7
|
tweek/diagnostics.py,sha256=KbtXQH8QrRBoyIFWumL6q9--aQQdR0tUo2GzjMhwpII,24601
|
|
8
|
-
tweek/licensing.py,sha256=
|
|
9
|
-
tweek/config/__init__.py,sha256=
|
|
8
|
+
tweek/licensing.py,sha256=wYN8wBYVCp1RbAi_sWeF7gKKBSU116ncX3tnZawYUpQ,11703
|
|
9
|
+
tweek/config/__init__.py,sha256=ENwimeLZd2gSJXpkASMY45hbMUDn2RwM-Zl_RMvpCbQ,772
|
|
10
10
|
tweek/config/allowed_dirs.yaml,sha256=dMF_DqKgQThzkdIEoXzDBfAjbopGrk0HTkiM7ENmBaU,788
|
|
11
11
|
tweek/config/families.yaml,sha256=jkNO0UsmX3MFlTKC9Or3p8_MlD3ZtHM0SrQIYFqx9i8,18212
|
|
12
|
-
tweek/config/manager.py,sha256=
|
|
13
|
-
tweek/config/
|
|
12
|
+
tweek/config/manager.py,sha256=Jk9l_UJM9e5_fxTvWFXrU0677u9HCttmunahp36woBE,40591
|
|
13
|
+
tweek/config/models.py,sha256=RbVjC2pxnkrBKanS6QGDrHwPVkmss5ouG_dqAHf_C3Q,10018
|
|
14
|
+
tweek/config/patterns.yaml,sha256=hu0lphSN0i_bY8kla65bTaBEQR8phhrb3BLC1KprMLw,85376
|
|
14
15
|
tweek/config/tiers.yaml,sha256=9hIXQ9izVKXd8ptoCsQiBo2r_XY8RvIk7VWrhWggkbc,10191
|
|
15
16
|
tweek/hooks/__init__.py,sha256=GcgDjPdhZayxmyZ4-GfBa-ISARNtt9087RsuprRq2-s,54
|
|
16
17
|
tweek/hooks/break_glass.py,sha256=GNMhCtLWPylNMlQ5QfsoUkEjgIT1Uk1Ik7HvRWeE5N8,4636
|
|
17
18
|
tweek/hooks/feedback.py,sha256=uuA4opHYyBHC5sElBz-fr2Je3cg2DAv-aRHvETZcag0,6555
|
|
18
19
|
tweek/hooks/overrides.py,sha256=1Yw_NPpZMvcFG_uyNY-ouBKSSomnxOptRedSjzkkhmE,18635
|
|
19
|
-
tweek/hooks/post_tool_use.py,sha256=
|
|
20
|
+
tweek/hooks/post_tool_use.py,sha256=22ugZdlZn2Q0eUcUucelrF18N7mCgaC_agb7kZT51Ww,17195
|
|
20
21
|
tweek/hooks/pre_tool_use.py,sha256=70XbonRSGh8rYpDlI4R_Z5Ug2LwU4iLyLsS87I5xlqc,71743
|
|
21
22
|
tweek/integrations/__init__.py,sha256=sl7wFwbygmnruugX4bO2EUjoXxBlCpzTKbj-1zHuUPg,78
|
|
22
23
|
tweek/integrations/openclaw.py,sha256=jX99__ODGI7Cq6gclSTK2pI5lsI7UGh5_iCHmq1R8RY,13798
|
|
@@ -113,11 +114,11 @@ tweek/skills/scanner.py,sha256=PaeZNnwxLTGls2O3hQaDgBhGw9jVJThPjfKCY_05_nI,27574
|
|
|
113
114
|
tweek/vault/__init__.py,sha256=L408fjdRYL8-VqLEsyyHSO9PkBDhd_2mPIbrCu53YhM,980
|
|
114
115
|
tweek/vault/cross_platform.py,sha256=D4UvX_7OpSo8iRx5sc2OUUWQIk8JHhgeFBYk1MbyIj4,8251
|
|
115
116
|
tweek/vault/keychain.py,sha256=XL18-SUj7HwuqxLXZDViuCH81--KMu68jN9Szn1aeyw,10624
|
|
116
|
-
tweek-0.3.
|
|
117
|
-
tweek-0.3.
|
|
117
|
+
tweek-0.3.1.dist-info/licenses/LICENSE,sha256=rjoDzr1vAf0bsqZglpIyekU5aewIkCk4jHZZDvVI2BE,15269
|
|
118
|
+
tweek-0.3.1.dist-info/licenses/NOTICE,sha256=taQokyDes5UTRNEC67G-13VmqvUyTOncrrT33pCcWL0,8729
|
|
118
119
|
tweek-openclaw-plugin/node_modules/flatted/python/flatted.py,sha256=UYburBDqkySaTfSpntPCUJRxiBGcplusJM7ECX8FEgA,3860
|
|
119
|
-
tweek-0.3.
|
|
120
|
-
tweek-0.3.
|
|
121
|
-
tweek-0.3.
|
|
122
|
-
tweek-0.3.
|
|
123
|
-
tweek-0.3.
|
|
120
|
+
tweek-0.3.1.dist-info/METADATA,sha256=iR7qpsuY7fLnF2DO8OWFrqUTE2vuDv3_VNMWddDIZMU,11939
|
|
121
|
+
tweek-0.3.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
122
|
+
tweek-0.3.1.dist-info/entry_points.txt,sha256=YXThD6UiF5XQXwqW33sphsvz-Bl4Zm6pm-xq-5wcCYE,1337
|
|
123
|
+
tweek-0.3.1.dist-info/top_level.txt,sha256=jtNcCxjoGXN8IBqEVL0F3LHDrZD_B0S-4XF9-Ur7Pbc,28
|
|
124
|
+
tweek-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|