tweek 0.2.1__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/cli_helpers.py CHANGED
@@ -32,7 +32,7 @@ def print_error(message: str, fix_hint: str = "") -> None:
32
32
  """Print an error message with red X and optional fix hint."""
33
33
  console.print(f"[red]\u2717[/red] {message}")
34
34
  if fix_hint:
35
- console.print(f" [dim]Hint: {fix_hint}[/dim]")
35
+ console.print(f" [white]Hint: {fix_hint}[/white]")
36
36
 
37
37
 
38
38
  def print_health_banner(checks: "List") -> None:
@@ -51,7 +51,7 @@ def print_health_banner(checks: "List") -> None:
51
51
 
52
52
  panel = Panel(
53
53
  f"[bold {color}]{verdict_text}[/bold {color}]\n"
54
- f"[dim]Run 'tweek doctor' for details[/dim]",
54
+ f"[white]Run 'tweek doctor' for details[/white]",
55
55
  border_style=color,
56
56
  padding=(0, 2),
57
57
  )
@@ -63,11 +63,11 @@ def format_command_example(command: str, description: str) -> str:
63
63
  Format a single command example line.
64
64
 
65
65
  Args:
66
- command: The command string, e.g., "tweek install --scope global"
66
+ command: The command string, e.g., "tweek protect claude-code --scope global"
67
67
  description: Brief explanation of what it does.
68
68
 
69
69
  Returns:
70
- Formatted string like " tweek install --scope global Install globally"
70
+ Formatted string like " tweek protect claude-code --scope global Install globally"
71
71
  """
72
72
  return f" {command:<40s} {description}"
73
73
 
@@ -141,11 +141,11 @@ def print_doctor_results(checks: "List") -> None:
141
141
  CheckStatus.OK: ("[green]OK[/green] ", "green"),
142
142
  CheckStatus.WARNING: ("[yellow]WARN[/yellow] ", "yellow"),
143
143
  CheckStatus.ERROR: ("[red]ERROR[/red] ", "red"),
144
- CheckStatus.SKIPPED: ("[dim]SKIP[/dim] ", "dim"),
144
+ CheckStatus.SKIPPED: ("[white]SKIP[/white] ", "white"),
145
145
  }
146
146
 
147
147
  for check in checks:
148
- style_text, _ = status_styles.get(check.status, ("[dim]???[/dim] ", "dim"))
148
+ style_text, _ = status_styles.get(check.status, ("[white]???[/white] ", "white"))
149
149
  console.print(f" {style_text} {check.label:<22s} {check.message}")
150
150
 
151
151
  # Verdict
tweek/cli_model.py CHANGED
@@ -90,7 +90,7 @@ def model_download(name: str, force: bool):
90
90
  console.print()
91
91
  console.print(f"[green]Model downloaded to {model_dir}[/green]")
92
92
  console.print(
93
- f"[dim]Local screening is now active for risky/dangerous operations.[/dim]"
93
+ f"[white]Local screening is now active for risky/dangerous operations.[/white]"
94
94
  )
95
95
 
96
96
  except ModelDownloadError as e:
@@ -130,8 +130,8 @@ def model_list(available: bool):
130
130
  defn.display_name,
131
131
  f"~{defn.size_mb:.0f} MB",
132
132
  defn.license,
133
- "[green]yes[/green]" if installed else "[dim]no[/dim]",
134
- "[green]yes[/green]" if active else "[dim]-[/dim]",
133
+ "[green]yes[/green]" if installed else "[white]no[/white]",
134
+ "[green]yes[/green]" if active else "[white]-[/white]",
135
135
  )
136
136
 
137
137
  console.print(table)
@@ -158,7 +158,7 @@ def model_list(available: bool):
158
158
  name,
159
159
  defn.display_name if defn else name,
160
160
  size_str,
161
- "[green]yes[/green]" if active else "[dim]-[/dim]",
161
+ "[green]yes[/green]" if active else "[white]-[/white]",
162
162
  )
163
163
 
164
164
  console.print(table)
@@ -221,10 +221,10 @@ def model_status():
221
221
  )
222
222
  else:
223
223
  fallback_lines.append(
224
- " Cloud LLM: [dim]none (no API keys configured)[/dim]"
224
+ " Cloud LLM: [white]none (no API keys configured)[/white]"
225
225
  )
226
226
  except Exception:
227
- fallback_lines.append(" Cloud LLM: [dim]unavailable[/dim]")
227
+ fallback_lines.append(" Cloud LLM: [white]unavailable[/white]")
228
228
 
229
229
  # Overall status
230
230
  if LOCAL_MODEL_AVAILABLE and installed:
@@ -232,7 +232,7 @@ def model_status():
232
232
  elif LOCAL_MODEL_AVAILABLE and not installed:
233
233
  status = "[yellow]Ready[/yellow] - Dependencies installed, model not downloaded"
234
234
  else:
235
- status = "[dim]Inactive[/dim] - Install dependencies: pip install tweek[local-models]"
235
+ status = "[white]Inactive[/white] - Install dependencies: pip install tweek[local-models]"
236
236
 
237
237
  content = f"Status: {status}\n\n"
238
238
  content += "[bold]Dependencies[/bold]\n" + "\n".join(deps_lines) + "\n\n"
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
@@ -173,7 +173,7 @@ class ConfigManager:
173
173
  "plugins", "mcp", "proxy", "sandbox", "isolation_chamber",
174
174
  "llm_review", "local_model", "rate_limiting", "session_analysis",
175
175
  "path_boundary", "non_english_handling", "version", "tiers",
176
- "heuristic_scorer",
176
+ "heuristic_scorer", "openclaw",
177
177
  }
178
178
 
179
179
  def __init__(
@@ -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
@@ -1,5 +1,5 @@
1
1
  # Tweek Attack Pattern Definitions v3
2
- # All 215 patterns included FREE
2
+ # All 259 patterns included FREE
3
3
  #
4
4
  # Update via: tweek update (pulls from github.com/gettweek/tweek)
5
5
  #
tweek/diagnostics.py CHANGED
@@ -3,7 +3,7 @@
3
3
  Tweek Diagnostics Engine
4
4
 
5
5
  Health check system for verifying Tweek installation, configuration,
6
- and runtime dependencies. Used by `tweek doctor` and the status banner.
6
+ and runtime dependencies. Used by `tweek doctor` and the health banner.
7
7
  """
