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.
- tweek/__init__.py +2 -2
- tweek/_keygen.py +53 -0
- tweek/audit.py +288 -0
- tweek/cli.py +5303 -2396
- tweek/cli_model.py +380 -0
- tweek/config/families.yaml +609 -0
- tweek/config/manager.py +42 -5
- tweek/config/patterns.yaml +1510 -8
- tweek/config/tiers.yaml +161 -11
- tweek/diagnostics.py +71 -2
- tweek/hooks/break_glass.py +163 -0
- tweek/hooks/feedback.py +223 -0
- tweek/hooks/overrides.py +531 -0
- tweek/hooks/post_tool_use.py +472 -0
- tweek/hooks/pre_tool_use.py +1024 -62
- tweek/integrations/openclaw.py +443 -0
- tweek/integrations/openclaw_server.py +385 -0
- tweek/licensing.py +14 -54
- tweek/logging/bundle.py +2 -2
- tweek/logging/security_log.py +56 -13
- tweek/mcp/approval.py +57 -16
- tweek/mcp/proxy.py +18 -0
- tweek/mcp/screening.py +5 -5
- tweek/mcp/server.py +4 -1
- tweek/memory/__init__.py +24 -0
- tweek/memory/queries.py +223 -0
- tweek/memory/safety.py +140 -0
- tweek/memory/schemas.py +80 -0
- tweek/memory/store.py +989 -0
- tweek/platform/__init__.py +4 -4
- tweek/plugins/__init__.py +40 -24
- tweek/plugins/base.py +1 -1
- tweek/plugins/detectors/__init__.py +3 -3
- tweek/plugins/detectors/{moltbot.py → openclaw.py} +30 -27
- tweek/plugins/git_discovery.py +16 -4
- tweek/plugins/git_registry.py +8 -2
- tweek/plugins/git_security.py +21 -9
- tweek/plugins/screening/__init__.py +10 -1
- tweek/plugins/screening/heuristic_scorer.py +477 -0
- tweek/plugins/screening/llm_reviewer.py +14 -6
- tweek/plugins/screening/local_model_reviewer.py +161 -0
- tweek/proxy/__init__.py +38 -37
- tweek/proxy/addon.py +22 -3
- tweek/proxy/interceptor.py +1 -0
- tweek/proxy/server.py +4 -2
- tweek/sandbox/__init__.py +11 -0
- tweek/sandbox/docker_bridge.py +143 -0
- tweek/sandbox/executor.py +9 -6
- tweek/sandbox/layers.py +97 -0
- tweek/sandbox/linux.py +1 -0
- tweek/sandbox/project.py +548 -0
- tweek/sandbox/registry.py +149 -0
- tweek/security/__init__.py +9 -0
- tweek/security/language.py +250 -0
- tweek/security/llm_reviewer.py +1146 -60
- tweek/security/local_model.py +331 -0
- tweek/security/local_reviewer.py +146 -0
- tweek/security/model_registry.py +371 -0
- tweek/security/rate_limiter.py +11 -6
- tweek/security/secret_scanner.py +70 -4
- tweek/security/session_analyzer.py +26 -2
- tweek/skill_template/SKILL.md +200 -0
- tweek/skill_template/__init__.py +0 -0
- tweek/skill_template/cli-reference.md +331 -0
- tweek/skill_template/overrides-reference.md +184 -0
- tweek/skill_template/scripts/__init__.py +0 -0
- tweek/skill_template/scripts/check_installed.py +170 -0
- tweek/skills/__init__.py +38 -0
- tweek/skills/config.py +150 -0
- tweek/skills/fingerprints.py +198 -0
- tweek/skills/guard.py +293 -0
- tweek/skills/isolation.py +469 -0
- tweek/skills/scanner.py +715 -0
- tweek/vault/__init__.py +0 -1
- tweek/vault/cross_platform.py +12 -1
- tweek/vault/keychain.py +87 -29
- tweek-0.2.0.dist-info/METADATA +281 -0
- tweek-0.2.0.dist-info/RECORD +121 -0
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/entry_points.txt +8 -1
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/licenses/LICENSE +80 -0
- tweek/integrations/moltbot.py +0 -243
- tweek-0.1.0.dist-info/METADATA +0 -335
- tweek-0.1.0.dist-info/RECORD +0 -85
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/WHEEL +0 -0
- {tweek-0.1.0.dist-info → tweek-0.2.0.dist-info}/top_level.txt +0 -0
tweek/platform/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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" #
|
|
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.
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
604
|
-
|
|
607
|
+
# Register built-in plugins
|
|
608
|
+
_register_builtin_plugins(registry)
|
|
605
609
|
|
|
606
|
-
|
|
607
|
-
|
|
610
|
+
# Discover git-installed plugins
|
|
611
|
+
_discover_git_plugins(registry)
|
|
608
612
|
|
|
609
|
-
|
|
610
|
-
|
|
613
|
+
# Discover external plugins via entry_points
|
|
614
|
+
registry.discover_plugins()
|
|
611
615
|
|
|
612
|
-
|
|
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
|
-
|
|
697
|
+
OpenClawDetector,
|
|
692
698
|
CursorDetector,
|
|
693
699
|
ContinueDetector,
|
|
694
700
|
CopilotDetector,
|
|
695
701
|
WindsurfDetector,
|
|
696
702
|
)
|
|
697
|
-
registry.register("
|
|
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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Tweek Tool Detector Plugins
|
|
4
4
|
|
|
5
5
|
Detector plugins identify installed LLM tools and IDEs:
|
|
6
|
-
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
3
|
+
Tweek OpenClaw Detector Plugin
|
|
4
4
|
|
|
5
|
-
Detects
|
|
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
|
|
20
|
+
class OpenClawDetector(ToolDetectorPlugin):
|
|
21
21
|
"""
|
|
22
|
-
|
|
22
|
+
OpenClaw AI personal assistant detector.
|
|
23
23
|
|
|
24
24
|
Detects:
|
|
25
25
|
- npm global installation
|
|
26
|
-
- Running
|
|
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
|
|
32
|
+
DESCRIPTION = "Detect OpenClaw AI personal assistant"
|
|
33
33
|
AUTHOR = "Tweek"
|
|
34
34
|
REQUIRES_LICENSE = "free"
|
|
35
|
-
TAGS = ["detector", "
|
|
35
|
+
TAGS = ["detector", "openclaw", "assistant"]
|
|
36
36
|
|
|
37
|
-
DEFAULT_PORT =
|
|
37
|
+
DEFAULT_PORT = 18789
|
|
38
38
|
CONFIG_LOCATIONS = [
|
|
39
|
-
Path.home() / ".
|
|
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 "
|
|
44
|
+
return "openclaw"
|
|
46
45
|
|
|
47
46
|
def detect(self) -> DetectionResult:
|
|
48
47
|
"""
|
|
49
|
-
Detect
|
|
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
|
|
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", "
|
|
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 "
|
|
109
|
+
if "openclaw" in deps:
|
|
106
110
|
return {
|
|
107
|
-
"version": deps["
|
|
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", "
|
|
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
|
|
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
|
|
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
|
-
|
|
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", "
|
|
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
|
|
164
|
+
# Also check for node process with openclaw
|
|
162
165
|
proc = subprocess.run(
|
|
163
|
-
["pgrep", "-af", "node.*
|
|
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
|
|
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"
|
|
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
|
-
"
|
|
204
|
+
"OpenClaw process is running. Gateway may start and "
|
|
202
205
|
"intercept LLM API calls."
|
|
203
206
|
)
|
|
204
207
|
|
tweek/plugins/git_discovery.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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):
|
tweek/plugins/git_registry.py
CHANGED
|
@@ -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
|
-
|
|
369
|
-
|
|
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)
|
tweek/plugins/git_security.py
CHANGED
|
@@ -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
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
]
|