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
@@ -4,8 +4,10 @@ Platform detection and cross-platform support for Tweek.
4
4
  Tweek supports:
5
5
  - macOS: Full support (Keychain via keyring, sandbox-exec)
6
6
  - Linux: Full support (Secret Service via keyring, firejail optional)
7
- - Windows: Partial support (Credential Locker via keyring, no sandbox)
7
+
8
+ Windows is not supported.
8
9
  """
10
+ from __future__ import annotations
9
11
 
10
12
  import platform
11
13
  import shutil
@@ -92,9 +94,7 @@ def get_vault_backend() -> str:
92
94
  return "macOS Keychain"
93
95
  elif IS_LINUX:
94
96
  return "Secret Service (GNOME Keyring/KWallet)"
95
- elif IS_WINDOWS:
96
- return "Windows Credential Locker"
97
- return "Unknown"
97
+ return "Not supported"
98
98
 
99
99
 
100
100
  def get_capabilities() -> PlatformCapabilities:
tweek/plugins/__init__.py CHANGED
@@ -5,7 +5,7 @@ Tweek Plugin System
5
5
  Modular plugin architecture supporting:
6
6
  - Domain compliance modules (Gov, HIPAA, PCI, Legal)
7
7
  - LLM provider plugins (Anthropic, OpenAI, Google, Bedrock)
8
- - Tool detector plugins (moltbot, Cursor, Continue)
8
+ - Tool detector plugins (openclaw, Cursor, Continue)
9
9
  - Screening method plugins (rate limiting, pattern matching, LLM review)
10
10
 
11
11
  Plugin Discovery:
@@ -43,7 +43,7 @@ class PluginCategory(Enum):
43
43
  """Categories of plugins supported by Tweek."""
44
44
  COMPLIANCE = "tweek.compliance" # Gov, HIPAA, PCI, Legal
45
45
  LLM_PROVIDER = "tweek.llm_providers" # Anthropic, OpenAI, etc.
46
- TOOL_DETECTOR = "tweek.tool_detectors" # moltbot, Cursor, etc.
46
+ TOOL_DETECTOR = "tweek.tool_detectors" # openclaw, Cursor, etc.
47
47
  SCREENING = "tweek.screening" # rate_limiter, pattern_matcher
48
48
 
49
49
 
@@ -248,7 +248,7 @@ class PluginRegistry:
248
248
  f"Git plugin {name} overriding builtin in {category.value}"
249
249
  )
250
250
  else:
251
- logger.warning(f"Plugin {name} already registered in {category.value}")
251
+ logger.debug(f"Plugin {name} already registered in {category.value}")
252
252
  return False
253
253
 
254
254
  # Create plugin info
@@ -551,6 +551,7 @@ class PluginRegistry:
551
551
 
552
552
  # Global registry instance
553
553
  _registry: Optional[PluginRegistry] = None
554
+ _initialized: bool = False
554
555
 
555
556
 
556
557
  def get_registry() -> PluginRegistry:
@@ -578,6 +579,7 @@ def init_plugins(config: Optional[Dict[str, Any]] = None) -> PluginRegistry:
578
579
  3. Entry point plugins (from installed packages)
579
580
 
580
581
  Git plugins with the same name as a builtin will override the builtin.
582
+ Safe to call multiple times — plugin discovery only runs once.
581
583
 
582
584
  Args:
583
585
  config: Optional configuration dictionary
@@ -585,31 +587,35 @@ def init_plugins(config: Optional[Dict[str, Any]] = None) -> PluginRegistry:
585
587
  Returns:
586
588
  The initialized plugin registry
587
589
  """
590
+ global _initialized
588
591
  registry = get_registry()
589
592
 
590
- # Log startup
591
- try:
592
- from tweek.logging.security_log import get_logger as get_sec_logger, SecurityEvent, EventType
593
- get_sec_logger().log(SecurityEvent(
594
- event_type=EventType.STARTUP,
595
- tool_name="plugin_system",
596
- decision="allow",
597
- metadata={"operation": "init_plugins"},
598
- source="plugins",
599
- ))
600
- except Exception:
601
- pass
593
+ if not _initialized:
594
+ # Log startup
595
+ try:
596
+ from tweek.logging.security_log import get_logger as get_sec_logger, SecurityEvent, EventType
597
+ get_sec_logger().log(SecurityEvent(
598
+ event_type=EventType.STARTUP,
599
+ tool_name="plugin_system",
600
+ decision="allow",
601
+ metadata={"operation": "init_plugins"},
602
+ source="plugins",
603
+ ))
604
+ except Exception:
605
+ pass
602
606
 