8
8
 
9
9
  import json
@@ -30,7 +30,7 @@ class HealthCheck:
30
30
  label: str # Human label: "Hook Installation"
31
31
  status: CheckStatus
32
32
  message: str # Description: "Global hooks installed at ~/.claude/"
33
- fix_hint: str = "" # Recovery: "Run: tweek install --scope global"
33
+ fix_hint: str = "" # Recovery: "Run: tweek protect claude-code --global"
34
34
 
35
35
 
36
36
  def run_health_checks(verbose: bool = False) -> List[HealthCheck]:
@@ -168,7 +168,7 @@ def _check_hooks_installed(verbose: bool = False) -> HealthCheck:
168
168
  label="Hook Installation",
169
169
  status=CheckStatus.WARNING,
170
170
  message="Installed in project only (./.claude)",
171
- fix_hint="Run: tweek install --scope global (to protect all projects)",
171
+ fix_hint="Run: tweek protect claude-code --global (to protect all projects)",
172
172
  )
173
173
  else:
174
174
  return HealthCheck(
@@ -176,7 +176,7 @@ def _check_hooks_installed(verbose: bool = False) -> HealthCheck:
176
176
  label="Hook Installation",
177
177
  status=CheckStatus.ERROR,
178
178
  message="No hooks installed",
179
- fix_hint="Run: tweek install",
179
+ fix_hint="Run: tweek protect claude-code",
180
180
  )
181
181
 
182
182
 
@@ -597,18 +597,53 @@ def _check_llm_review(verbose: bool = False) -> HealthCheck:
597
597
  get_llm_reviewer,
598
598
  _detect_local_server,
599
599
  FallbackReviewProvider,
600
+ DEFAULT_API_KEY_ENVS,
600
601
  )
602
+ from tweek.security.local_model import LOCAL_MODEL_AVAILABLE
601
603
 
602
604
  reviewer = get_llm_reviewer()
603
605
 
604
606
  if not reviewer.enabled:
607
+ # Check which env vars are missing to give specific guidance
608
+ missing_keys = []
609
+ for provider, env_names in DEFAULT_API_KEY_ENVS.items():
610
+ if isinstance(env_names, list):
611
+ if not any(os.environ.get(e) for e in env_names):
612
+ missing_keys.append(f"{' or '.join(env_names)} ({provider})")
613
+ else:
614
+ if not os.environ.get(env_names):
615
+ missing_keys.append(f"{env_names} ({provider})")
616
+
617
+ hint_parts = [
618
+ "To enable cloud LLM review for uncertain classifications:",
619
+ " Set one of: " + ", ".join(
620
+ k.split(" (")[0] for k in missing_keys
621
+ ),
622
+ ]
623
+
624
+ if not LOCAL_MODEL_AVAILABLE:
625
+ hint_parts.append(
626
+ " Or install the local model: pip install 'tweek[local]'"
627
+ )
628
+ else:
629
+ hint_parts.append(
630
+ " Local ONNX model is available but could not initialize"
631
+ )
632
+
633
+ hint_parts.append(
634
+ " Or install Ollama (https://ollama.ai) for local LLM review"
635
+ )
636
+ hint_parts.append(
637
+ " See: docs/CONFIGURATION.md or ~/.tweek/config.yaml"
638
+ )
639
+
605
640
  return HealthCheck(
606
641
  name="llm_review",
607
642
  label="LLM Review",
608
643
  status=CheckStatus.WARNING,
609
- message="No LLM provider available (review disabled)",
610
- fix_hint="Set ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY. "
611
- "Or install Ollama: https://ollama.ai",
644
+ message="No LLM provider available review disabled, "
645
+ "pattern matching and heuristic scoring still active",
646
+ fix_hint="\n".join(hint_parts),
612
647
  )
