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.
Files changed (61) hide show
  1. tweek/__init__.py +2 -2
  2. tweek/audit.py +2 -2
  3. tweek/cli.py +78 -6605
  4. tweek/cli_config.py +643 -0
  5. tweek/cli_configure.py +413 -0
  6. tweek/cli_core.py +718 -0
  7. tweek/cli_dry_run.py +390 -0
  8. tweek/cli_helpers.py +316 -0
  9. tweek/cli_install.py +1666 -0
  10. tweek/cli_logs.py +301 -0
  11. tweek/cli_mcp.py +148 -0
  12. tweek/cli_memory.py +343 -0
  13. tweek/cli_plugins.py +748 -0
  14. tweek/cli_protect.py +564 -0
  15. tweek/cli_proxy.py +405 -0
  16. tweek/cli_security.py +236 -0
  17. tweek/cli_skills.py +289 -0
  18. tweek/cli_uninstall.py +551 -0
  19. tweek/cli_vault.py +313 -0
  20. tweek/config/allowed_dirs.yaml +16 -17
  21. tweek/config/families.yaml +4 -1
  22. tweek/config/manager.py +17 -0
  23. tweek/config/patterns.yaml +29 -5
  24. tweek/config/templates/config.yaml.template +212 -0
  25. tweek/config/templates/env.template +45 -0
  26. tweek/config/templates/overrides.yaml.template +121 -0
  27. tweek/config/templates/tweek.yaml.template +20 -0
  28. tweek/config/templates.py +136 -0
  29. tweek/config/tiers.yaml +5 -4
  30. tweek/diagnostics.py +112 -32
  31. tweek/hooks/overrides.py +4 -0
  32. tweek/hooks/post_tool_use.py +46 -1
  33. tweek/hooks/pre_tool_use.py +149 -49
  34. tweek/integrations/openclaw.py +84 -0
  35. tweek/licensing.py +1 -1
  36. tweek/mcp/__init__.py +7 -9
  37. tweek/mcp/clients/chatgpt.py +2 -2
  38. tweek/mcp/clients/claude_desktop.py +2 -2
  39. tweek/mcp/clients/gemini.py +2 -2
  40. tweek/mcp/proxy.py +165 -1
  41. tweek/memory/provenance.py +438 -0
  42. tweek/memory/queries.py +2 -0
  43. tweek/memory/safety.py +23 -4
  44. tweek/memory/schemas.py +1 -0
  45. tweek/memory/store.py +101 -71
  46. tweek/plugins/screening/heuristic_scorer.py +1 -1
  47. tweek/security/integrity.py +77 -0
  48. tweek/security/llm_reviewer.py +170 -74
  49. tweek/security/local_reviewer.py +44 -2
  50. tweek/security/model_registry.py +73 -7
  51. tweek/skill_template/overrides-reference.md +1 -1
  52. tweek/skills/context.py +221 -0
  53. tweek/skills/scanner.py +2 -2
  54. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/METADATA +8 -7
  55. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/RECORD +60 -38
  56. tweek/mcp/server.py +0 -320
  57. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/WHEEL +0 -0
  58. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/entry_points.txt +0 -0
  59. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/LICENSE +0 -0
  60. {tweek-0.3.1.dist-info → tweek-0.4.1.dist-info}/licenses/NOTICE +0 -0
  61. {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
- Returns a ConfidenceAdjustment if memory has enough data,
276
- or None if insufficient data / pattern is immune.
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
- # Query the confidence view
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
- row = conn.execute(
292
- """
293
- SELECT * FROM pattern_confidence_view
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
- ).fetchone()
298
- else:
299
- row = conn.execute(
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
- # Also try without path prefix as fallback
308
- if not row and path_prefix:
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
- NULL as path_prefix,
314
- SUM(total_decisions) as total_decisions,
315
- SUM(weighted_approvals) as weighted_approvals,
316
- SUM(weighted_denials) as weighted_denials,
317
- CASE WHEN SUM(weighted_approvals) + SUM(weighted_denials) > 0 THEN
318
- SUM(weighted_approvals) / (SUM(weighted_approvals) + SUM(weighted_denials))
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(last_decision) as last_decision
321
- FROM pattern_confidence_view
322
- WHERE pattern_name = ?
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
- (pattern_name,),
347
+ params,
326
348
  ).fetchone()
327
349
 
328
- if not row:
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
- "no_data",
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
- total = row["total_decisions"]
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
- f"total={total}, ratio={approval_ratio:.2f}, suggested={suggested}, "
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 259 regex patterns but exhibit
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