603
- # Register built-in plugins
604
- _register_builtin_plugins(registry)
607
+ # Register built-in plugins
608
+ _register_builtin_plugins(registry)
605
609
 
606
- # Discover git-installed plugins
607
- _discover_git_plugins(registry)
610
+ # Discover git-installed plugins
611
+ _discover_git_plugins(registry)
608
612
 
609
- # Discover external plugins via entry_points
610
- registry.discover_plugins()
613
+ # Discover external plugins via entry_points
614
+ registry.discover_plugins()
611
615
 
612
- # Load configuration
616
+ _initialized = True
617
+
618
+ # Load configuration (always applied, even on subsequent calls)
613
619
  if config:
614
620
  registry.load_config(config)
615
621
 
@@ -688,13 +694,13 @@ def _register_builtin_plugins(registry: PluginRegistry) -> None:
688
694
  # Detector plugins
689
695
  try:
690
696
  from tweek.plugins.detectors import (
691
- MoltbotDetector,
697
+ OpenClawDetector,
692
698
  CursorDetector,
693
699
  ContinueDetector,
694
700
  CopilotDetector,
695
701
  WindsurfDetector,
696
702
  )
697
- registry.register("moltbot", MoltbotDetector, PluginCategory.TOOL_DETECTOR)
703
+ registry.register("openclaw", OpenClawDetector, PluginCategory.TOOL_DETECTOR)
698
704
  registry.register("cursor", CursorDetector, PluginCategory.TOOL_DETECTOR)
699
705
  registry.register("continue", ContinueDetector, PluginCategory.TOOL_DETECTOR)
700
706
  registry.register("copilot", CopilotDetector, PluginCategory.TOOL_DETECTOR)
@@ -707,16 +713,26 @@ def _register_builtin_plugins(registry: PluginRegistry) -> None:
707
713
  from tweek.plugins.screening import (
708
714
  RateLimiterPlugin,
709
715
  PatternMatcherPlugin,
716
+ HeuristicScorerPlugin,
710
717
  LLMReviewerPlugin,
711
718
  SessionAnalyzerPlugin,
712
719
  )
713
720
  registry.register("rate_limiter", RateLimiterPlugin, PluginCategory.SCREENING)
714
721
  registry.register("pattern_matcher", PatternMatcherPlugin, PluginCategory.SCREENING)
722
+ registry.register("heuristic_scorer", HeuristicScorerPlugin, PluginCategory.SCREENING)
715
723
  registry.register("llm_reviewer", LLMReviewerPlugin, PluginCategory.SCREENING)
716
724
  registry.register("session_analyzer", SessionAnalyzerPlugin, PluginCategory.SCREENING)
717
725
  except ImportError as e:
718
726
  logger.debug(f"Screening plugins not available: {e}")
719
727
 
728
+ # Local model reviewer plugin (optional — requires tweek[local-models])
729
+ try:
730
+ from tweek.plugins.screening.local_model_reviewer import LocalModelReviewerPlugin
731
+ if LocalModelReviewerPlugin is not None:
732
+ registry.register("local_model_reviewer", LocalModelReviewerPlugin, PluginCategory.SCREENING)
733
+ except ImportError:
734
+ pass
735
+
720
736
 
721
737
  def _discover_git_plugins(registry: PluginRegistry) -> int:
722
738
  """
tweek/plugins/base.py CHANGED
@@ -854,7 +854,7 @@ class ToolDetectorPlugin(ABC):
854
854
  @property
855
855
  @abstractmethod
856
856
  def name(self) -> str:
857
- """Tool name (e.g., 'moltbot', 'cursor')."""
857
+ """Tool name (e.g., 'openclaw', 'cursor')."""
858
858
  pass
859
859
 
860
860
  @abstractmethod
@@ -3,7 +3,7 @@
3
3
  Tweek Tool Detector Plugins
4
4
 
5
5
  Detector plugins identify installed LLM tools and IDEs:
6
- - Moltbot: AI coding assistant
6
+ - OpenClaw: AI personal assistant
7
7
  - Cursor: AI-powered IDE
8
8
  - Continue.dev: VS Code AI extension
9
9
  - Copilot: GitHub Copilot
@@ -15,14 +15,14 @@ Detection helps:
15
15
  - Suggest integration options
16
16
  """
17
17
 
18
- from tweek.plugins.detectors.moltbot import MoltbotDetector
18
+ from tweek.plugins.detectors.openclaw import OpenClawDetector
19
19
  from tweek.plugins.detectors.cursor import CursorDetector
20
20
  from tweek.plugins.detectors.continue_dev import ContinueDetector
21
21
  from tweek.plugins.detectors.copilot import CopilotDetector
22
22
  from tweek.plugins.detectors.windsurf import WindsurfDetector
23
23
 