613
648
 
614
649
  # Build status message
@@ -636,6 +671,23 @@ def _check_llm_review(verbose: bool = False) -> HealthCheck:
636
671
  except Exception:
637
672
  pass
638
673
 
674
+ # In verbose mode, report escalation path
675
+ if verbose:
676
+ if provider_name == "local":
677
+ # Check if cloud escalation is available
678
+ cloud_env_found = any(
679
+ os.environ.get(e) if isinstance(e, str)
680
+ else any(os.environ.get(v) for v in e)
681
+ for e in DEFAULT_API_KEY_ENVS.values()
682
+ )
683
+ if cloud_env_found:
684
+ parts.append("cloud escalation: available")
685
+ else:
686
+ parts.append("cloud escalation: not configured")
687
+
688
+ if LOCAL_MODEL_AVAILABLE:
689
+ parts.append("local ONNX: available")
690
+
639
691
  return HealthCheck(
640
692
  name="llm_review",
641
693
  label="LLM Review",
@@ -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 — 215 regex patterns for known attack vectors
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:
@@ -925,10 +925,10 @@ def process_hook(input_data: dict, logger: SecurityLogger) -> dict:
925
925
  }
926
926
  }
927
927
 
928
- # Block AI from running tweek trust/untrust/uninstall — human-only commands
928
+ # Block AI from running tweek trust/untrust/uninstall/unprotect — human-only commands
929
929
  command_stripped = command.strip()
930
- if re.match(r"tweek\s+(trust|untrust|uninstall)\b", command_stripped):
931
- if "uninstall" in command_stripped:
930
+ if re.match(r"tweek\s+(trust|untrust|uninstall|unprotect)\b", command_stripped):
931
+ if "uninstall" in command_stripped or "unprotect" in command_stripped:
932
932
  reason = (
933
933
  "TWEEK SELF-PROTECTION: Uninstall must be done by a human.\n"
934
934
  "Run this command directly in your terminal:\n"
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 215 patterns included free
77
+ "pattern_matching", # All 259 patterns included free
78
78
  "basic_logging",
79
79
  "vault_storage",
80
80
  "cli_commands",
tweek/mcp/approval_cli.py CHANGED
@@ -39,7 +39,7 @@ def display_pending(queue: ApprovalQueue) -> int:
39
39
  pending = queue.get_pending()
40
40
 
41
41
  if not pending:
42
- console.print("[dim]No pending approval requests.[/dim]")
42
+ console.print("[white]No pending approval requests.[/white]")
43
43
  return 0
44
44
 
45
45
  table = Table(title="Pending Approval Requests", show_lines=True)
@@ -112,7 +112,7 @@ def display_request_detail(request: ApprovalRequest):
112
112
  remaining = request.time_remaining
113
113
  if remaining > 0:
114
114
  lines.append("")
115
- lines.append(f"[dim]Auto-deny in {int(remaining)}s[/dim]")
115
+ lines.append(f"[white]Auto-deny in {int(remaining)}s[/white]")
116
116
 
117
117
  panel = Panel(
118
118
  "\n".join(lines),
@@ -160,7 +160,7 @@ def run_approval_daemon(
160
160
  # Prompt for decision
161
161
  decision = _prompt_decision(console, req)
162
162
  if decision == "quit":
163
- console.print("[dim]Exiting approval daemon.[/dim]")
163
+ console.print("[white]Exiting approval daemon.[/white]")
164
164
  return
165
165
  elif decision == "skip":
166
166
  continue
@@ -200,7 +200,7 @@ def run_approval_daemon(
200
200
  time.sleep(poll_interval)
201
201
 
202
202
  except KeyboardInterrupt:
203
- console.print("\n[dim]Approval daemon stopped.[/dim]")
203
+ console.print("\n[white]Approval daemon stopped.[/white]")
204
204
 
205
205
  # Print summary
206
206
  stats = queue.get_stats()