tweek 0.3.1__py3-none-any.whl → 0.4.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 +2 -2
- tweek/audit.py +2 -2
- tweek/cli.py +78 -6605
- tweek/cli_config.py +643 -0
- tweek/cli_configure.py +413 -0
- tweek/cli_core.py +718 -0
- tweek/cli_dry_run.py +390 -0
- tweek/cli_helpers.py +316 -0
- tweek/cli_install.py +1666 -0
- tweek/cli_logs.py +301 -0
- tweek/cli_mcp.py +148 -0
- tweek/cli_memory.py +343 -0
- tweek/cli_plugins.py +748 -0
- tweek/cli_protect.py +564 -0
- tweek/cli_proxy.py +405 -0
- tweek/cli_security.py +236 -0
- tweek/cli_skills.py +289 -0
- tweek/cli_uninstall.py +551 -0
- tweek/cli_vault.py +313 -0
- tweek/config/allowed_dirs.yaml +16 -17
- tweek/config/families.yaml +4 -1
- tweek/config/manager.py +17 -0
- tweek/config/patterns.yaml +29 -5
- tweek/config/templates/config.yaml.template +212 -0
- tweek/config/templates/env.template +45 -0
- tweek/config/templates/overrides.yaml.template +121 -0
- tweek/config/templates/tweek.yaml.template +20 -0
- tweek/config/templates.py +136 -0
- tweek/config/tiers.yaml +5 -4
- tweek/diagnostics.py +112 -32
- tweek/hooks/overrides.py +4 -0
- tweek/hooks/post_tool_use.py +46 -1
- tweek/hooks/pre_tool_use.py +149 -49
- tweek/integrations/openclaw.py +84 -0
- tweek/licensing.py +1 -1
- tweek/mcp/__init__.py +7 -9
- tweek/mcp/clients/chatgpt.py +2 -2
- tweek/mcp/clients/claude_desktop.py +2 -2
- tweek/mcp/clients/gemini.py +2 -2
- tweek/mcp/proxy.py +165 -1
- tweek/memory/provenance.py +438 -0
- tweek/memory/queries.py +2 -0
- tweek/memory/safety.py +23 -4
- tweek/memory/schemas.py +1 -0
- tweek/memory/store.py +101 -71
- tweek/plugins/screening/heuristic_scorer.py +1 -1
- tweek/security/integrity.py +77 -0
- tweek/security/llm_reviewer.py +170 -74
- tweek/security/local_reviewer.py +44 -2
- tweek/security/model_registry.py +73 -7
- tweek/skill_template/overrides-reference.md +1 -1
- tweek/skills/context.py +221 -0
- tweek/skills/scanner.py +2 -2
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
- tweek/mcp/server.py +0 -320
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/top_level.txt +0 -0
tweek/memory/schemas.py
CHANGED
|
@@ -39,6 +39,7 @@ class ConfidenceAdjustment:
|
|
|
39
39
|
last_decision: Optional[str]
|
|
40
40
|
adjusted_decision: Optional[str] = None # suggested decision override
|
|
41
41
|
confidence_score: float = 0.0 # 0.0-1.0 how confident the suggestion is
|
|
42
|
+
scope: Optional[str] = None # which scope matched: exact/tool_project/path
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
@dataclass
|
tweek/memory/store.py
CHANGED
|
@@ -27,6 +27,7 @@ from tweek.memory.safety import (
|
|
|
27
27
|
MIN_APPROVAL_RATIO,
|
|
28
28
|
MIN_CONFIDENCE_SCORE,
|
|
29
29
|
MIN_DECISION_THRESHOLD,
|
|
30
|
+
SCOPED_THRESHOLDS,
|
|
30
31
|
compute_suggested_decision,
|
|
31
32
|
is_immune_pattern,
|
|
32
33
|
)
|
|
@@ -269,11 +270,19 @@ class MemoryStore:
|
|
|
269
270
|
current_decision: str = "ask",
|
|
270
271
|
original_severity: str = "medium",
|
|
271
272
|
original_confidence: str = "heuristic",
|
|
273
|
+
tool_name: Optional[str] = None,
|
|
274
|
+
project_hash: Optional[str] = None,
|
|
272
275
|
) -> Optional[ConfidenceAdjustment]:
|
|
273
276
|
"""Query memory for a confidence adjustment on a pattern.
|
|
274
277
|
|
|
275
|
-
|
|
276
|
-
|
|
278
|
+
Uses a narrowest-first scope cascade:
|
|
279
|
+
1. exact: pattern + tool + path + project (threshold: 1)
|
|
280
|
+
2. tool_project: pattern + tool + project (threshold: 3)
|
|
281
|
+
3. path: pattern + path_prefix (threshold: 5)
|
|
282
|
+
4. global: NEVER — intentionally omitted
|
|
283
|
+
|
|
284
|
+
Returns a ConfidenceAdjustment if memory has enough data at any
|
|
285
|
+
scope, or None if insufficient data / pattern is immune.
|
|
277
286
|
"""
|
|
278
287
|
conn = self._get_connection()
|
|
279
288
|
|
|
@@ -286,96 +295,117 @@ class MemoryStore:
|
|
|
286
295
|
)
|
|
287
296
|
return None
|
|
288
297
|
|
|
289
|
-
#
|
|
298
|
+
# Build scope cascade: (scope_name, sql_where, params, threshold)
|
|
299
|
+
scopes = []
|
|
300
|
+
|
|
301
|
+
if tool_name and path_prefix and project_hash:
|
|
302
|
+
scopes.append((
|
|
303
|
+
"exact",
|
|
304
|
+
"pattern_name = ? AND tool_name = ? AND path_prefix = ? AND project_hash = ?",
|
|
305
|
+
(pattern_name, tool_name, path_prefix, project_hash),
|
|
306
|
+
SCOPED_THRESHOLDS["exact"],
|
|
307
|
+
))
|
|
308
|
+
|
|
309
|
+
if tool_name and project_hash:
|
|
310
|
+
scopes.append((
|
|
311
|
+
"tool_project",
|
|
312
|
+
"pattern_name = ? AND tool_name = ? AND project_hash = ?",
|
|
313
|
+
(pattern_name, tool_name, project_hash),
|
|
314
|
+
SCOPED_THRESHOLDS["tool_project"],
|
|
315
|
+
))
|
|
316
|
+
|
|
290
317
|
if path_prefix:
|
|
291
|
-
|
|
292
|
-
""
|
|
293
|
-
|
|
294
|
-
WHERE pattern_name = ? AND path_prefix = ?
|
|
295
|
-
""",
|
|
318
|
+
scopes.append((
|
|
319
|
+
"path",
|
|
320
|
+
"pattern_name = ? AND path_prefix = ?",
|
|
296
321
|
(pattern_name, path_prefix),
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
SELECT * FROM pattern_confidence_view
|
|
302
|
-
WHERE pattern_name = ? AND path_prefix IS NULL
|
|
303
|
-
""",
|
|
304
|
-
(pattern_name,),
|
|
305
|
-
).fetchone()
|
|
322
|
+
SCOPED_THRESHOLDS["path"],
|
|
323
|
+
))
|
|
324
|
+
|
|
325
|
+
# No global fallback — intentionally omitted
|
|
306
326
|
|
|
307
|
-
#
|
|
308
|
-
|
|
327
|
+
# Try each scope narrowest-first
|
|
328
|
+
for scope_name, where_clause, params, threshold in scopes:
|
|
309
329
|
row = conn.execute(
|
|
310
|
-
"""
|
|
330
|
+
f"""
|
|
311
331
|
SELECT
|
|
312
332
|
pattern_name,
|
|
313
|
-
|
|
314
|
-
SUM(
|
|
315
|
-
|
|
316
|
-
SUM(
|
|
317
|
-
|
|
318
|
-
|
|
333
|
+
COUNT(*) as total_decisions,
|
|
334
|
+
SUM(CASE WHEN user_response = 'approved' THEN decay_weight ELSE 0 END)
|
|
335
|
+
as weighted_approvals,
|
|
336
|
+
SUM(CASE WHEN user_response = 'denied' THEN decay_weight ELSE 0 END)
|
|
337
|
+
as weighted_denials,
|
|
338
|
+
CASE WHEN SUM(decay_weight) > 0 THEN
|
|
339
|
+
SUM(CASE WHEN user_response = 'approved' THEN decay_weight ELSE 0 END)
|
|
340
|
+
/ SUM(decay_weight)
|
|
319
341
|
ELSE 0.5 END as approval_ratio,
|
|
320
|
-
MAX(
|
|
321
|
-
FROM
|
|
322
|
-
WHERE
|
|
342
|
+
MAX(timestamp) as last_decision
|
|
343
|
+
FROM pattern_decisions
|
|
344
|
+
WHERE {where_clause} AND decay_weight > 0.01
|
|
323
345
|
GROUP BY pattern_name
|
|
324
346
|
""",
|
|
325
|
-
|
|
347
|
+
params,
|
|
326
348
|
).fetchone()
|
|
327
349
|
|
|
328
|
-
|
|
350
|
+
if not row:
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
total = row["total_decisions"]
|
|
354
|
+
weighted_approvals = row["weighted_approvals"] or 0.0
|
|
355
|
+
weighted_denials = row["weighted_denials"] or 0.0
|
|
356
|
+
approval_ratio = row["approval_ratio"] or 0.5
|
|
357
|
+
total_weighted = weighted_approvals + weighted_denials
|
|
358
|
+
|
|
359
|
+
# Check if this scope has enough data
|
|
360
|
+
if total_weighted < threshold:
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
# Compute suggested decision with scope-specific threshold
|
|
364
|
+
suggested = compute_suggested_decision(
|
|
365
|
+
current_decision=current_decision,
|
|
366
|
+
approval_ratio=approval_ratio,
|
|
367
|
+
total_weighted_decisions=total_weighted,
|
|
368
|
+
original_severity=original_severity,
|
|
369
|
+
original_confidence=original_confidence,
|
|
370
|
+
min_threshold=threshold,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Confidence score: based on data quantity and consistency
|
|
374
|
+
confidence_score = 0.0
|
|
375
|
+
if total_weighted >= threshold:
|
|
376
|
+
data_factor = min(total_weighted / (threshold * 3), 1.0)
|
|
377
|
+
ratio_factor = approval_ratio if suggested == "log" else (1 - approval_ratio)
|
|
378
|
+
confidence_score = data_factor * ratio_factor
|
|
379
|
+
|
|
380
|
+
adjustment = ConfidenceAdjustment(
|
|
381
|
+
pattern_name=pattern_name,
|
|
382
|
+
path_prefix=path_prefix,
|
|
383
|
+
total_decisions=total,
|
|
384
|
+
weighted_approvals=weighted_approvals,
|
|
385
|
+
weighted_denials=weighted_denials,
|
|
386
|
+
approval_ratio=approval_ratio,
|
|
387
|
+
last_decision=row["last_decision"],
|
|
388
|
+
adjusted_decision=suggested,
|
|
389
|
+
confidence_score=confidence_score,
|
|
390
|
+
scope=scope_name,
|
|
391
|
+
)
|
|
392
|
+
|
|
329
393
|
self._audit(
|
|
330
394
|
"read", "pattern_decisions",
|
|
331
395
|
f"{pattern_name}:{path_prefix}",
|
|
332
|
-
"
|
|
396
|
+
f"scope={scope_name}, total={total}, ratio={approval_ratio:.2f}, "
|
|
397
|
+
f"suggested={suggested}, confidence={confidence_score:.2f}",
|
|
333
398
|
)
|
|
334
|
-
return None
|
|
335
399
|
|
|
336
|
-
|
|
337
|
-
weighted_approvals = row["weighted_approvals"] or 0.0
|
|
338
|
-
weighted_denials = row["weighted_denials"] or 0.0
|
|
339
|
-
approval_ratio = row["approval_ratio"] or 0.5
|
|
340
|
-
total_weighted = weighted_approvals + weighted_denials
|
|
341
|
-
|
|
342
|
-
# Compute suggested decision
|
|
343
|
-
suggested = compute_suggested_decision(
|
|
344
|
-
current_decision=current_decision,
|
|
345
|
-
approval_ratio=approval_ratio,
|
|
346
|
-
total_weighted_decisions=total_weighted,
|
|
347
|
-
original_severity=original_severity,
|
|
348
|
-
original_confidence=original_confidence,
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
# Confidence score: based on data quantity and consistency
|
|
352
|
-
confidence_score = 0.0
|
|
353
|
-
if total_weighted >= MIN_DECISION_THRESHOLD:
|
|
354
|
-
# Scale 0-1 based on how far above threshold and ratio strength
|
|
355
|
-
data_factor = min(total_weighted / (MIN_DECISION_THRESHOLD * 3), 1.0)
|
|
356
|
-
ratio_factor = approval_ratio if suggested == "log" else (1 - approval_ratio)
|
|
357
|
-
confidence_score = data_factor * ratio_factor
|
|
358
|
-
|
|
359
|
-
adjustment = ConfidenceAdjustment(
|
|
360
|
-
pattern_name=pattern_name,
|
|
361
|
-
path_prefix=path_prefix,
|
|
362
|
-
total_decisions=total,
|
|
363
|
-
weighted_approvals=weighted_approvals,
|
|
364
|
-
weighted_denials=weighted_denials,
|
|
365
|
-
approval_ratio=approval_ratio,
|
|
366
|
-
last_decision=row["last_decision"],
|
|
367
|
-
adjusted_decision=suggested,
|
|
368
|
-
confidence_score=confidence_score,
|
|
369
|
-
)
|
|
400
|
+
return adjustment
|
|
370
401
|
|
|
402
|
+
# No scope had enough data
|
|
371
403
|
self._audit(
|
|
372
404
|
"read", "pattern_decisions",
|
|
373
405
|
f"{pattern_name}:{path_prefix}",
|
|
374
|
-
|
|
375
|
-
f"confidence={confidence_score:.2f}",
|
|
406
|
+
"no_data_any_scope",
|
|
376
407
|
)
|
|
377
|
-
|
|
378
|
-
return adjustment
|
|
408
|
+
return None
|
|
379
409
|
|
|
380
410
|
# =====================================================================
|
|
381
411
|
# Source Trust
|
|
@@ -3,7 +3,7 @@ Tweek Heuristic Scorer Screening Plugin
|
|
|
3
3
|
|
|
4
4
|
Lightweight signal-based scoring for confidence-gated LLM escalation.
|
|
5
5
|
Runs between Layer 2 (regex) and Layer 3 (LLM) to detect novel attack
|
|
6
|
-
variants that don't match any of the
|
|
6
|
+
variants that don't match any of the 262 regex patterns but exhibit
|
|
7
7
|
suspicious characteristics.
|
|
8
8
|
|
|
9
9
|
Scoring signals (all local, no network, no LLM):
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tweek Source File Integrity — Self-Trust for Own Package Files
|
|
4
|
+
|
|
5
|
+
Prevents false-positive security warnings when Tweek's hooks screen
|
|
6
|
+
Tweek's own source code (which naturally contains patterns like
|
|
7
|
+
"prompt injection", ".env", "bypass hooks", etc.).
|
|
8
|
+
|
|
9
|
+
Security model:
|
|
10
|
+
- Package-relative: only files physically inside the installed
|
|
11
|
+
tweek Python package are trusted.
|
|
12
|
+
- Resolved paths: symlinks and ".." traversal are resolved before
|
|
13
|
+
comparison, so an attacker cannot trick the check with crafted paths.
|
|
14
|
+
- Read-only trust: this only skips *screening* of file content that
|
|
15
|
+
Claude reads. It does NOT allow execution, writing, or any other
|
|
16
|
+
privileged action.
|
|
17
|
+
|
|
18
|
+
What IS trusted:
|
|
19
|
+
- Python source (.py), YAML configs (.yaml/.yml), and Markdown (.md)
|
|
20
|
+
files shipped inside the tweek package directory.
|
|
21
|
+
|
|
22
|
+
What is NOT trusted:
|
|
23
|
+
- User config files (~/.tweek/*)
|
|
24
|
+
- Downloaded model files (~/.tweek/models/*)
|
|
25
|
+
- Any file outside the package directory, even if named similarly
|
|
26
|
+
- Non-allowlisted file extensions (e.g., .onnx, .bin, .pkl)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
# Resolve the tweek package root at import time.
|
|
32
|
+
# This file lives at tweek/security/integrity.py, so .parent.parent = tweek/
|
|
33
|
+
_TWEEK_PACKAGE_ROOT: Path = Path(__file__).resolve().parent.parent
|
|
34
|
+
|
|
35
|
+
# Only trust files with these extensions — never trust binary/model files
|
|
36
|
+
_TRUSTED_EXTENSIONS: frozenset = frozenset({
|
|
37
|
+
".py", ".yaml", ".yml", ".md", ".txt", ".json",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def is_trusted_tweek_file(file_path: str) -> bool:
|
|
42
|
+
"""Check whether a file is a verified Tweek package source file.
|
|
43
|
+
|
|
44
|
+
A file is trusted if and only if:
|
|
45
|
+
1. Its fully-resolved path is inside the tweek package directory.
|
|
46
|
+
2. It has an allowlisted extension (source/config only, no binaries).
|
|
47
|
+
3. The file actually exists on disk (prevents speculative path trust).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
file_path: Absolute or relative path to check.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
True if the file is a Tweek source file that should skip screening.
|
|
54
|
+
"""
|
|
55
|
+
if not file_path:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
resolved = Path(file_path).resolve()
|
|
60
|
+
|
|
61
|
+
# Must exist — don't trust hypothetical paths
|
|
62
|
+
if not resolved.is_file():
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
# Must have a safe extension
|
|
66
|
+
if resolved.suffix.lower() not in _TRUSTED_EXTENSIONS:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
# Must be inside the tweek package directory
|
|
70
|
+
# Uses is_relative_to (Python 3.9+) for safe containment check
|
|
71
|
+
if not resolved.is_relative_to(_TWEEK_PACKAGE_ROOT):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
except (OSError, ValueError, TypeError):
|
|
77
|
+
return False
|