24
24
  __all__ = [
25
- "MoltbotDetector",
25
+ "OpenClawDetector",
26
26
  "CursorDetector",
27
27
  "ContinueDetector",
28
28
  "CopilotDetector",
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Tweek Moltbot Detector Plugin
3
+ Tweek OpenClaw Detector Plugin
4
4
 
5
- Detects Moltbot AI coding assistant:
5
+ Detects OpenClaw AI personal assistant:
6
6
  - Global npm installation
7
7
  - Running process
8
8
  - Gateway configuration
@@ -17,36 +17,35 @@ from typing import Optional, List, Dict, Any
17
17
  from tweek.plugins.base import ToolDetectorPlugin, DetectionResult
18
18
 
19
19
 
20
- class MoltbotDetector(ToolDetectorPlugin):
20
+ class OpenClawDetector(ToolDetectorPlugin):
21
21
  """
22
- Moltbot AI coding assistant detector.
22
+ OpenClaw AI personal assistant detector.
23
23
 
24
24
  Detects:
25
25
  - npm global installation
26
- - Running moltbot process
26
+ - Running openclaw process
27
27
  - Gateway service on default port
28
28
  - Configuration file location
29
29
  """
30
30
 
31
31
  VERSION = "1.0.0"
32
- DESCRIPTION = "Detect Moltbot AI coding assistant"
32
+ DESCRIPTION = "Detect OpenClaw AI personal assistant"
33
33
  AUTHOR = "Tweek"
34
34
  REQUIRES_LICENSE = "free"
35
- TAGS = ["detector", "moltbot", "ide"]
35
+ TAGS = ["detector", "openclaw", "assistant"]
36
36
 
37
- DEFAULT_PORT = 8080
37
+ DEFAULT_PORT = 18789
38
38
  CONFIG_LOCATIONS = [
39
- Path.home() / ".moltbot" / "config.json",
40
- Path.home() / ".config" / "moltbot" / "config.json",
39
+ Path.home() / ".openclaw" / "openclaw.json",
41
40
  ]
42
41
 
43
42
  @property
44
43
  def name(self) -> str:
45
- return "moltbot"
44
+ return "openclaw"
46
45
 
47
46
  def detect(self) -> DetectionResult:
48
47
  """
49
- Detect Moltbot installation and status.
48
+ Detect OpenClaw installation and status.
50
49
  """
51
50
  result = DetectionResult(
52
51
  detected=False,
@@ -74,6 +73,11 @@ class MoltbotDetector(ToolDetectorPlugin):
74
73
  except (json.JSONDecodeError, IOError):
75
74
  result.port = self.DEFAULT_PORT
76
75
 
76
+ # Check for home directory existence
77
+ openclaw_home = Path.home() / ".openclaw"
78
+ if openclaw_home.exists():
79
+ result.detected = True
80
+
77
81
  # Check for running process
78
82
  process_info = self._check_running_process()
79
83
  if process_info:
@@ -90,11 +94,11 @@ class MoltbotDetector(ToolDetectorPlugin):
90
94
  return result
91
95
 
92
96
  def _check_npm_installation(self) -> Optional[Dict[str, str]]:
93
- """Check if moltbot is installed via npm."""
97
+ """Check if openclaw is installed via npm."""
94
98
  try:
95
99
  # Try npm list -g
96
100
  proc = subprocess.run(
97
- ["npm", "list", "-g", "moltbot", "--json"],
101
+ ["npm", "list", "-g", "openclaw", "--json"],
98
102
  capture_output=True,
99
103
  text=True,
100
104
  timeout=10,
@@ -102,9 +106,9 @@ class MoltbotDetector(ToolDetectorPlugin):
102
106
  if proc.returncode == 0:
103
107
  data = json.loads(proc.stdout)
104
108
  deps = data.get("dependencies", {})
105
- if "moltbot" in deps:
109
+ if "openclaw" in deps:
106
110
  return {
107
- "version": deps["moltbot"].get("version", "unknown"),
111
+ "version": deps["openclaw"].get("version", "unknown"),
108
112
  "path": data.get("path", ""),
109
113
  }
110
114
  except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError):
@@ -113,7 +117,7 @@ class MoltbotDetector(ToolDetectorPlugin):
113
117
  # Try which/where
114
118
  try:
115
119
  proc = subprocess.run(
116
- ["which", "moltbot"] if os.name != "nt" else ["where", "moltbot"],
120
+ ["which", "openclaw"] if os.name != "nt" else ["where", "openclaw"],
117
121
  capture_output=True,
118
122
  text=True,
119
123
  timeout=5,
@@ -126,14 +130,14 @@ class MoltbotDetector(ToolDetectorPlugin):
126
130
  return None
127
131
 
128
132
  def _find_config(self) -> Optional[Path]:
129
- """Find moltbot config file."""
133
+ """Find OpenClaw config file."""
130
134
  for path in self.CONFIG_LOCATIONS:
131
135
  if path.exists():
132
136
  return path
133
137
  return None
134
138
 
135
139
  def _check_running_process(self) -> Optional[Dict[str, Any]]:
136
- """Check if moltbot process is running."""
140
+ """Check if openclaw process is running."""
137
141
  try:
138
142
  if os.name == "nt":
139
143
  # Windows
@@ -143,13 +147,12 @@ class MoltbotDetector(ToolDetectorPlugin):
143
147
  text=True,
144
148
  timeout=10,
145
149
  )
146
- # This is a rough check - would need more sophisticated detection
147
- if "moltbot" in proc.stdout.lower():
150
+ if "openclaw" in proc.stdout.lower():
148
151
  return {"running": True}
149
152
  else:
150
153
  # Unix-like
151
154
  proc = subprocess.run(
152
- ["pgrep", "-f", "moltbot"],
155
+ ["pgrep", "-f", "openclaw"],
153
156
  capture_output=True,
154
157
  text=True,
155
158
  timeout=10,
@@ -158,9 +161,9 @@ class MoltbotDetector(ToolDetectorPlugin):
158
161
  pids = proc.stdout.strip().split("\n")
159
162
  return {"pid": pids[0]}
160
163
 
161
- # Also check for node process with moltbot
164
+ # Also check for node process with openclaw
162
165
  proc = subprocess.run(
163
- ["pgrep", "-af", "node.*moltbot"],
166
+ ["pgrep", "-af", "node.*openclaw"],
164
167
  capture_output=True,
165
168
  text=True,
166
169
  timeout=10,
@@ -174,7 +177,7 @@ class MoltbotDetector(ToolDetectorPlugin):
174
177
  return None
175
178
 
176
179
  def _check_gateway_active(self, port: int) -> bool:
177
- """Check if moltbot gateway is listening on port."""
180
+ """Check if OpenClaw gateway is listening on port."""
178
181
  try:
179
182
  import socket
180
183
  sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -193,12 +196,12 @@ class MoltbotDetector(ToolDetectorPlugin):
193
196
  if result.detected:
194
197
  if result.metadata.get("gateway_active"):
195
198
  conflicts.append(
196
- f"Moltbot gateway is active on port {result.port}. "
199
+ f"OpenClaw gateway is active on port {result.port}. "
197
200
  "This may intercept LLM API calls before Tweek."
198
201
  )
199
202
  elif result.running:
200
203
  conflicts.append(
201
- "Moltbot process is running. Gateway may start and "
204
+ "OpenClaw process is running. Gateway may start and "
202
205
  "intercept LLM API calls."
203
206
  )
204
207
 
@@ -242,8 +242,12 @@ def _import_plugin_class(
242
242
 
243
243
  module_name, class_name = entry_point.split(":", 1)
244
244
 
245
- # Construct file path
246
- module_file = plugin_dir / f"{module_name}.py"
245
+ # Construct file path and validate against path traversal
246
+ module_file = (plugin_dir / f"{module_name}.py").resolve()
247
+ if not module_file.is_relative_to(plugin_dir.resolve()):
248
+ raise PluginDiscoveryError(
249
+ f"Entry point module '{module_name}' escapes plugin directory (path traversal blocked)"
250
+ )
247
251
  if not module_file.exists():
248
252
  raise PluginDiscoveryError(
249
253
  f"Entry point module '{module_name}.py' not found in {plugin_dir}"
@@ -266,13 +270,21 @@ def _import_plugin_class(
266
270
  module = importlib.util.module_from_spec(spec)
267
271
 
268
272
  # Add plugin directory to the module's path for relative imports
269
- if plugin_dir not in sys.path:
273
+ # Use try/finally to ensure sys.path is always cleaned up
274
+ path_added = False
275
+ if str(plugin_dir) not in sys.path:
270
276
  sys.path.insert(0, str(plugin_dir))
277
+ path_added = True
271
278
 
272
279
  # Register the module in sys.modules before executing
273
280
  sys.modules[full_module_name] = module
274
281
 
275
- spec.loader.exec_module(module)
282
+ try:
283
+ spec.loader.exec_module(module)
284
+ finally:
285
+ # Always remove plugin dir from sys.path to prevent pollution
286
+ if path_added and str(plugin_dir) in sys.path:
287
+ sys.path.remove(str(plugin_dir))
276
288
 
277
289
  # Get the class
278
290
  if not hasattr(module, class_name):
@@ -365,8 +365,11 @@ class PluginRegistryClient:
365
365
  """Verify the HMAC signature of the registry."""
366
366
  signature = registry.get("registry_signature", "")
367
367
  if not signature:
368
- logger.warning("Registry has no signature - skipping verification")
369
- return True # Allow unsigned registries in development
368
+ if not self._signing_key:
369
+ logger.warning("Registry has no signature and no signing key configured — allowing in dev mode")
370
+ return True
371
+ logger.warning("Registry has no signature but signing key is configured — rejecting")
372
+ return False
370
373
 
371
374
  # Sign the plugins array (the payload)
372
375
  plugins_json = json.dumps(
@@ -375,6 +378,9 @@ class PluginRegistryClient:
375
378
  separators=(",", ":"),
376
379
  ).encode()
377
380
 
381
+ if not self._signing_key:
382
+ logger.warning("Registry has signature but no signing key configured — cannot verify, rejecting")
383
+ return False
378
384
  key = self._signing_key.encode()
379
385
  expected_sig = hmac.new(key, plugins_json, hashlib.sha256).hexdigest()
380
386
  return hmac.compare_digest(expected_sig, signature)
@@ -25,13 +25,10 @@ from typing import Dict, List, Optional, Tuple, Type
25
25
  logger = logging.getLogger(__name__)
26
26
 
27
27
  # Signing key for plugin verification.
28
- # In production, this would use asymmetric keys (Ed25519).
29
- # The HMAC approach is simpler and sufficient for the curated model
30
- # where Tweek controls both signing and verification.
31
- TWEEK_SIGNING_KEY = os.environ.get(
32
- "TWEEK_PLUGIN_SIGNING_KEY",
33
- "tweek-plugin-signing-key-v1"
34
- )
28
+ # SECURITY: No hardcoded fallback. Set TWEEK_PLUGIN_SIGNING_KEY env var
29
+ # for plugin signature verification. Without it, signature checks will
30
+ # fail (which is the correct behavior — fail closed).
31
+ TWEEK_SIGNING_KEY = os.environ.get("TWEEK_PLUGIN_SIGNING_KEY", "")
35
32
 
36
33
  # Modules/functions that are forbidden in plugin code
37
34
  FORBIDDEN_IMPORTS = frozenset({
@@ -57,6 +54,8 @@ FORBIDDEN_IMPORTS = frozenset({
57
54
  "os.spawnvpe",
58
55
  "ctypes",
59
56
  "multiprocessing",
57
+ "importlib",
58
+ "importlib.util",
60
59
  })
61
60
 
62
61
  FORBIDDEN_CALLS = frozenset({
@@ -72,6 +71,9 @@ FORBIDDEN_CALLS = frozenset({
72
71
  "os.removedirs",
73
72
  "shutil.rmtree",
74
73
  "shutil.move",
74
+ "importlib.import_module",
75
+ "importlib.util.spec_from_file_location",
76
+ "getattr",
75
77
  })
76
78
 
77
79
  # Modules that indicate network access
@@ -215,7 +217,11 @@ def verify_checksum_signature(
215
217
  Returns:
216
218
  True if signature is valid
217
219
  """
218
- key = (signing_key or TWEEK_SIGNING_KEY).encode()
220
+ key_str = signing_key or TWEEK_SIGNING_KEY
221
+ if not key_str:
222
+ logger.warning("TWEEK_PLUGIN_SIGNING_KEY not configured — rejecting signature (fail closed)")
223
+ return False
224
+ key = key_str.encode()
219
225
  expected_sig = hmac.new(key, checksums_content, hashlib.sha256).hexdigest()
220
226
  return hmac.compare_digest(expected_sig, signature)
221
227
 
@@ -477,6 +483,12 @@ def sign_checksums(checksums_content: bytes, signing_key: str = None) -> str:
477
483
 
478
484
  Returns:
479
485
  Hex-encoded HMAC signature
486
+
487
+ Raises:
488
+ ValueError: If signing key is empty/not configured
480
489
  """
481
- key = (signing_key or TWEEK_SIGNING_KEY).encode()
490
+ key_str = signing_key or TWEEK_SIGNING_KEY
491
+ if not key_str:
492
+ raise ValueError("Cannot sign checksums: TWEEK_PLUGIN_SIGNING_KEY not configured")
493
+ key = key_str.encode()
482
494
  return hmac.new(key, checksums_content, hashlib.sha256).hexdigest()
@@ -5,22 +5,31 @@ Tweek Screening Plugins
5
5
  Screening plugins provide security analysis methods:
6
6
  - RateLimiter: Detect burst patterns and abuse
7
7
  - PatternMatcher: Regex-based pattern matching
8
+ - HeuristicScorer: Signal-based scoring for confidence-gated LLM escalation
8
9
  - LLMReviewer: Semantic analysis using LLM
9
10
  - SessionAnalyzer: Cross-turn anomaly detection
10
11
 
11
12
  License tiers:
12
- - FREE: PatternMatcher (basic patterns)
13
+ - FREE: PatternMatcher (basic patterns), HeuristicScorer
13
14
  - PRO: RateLimiter, LLMReviewer, SessionAnalyzer
14
15
  """
15
16
 
16
17
  from tweek.plugins.screening.rate_limiter import RateLimiterPlugin
17
18
  from tweek.plugins.screening.pattern_matcher import PatternMatcherPlugin
19
+ from tweek.plugins.screening.heuristic_scorer import HeuristicScorerPlugin
18
20
  from tweek.plugins.screening.llm_reviewer import LLMReviewerPlugin
19
21
  from tweek.plugins.screening.session_analyzer import SessionAnalyzerPlugin
20
22
 
23
+ try:
24
+ from tweek.plugins.screening.local_model_reviewer import LocalModelReviewerPlugin
25
+ except ImportError:
26
+ LocalModelReviewerPlugin = None # type: ignore[assignment,misc]
27
+
21
28
  __all__ = [
22
29
  "RateLimiterPlugin",
23
30
  "PatternMatcherPlugin",
31
+ "HeuristicScorerPlugin",
24
32
  "LLMReviewerPlugin",
33
+ "LocalModelReviewerPlugin",
25
34
  "SessionAnalyzerPlugin",
26
35
  ]