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
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek Local Model Inference Engine
4
+
5
+ Runs ONNX-based security classifiers for local prompt injection detection.
6
+ No cloud API calls needed — inference runs entirely on-device.
7
+
8
+ Dependencies (optional):
9
+ pip install onnxruntime tokenizers numpy
10
+
11
+ When dependencies are not installed, the module gracefully degrades:
12
+ LOCAL_MODEL_AVAILABLE will be False, and get_local_model() returns None.
13
+ """
14
+
15
+ import threading
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+ from typing import Dict, List, Optional
20
+
21
+ # Optional dependency guards
22
+ try:
23
+ import onnxruntime as ort
24
+
25
+ ONNX_AVAILABLE = True
26
+ except ImportError:
27
+ ONNX_AVAILABLE = False
28
+
29
+ try:
30
+ from tokenizers import Tokenizer
31
+
32
+ TOKENIZERS_AVAILABLE = True
33
+ except ImportError:
34
+ TOKENIZERS_AVAILABLE = False
35
+
36
+ try:
37
+ import numpy as np
38
+
39
+ NUMPY_AVAILABLE = True
40
+ except ImportError:
41
+ NUMPY_AVAILABLE = False
42
+
43
+ LOCAL_MODEL_AVAILABLE = ONNX_AVAILABLE and TOKENIZERS_AVAILABLE and NUMPY_AVAILABLE
44
+
45
+
46
+ # ============================================================================
47
+ # DATA CLASSES
48
+ # ============================================================================
49
+
50
+
51
+ @dataclass
52
+ class LocalModelResult:
53
+ """Result from local model inference."""
54
+
55
+ risk_level: str # "safe", "suspicious", "dangerous"
56
+ label: str # Raw label from model (e.g., "benign", "injection", "jailbreak")
57
+ confidence: float # 0.0 - 1.0, confidence in the predicted label
58
+ all_scores: Dict[str, float] # All label scores
59
+ should_escalate: bool # Whether to escalate to cloud LLM
60
+ model_name: str
61
+ inference_time_ms: float
62
+
63
+ @property
64
+ def is_dangerous(self) -> bool:
65
+ return self.risk_level == "dangerous"
66
+
67
+ @property
68
+ def is_suspicious(self) -> bool:
69
+ return self.risk_level in ("suspicious", "dangerous")
70
+
71
+
72
+ # ============================================================================
73
+ # INFERENCE ENGINE
74
+ # ============================================================================
75
+
76
+
77
+ class LocalModelInference:
78
+ """ONNX-based local model inference engine.
79
+
80
+ Thread-safe with lazy loading. The model is loaded on first predict()
81
+ call and cached for subsequent calls.
82
+ """
83
+
84
+ def __init__(self, model_dir: Path, model_name: str = "unknown"):
85
+ self._model_dir = model_dir
86
+ self._model_name = model_name
87
+ self._session: Optional[object] = None # ort.InferenceSession
88
+ self._tokenizer: Optional[object] = None # Tokenizer
89
+ self._lock = threading.Lock()
90
+ self._loaded = False
91
+
92
+ # Load metadata
93
+ self._label_map: Dict[int, str] = {}
94
+ self._risk_map: Dict[str, str] = {}
95
+ self._max_length: int = 512
96
+ self._escalate_min: float = 0.1
97
+ self._escalate_max: float = 0.9
98
+
99
+ def _load_metadata(self) -> None:
100
+ """Load model metadata from catalog or meta file."""
101
+ from tweek.security.model_registry import get_model_definition
102
+
103
+ definition = get_model_definition(self._model_name)
104
+ if definition:
105
+ self._label_map = definition.label_map
106
+ self._risk_map = definition.risk_map
107
+ self._max_length = definition.max_length
108
+ self._escalate_min = definition.escalate_min_confidence
109
+ self._escalate_max = definition.escalate_max_confidence
110
+ return
111
+
112
+ # Fallback: try to load from model_meta.yaml
113
+ meta_path = self._model_dir / "model_meta.yaml"
114
+ if meta_path.exists():
115
+ import yaml
116
+
117
+ with open(meta_path) as f:
118
+ meta = yaml.safe_load(f) or {}
119
+
120
+ self._label_map = {
121
+ int(k): v for k, v in meta.get("label_map", {}).items()
122
+ }
123
+ self._risk_map = meta.get("risk_map", {})
124
+ self._max_length = meta.get("max_length", 512)
125
+
126
+ def load(self) -> None:
127
+ """Load the model and tokenizer. Thread-safe."""
128
+ if self._loaded:
129
+ return
130
+
131
+ with self._lock:
132
+ if self._loaded:
133
+ return
134
+
135
+ if not LOCAL_MODEL_AVAILABLE:
136
+ raise RuntimeError(
137
+ "Local model dependencies not installed. "
138
+ "Install with: pip install tweek[local-models]"
139
+ )
140
+
141
+ model_path = self._model_dir / "model.onnx"
142
+ tokenizer_path = self._model_dir / "tokenizer.json"
143
+
144
+ if not model_path.exists():
145
+ raise FileNotFoundError(
146
+ f"Model file not found: {model_path}. "
147
+ f"Run 'tweek model download' to install."
148
+ )
149
+
150
+ if not tokenizer_path.exists():
151
+ raise FileNotFoundError(
152
+ f"Tokenizer file not found: {tokenizer_path}. "
153
+ f"Run 'tweek model download' to install."
154
+ )
155
+
156
+ # Load ONNX session with CPU-only execution
157
+ sess_options = ort.SessionOptions()
158
+ sess_options.graph_optimization_level = (
159
+ ort.GraphOptimizationLevel.ORT_ENABLE_ALL
160
+ )
161
+ sess_options.intra_op_num_threads = 1 # Minimize CPU impact
162
+
163
+ self._session = ort.InferenceSession(
164
+ str(model_path),
165
+ sess_options,
166
+ providers=["CPUExecutionProvider"],
167
+ )
168
+
169
+ # Load tokenizer
170
+ self._tokenizer = Tokenizer.from_file(str(tokenizer_path))
171
+ self._tokenizer.enable_truncation(max_length=self._max_length)
172
+ self._tokenizer.enable_padding(
173
+ length=self._max_length, pad_id=0, pad_token="[PAD]"
174
+ )
175
+
176
+ # Load metadata
177
+ self._load_metadata()
178
+
179
+ self._loaded = True
180
+
181
+ def is_loaded(self) -> bool:
182
+ """Check if the model is loaded."""
183
+ return self._loaded
184
+
185
+ def unload(self) -> None:
186
+ """Unload the model and free memory."""
187
+ with self._lock:
188
+ self._session = None
189
+ self._tokenizer = None
190
+ self._loaded = False
191
+
192
+ def predict(self, text: str) -> LocalModelResult:
193
+ """Run inference on the given text.
194
+
195
+ Args:
196
+ text: The command or content to classify.
197
+
198
+ Returns:
199
+ LocalModelResult with classification and confidence.
200
+ """
201
+ self.load()
202
+
203
+ start_time = time.perf_counter()
204
+
205
+ # Tokenize
206
+ encoding = self._tokenizer.encode(text)
207
+ input_ids = np.array([encoding.ids], dtype=np.int64)
208
+ attention_mask = np.array([encoding.attention_mask], dtype=np.int64)
209
+
210
+ # Run inference
211
+ feeds = {
212
+ "input_ids": input_ids,
213
+ "attention_mask": attention_mask,
214
+ }
215
+
216
+ # Some models also need token_type_ids
217
+ input_names = [inp.name for inp in self._session.get_inputs()]
218
+ if "token_type_ids" in input_names:
219
+ token_type_ids = np.zeros_like(input_ids)
220
+ feeds["token_type_ids"] = token_type_ids
221
+
222
+ outputs = self._session.run(None, feeds)
223
+ logits = outputs[0][0] # First output, first batch item
224
+
225
+ # Softmax
226
+ scores = _softmax(logits)
227
+
228
+ # Get predicted label
229
+ predicted_idx = int(np.argmax(scores))
230
+ confidence = float(scores[predicted_idx])
231
+
232
+ # Map to label and risk
233
+ label = self._label_map.get(predicted_idx, f"label_{predicted_idx}")
234
+ risk_level = self._risk_map.get(label, "suspicious")
235
+
236
+ # Build all scores dict
237
+ all_scores = {}
238
+ for idx, score in enumerate(scores):
239
+ lbl = self._label_map.get(idx, f"label_{idx}")
240
+ all_scores[lbl] = float(score)
241
+
242
+ # Determine escalation
243
+ should_escalate = self._escalate_min <= confidence <= self._escalate_max
244
+ # If the prediction is "safe" with high confidence, don't escalate
245
+ if risk_level == "safe" and confidence > self._escalate_max:
246
+ should_escalate = False
247
+ # If the prediction is "dangerous" with high confidence, don't escalate
248
+ if risk_level == "dangerous" and confidence > self._escalate_max:
249
+ should_escalate = False
250
+
251
+ elapsed_ms = (time.perf_counter() - start_time) * 1000
252
+
253
+ return LocalModelResult(
254
+ risk_level=risk_level,
255
+ label=label,
256
+ confidence=confidence,
257
+ all_scores=all_scores,
258
+ should_escalate=should_escalate,
259
+ model_name=self._model_name,
260
+ inference_time_ms=round(elapsed_ms, 2),
261
+ )
262
+
263
+
264
+ # ============================================================================
265
+ # UTILITIES
266
+ # ============================================================================
267
+
268
+
269
+ def _softmax(logits) -> "np.ndarray":
270
+ """Compute softmax over logits."""
271
+ exp_logits = np.exp(logits - np.max(logits))
272
+ return exp_logits / exp_logits.sum()
273
+
274
+
275
+ # ============================================================================
276
+ # SINGLETON
277
+ # ============================================================================
278
+
279
+ _local_model: Optional[LocalModelInference] = None
280
+ _local_model_lock = threading.Lock()
281
+
282
+
283
+ def get_local_model(model_name: Optional[str] = None) -> Optional[LocalModelInference]:
284
+ """Get the singleton local model instance.
285
+
286
+ Returns None if local model dependencies are not installed or
287
+ the model is not downloaded.
288
+
289
+ Args:
290
+ model_name: Override model name. None = use configured default.
291
+
292
+ Returns:
293
+ LocalModelInference instance, or None if unavailable.
294
+ """
295
+ if not LOCAL_MODEL_AVAILABLE:
296
+ return None
297
+
298
+ global _local_model
299
+
300
+ if _local_model is not None:
301
+ return _local_model
302
+
303
+ with _local_model_lock:
304
+ if _local_model is not None:
305
+ return _local_model
306
+
307
+ from tweek.security.model_registry import (
308
+ get_default_model_name,
309
+ get_model_dir,
310
+ is_model_installed,
311
+ )
312
+
313
+ name = model_name or get_default_model_name()
314
+ if not is_model_installed(name):
315
+ return None
316
+
317
+ _local_model = LocalModelInference(
318
+ model_dir=get_model_dir(name),
319
+ model_name=name,
320
+ )
321
+
322
+ return _local_model
323
+
324
+
325
+ def reset_local_model() -> None:
326
+ """Reset the singleton local model (for testing)."""
327
+ global _local_model
328
+ with _local_model_lock:
329
+ if _local_model is not None:
330
+ _local_model.unload()
331
+ _local_model = None
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Tweek Local Model Review Provider
4
+
5
+ Bridges the local ONNX model into the ReviewProvider interface used by
6
+ LLMReviewer. This allows the local model to be a drop-in replacement
7
+ for cloud LLM providers with zero changes to the hook pipeline.
8
+
9
+ Two-tier escalation:
10
+ - High-confidence local results are returned directly (~20ms)
11
+ - Uncertain results escalate to cloud LLM if available (~500-5000ms)
12
+ - If no cloud LLM is available, local result is used as-is
13
+ """
14
+
15
+ import json
16
+ import re
17
+ from typing import Optional
18
+
19
+ from tweek.security.llm_reviewer import ReviewProvider, ReviewProviderError
20
+
21
+
22
+ class LocalModelReviewProvider(ReviewProvider):
23
+ """ReviewProvider backed by a local ONNX model.
24
+
25
+ Runs inference locally and returns JSON-formatted results compatible
26
+ with LLMReviewer._parse_response(). Optionally escalates uncertain
27
+ results to a cloud LLM provider.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ model_name: str = "deberta-v3-injection",
33
+ escalation_provider: Optional[ReviewProvider] = None,
34
+ ):
35
+ """Initialize the local model review provider.
36
+
37
+ Args:
38
+ model_name: Name of the local model to use.
39
+ escalation_provider: Optional cloud LLM provider for uncertain results.
40
+ """
41
+ self._model_name = model_name
42
+ self._escalation_provider = escalation_provider
43
+
44
+ def call(self, system_prompt: str, user_prompt: str, max_tokens: int = 256) -> str:
45
+ """Run local inference and return JSON result.
46
+
47
+ Extracts the command from <untrusted_command> tags in the user prompt,
48
+ runs local inference, and returns a JSON string in the same format
49
+ that LLMReviewer._parse_response() expects.
50
+
51
+ If the local model is uncertain and an escalation provider is
52
+ available, the request is forwarded to the cloud LLM.
53
+
54
+ Args:
55
+ system_prompt: System-level instructions (used for escalation only).
56
+ user_prompt: User message containing <untrusted_command> tags.
57
+ max_tokens: Max tokens (used for escalation only).
58
+
59
+ Returns:
60
+ JSON string with risk_level, reason, and confidence.
61
+ """
62
+ from tweek.security.local_model import get_local_model
63
+
64
+ # Extract command from untrusted_command tags
65
+ command = self._extract_command(user_prompt)
66
+ if not command:
67
+ return json.dumps({
68
+ "risk_level": "safe",
69
+ "reason": "No command content to analyze",
70
+ "confidence": 1.0,
71
+ })
72
+
73
+ model = get_local_model(self._model_name)
74
+ if model is None:
75
+ # Model not available — fall through to escalation or safe default
76
+ if self._escalation_provider:
77
+ return self._escalation_provider.call(
78
+ system_prompt, user_prompt, max_tokens
79
+ )
80
+ return json.dumps({
81
+ "risk_level": "safe",
82
+ "reason": "Local model not available",
83
+ "confidence": 0.0,
84
+ })
85
+
86
+ # Run local inference
87
+ result = model.predict(command)
88
+
89
+ # Check if we should escalate to cloud LLM
90
+ if result.should_escalate and self._escalation_provider:
91
+ try:
92
+ return self._escalation_provider.call(
93
+ system_prompt, user_prompt, max_tokens
94
+ )
95
+ except ReviewProviderError:
96
+ # Cloud LLM failed — fall back to local result
97
+ pass
98
+
99
+ # Map local result to LLM reviewer JSON format
100
+ return json.dumps({
101
+ "risk_level": result.risk_level,
102
+ "reason": (
103
+ f"Local model ({result.model_name}): "
104
+ f"{result.label} (confidence: {result.confidence:.1%})"
105
+ ),
106
+ "confidence": result.confidence,
107
+ })
108
+
109
+ def is_available(self) -> bool:
110
+ """Check if the local model is available."""
111
+ from tweek.security.local_model import LOCAL_MODEL_AVAILABLE, get_local_model
112
+
113
+ if not LOCAL_MODEL_AVAILABLE:
114
+ return False
115
+
116
+ model = get_local_model(self._model_name)
117
+ return model is not None
118
+
119
+ @property
120
+ def name(self) -> str:
121
+ return "local"
122
+
123
+ @property
124
+ def model_name(self) -> str:
125
+ return self._model_name
126
+
127
+ @staticmethod
128
+ def _extract_command(user_prompt: str) -> str:
129
+ """Extract the command from <untrusted_command> tags.
130
+
131
+ Args:
132
+ user_prompt: The full user prompt from LLMReviewer.
133
+
134
+ Returns:
135
+ The extracted command text, or the full prompt if no tags found.
136
+ """
137
+ match = re.search(
138
+ r"<untrusted_command>\s*(.*?)\s*</untrusted_command>",
139
+ user_prompt,
140
+ re.DOTALL,
141
+ )
142
+ if match:
143
+ return match.group(1).strip()
144
+
145
+ # Fallback: use the whole prompt (minus any obvious framing text)
146
+ return user_prompt.strip()