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
tweek/proxy/__init__.py CHANGED
@@ -16,6 +16,7 @@ Usage:
16
16
  The proxy is DISABLED by default. Enable with:
17
17
  tweek proxy enable
18
18
  """
19
+ from __future__ import annotations
19
20
 
20
21
  import shutil
21
22
  import socket
@@ -34,7 +35,7 @@ except ImportError:
34
35
 
35
36
 
36
37
  # Default ports
37
- MOLTBOT_DEFAULT_PORT = 18789
38
+ OPENCLAW_DEFAULT_PORT = 18789
38
39
  TWEEK_DEFAULT_PORT = 9877
39
40
 
40
41
 
@@ -58,8 +59,8 @@ def is_port_in_use(port: int, host: str = "127.0.0.1") -> bool:
58
59
  return False
59
60
 
60
61
 
61
- def check_moltbot_gateway_running(port: int = MOLTBOT_DEFAULT_PORT) -> bool:
62
- """Check if moltbot's gateway is actively listening on its port."""
62
+ def check_openclaw_gateway_running(port: int = OPENCLAW_DEFAULT_PORT) -> bool:
63
+ """Check if OpenClaw's gateway is actively listening on its port."""
63
64
  return is_port_in_use(port)
64
65
 
65
66
 
@@ -71,19 +72,19 @@ def detect_proxy_conflicts() -> list[ProxyConflict]:
71
72
  """
72
73
  conflicts = []
73
74
 
74
- # Check for moltbot
75
- moltbot_info = detect_moltbot()
76
- if moltbot_info:
77
- moltbot_port = moltbot_info.get("gateway_port", MOLTBOT_DEFAULT_PORT)
78
- is_running = check_moltbot_gateway_running(moltbot_port)
75
+ # Check for OpenClaw
76
+ openclaw_info = detect_openclaw()
77
+ if openclaw_info:
78
+ openclaw_port = openclaw_info.get("gateway_port", OPENCLAW_DEFAULT_PORT)
79
+ is_running = check_openclaw_gateway_running(openclaw_port)
79
80
 
80
- if moltbot_info.get("process_running") or is_running:
81
+ if openclaw_info.get("process_running") or is_running:
81
82
  conflicts.append(ProxyConflict(
82
- tool_name="moltbot",
83
- port=moltbot_port,
83
+ tool_name="openclaw",
84
+ port=openclaw_port,
84
85
  is_running=is_running,
85
- description="Moltbot gateway detected" +
86
- (f" on port {moltbot_port}" if is_running else " (process found)")
86
+ description="OpenClaw gateway detected" +
87
+ (f" on port {openclaw_port}" if is_running else " (process found)")
87
88
  ))
88
89
 
89
90
  # Check if something else is using Tweek's default port
@@ -98,31 +99,31 @@ def detect_proxy_conflicts() -> list[ProxyConflict]:
98
99
  return conflicts
99
100
 
100
101
 
101
- def get_moltbot_status() -> dict:
102
+ def get_openclaw_status() -> dict:
102
103
  """
103
- Get detailed moltbot status including whether its gateway is running.
104
+ Get detailed OpenClaw status including whether its gateway is running.
104
105
 
105
106
  Returns:
106
107
  Dict with keys: installed, running, gateway_active, port, config_path
107
108
  """
108
109
  from pathlib import Path
109
110
 
110
- moltbot_info = detect_moltbot()
111
+ openclaw_info = detect_openclaw()
111
112
 
112
113
  status = {
113
- "installed": moltbot_info is not None,
114
+ "installed": openclaw_info is not None,
114
115
  "running": False,
115
116
  "gateway_active": False,
116
- "port": MOLTBOT_DEFAULT_PORT,
117
+ "port": OPENCLAW_DEFAULT_PORT,
117
118
  "config_path": None,
118
119
  }
119
120
 
120
- if moltbot_info:
121
- status["running"] = moltbot_info.get("process_running", False)
122
- status["port"] = moltbot_info.get("gateway_port", MOLTBOT_DEFAULT_PORT)
123
- status["gateway_active"] = check_moltbot_gateway_running(status["port"])
121
+ if openclaw_info:
122
+ status["running"] = openclaw_info.get("process_running", False)
123
+ status["port"] = openclaw_info.get("gateway_port", OPENCLAW_DEFAULT_PORT)
124
+ status["gateway_active"] = check_openclaw_gateway_running(status["port"])
124
125
 
125
- config_path = Path.home() / ".moltbot"
126
+ config_path = Path.home() / ".openclaw"
126
127
  if config_path.exists():
127
128
  status["config_path"] = str(config_path)
128
129
 
@@ -130,8 +131,8 @@ def get_moltbot_status() -> dict:
130
131
 
131
132
 
132
133
  # Detection functions for supported tools
133
- def detect_moltbot() -> Optional[dict]:
134
- """Detect if moltbot is installed on the system."""
134
+ def detect_openclaw() -> Optional[dict]:
135
+ """Detect if OpenClaw is installed on the system."""
135
136
  import subprocess
136
137
  import json
137
138
  from pathlib import Path
@@ -146,22 +147,22 @@ def detect_moltbot() -> Optional[dict]:
146
147
  # Check for npm global installation
147
148
  try:
148
149
  result = subprocess.run(
149
- ["npm", "list", "-g", "moltbot", "--json"],
150
+ ["npm", "list", "-g", "openclaw", "--json"],
150
151
  capture_output=True,
151
152
  text=True,
152
153
  timeout=5
153
154
  )
154
155
  if result.returncode == 0:
155
156
  data = json.loads(result.stdout)
156
- if "dependencies" in data and "moltbot" in data.get("dependencies", {}):
157
+ if "dependencies" in data and "openclaw" in data.get("dependencies", {}):
157
158
  indicators["npm_global"] = True
158
159
  except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
159
160
  pass
160
161
 
161
- # Check for running moltbot process
162
+ # Check for running openclaw process
162
163
  try:
163
164
  result = subprocess.run(
164
- ["pgrep", "-f", "moltbot"],
165
+ ["pgrep", "-f", "openclaw"],
165
166
  capture_output=True,
166
167
  timeout=5
167
168
  )
@@ -170,9 +171,9 @@ def detect_moltbot() -> Optional[dict]:
170
171
  except (subprocess.TimeoutExpired, FileNotFoundError):
171
172
  pass
172
173
 
173
- # Check for moltbot config directory
174
- moltbot_config = Path.home() / ".moltbot"
175
- if moltbot_config.exists():
174
+ # Check for OpenClaw config directory
175
+ openclaw_config = Path.home() / ".openclaw"
176
+ if openclaw_config.exists():
176
177
  indicators["config_exists"] = True
177
178
 
178
179
  # Default gateway port
@@ -231,7 +232,7 @@ def detect_continue() -> Optional[dict]:
231
232
  def detect_supported_tools() -> dict:
232
233
  """Detect all supported LLM tools on the system."""
233
234
  return {
234
- "moltbot": detect_moltbot(),
235
+ "openclaw": detect_openclaw(),
235
236
  "cursor": detect_cursor(),
236
237
  "continue": detect_continue(),
237
238
  }
@@ -287,14 +288,14 @@ def get_proxy_status() -> dict:
287
288
  __all__ = [
288
289
  "PROXY_AVAILABLE",
289
290
  "PROXY_MISSING_DEPS",
290
- "MOLTBOT_DEFAULT_PORT",
291
+ "OPENCLAW_DEFAULT_PORT",
291
292
  "TWEEK_DEFAULT_PORT",
292
293
  "ProxyConflict",
293
294
  "is_port_in_use",
294
- "check_moltbot_gateway_running",
295
+ "check_openclaw_gateway_running",
295
296
  "detect_proxy_conflicts",
296
- "get_moltbot_status",
297
- "detect_moltbot",
297
+ "get_openclaw_status",
298
+ "detect_openclaw",
298
299
  "detect_cursor",
299
300
  "detect_continue",
300
301
  "detect_supported_tools",
tweek/proxy/addon.py CHANGED
@@ -95,7 +95,6 @@ class TweekProxyAddon:
95
95
  result = self.interceptor.screen_request(flow.request.content, provider)
96
96
 
97
97
  if result.warnings:
98
- # Log warnings but don't block requests
99
98
  logger.warning(
100
99
  f"Prompt injection warning: {result.warnings} "
101
100
  f"(provider={provider.value}, path={flow.request.path})"
@@ -104,6 +103,21 @@ class TweekProxyAddon:
104
103
  # Add header to track warning
105
104
  flow.request.headers["X-Tweek-Warning"] = "prompt-injection-suspected"
106
105
 
106
+ # Block request if screening says it's not allowed
107
+ if not result.allowed and self.block_mode and not self.log_only:
108
+ self.stats["requests_blocked"] += 1
109
+ flow.response = http.Response.make(
110
+ 403,
111
+ json.dumps({
112
+ "error": {
113
+ "type": "security_blocked",
114
+ "message": f"Tweek Security: Request blocked — {result.reason}",
115
+ "patterns": result.matched_patterns,
116
+ }
117
+ }),
118
+ {"Content-Type": "application/json"},
119
+ )
120
+
107
121
  @concurrent
108
122
  def response(self, flow: http.HTTPFlow):
109
123
  """Handle incoming responses from LLM APIs."""
@@ -112,10 +126,15 @@ class TweekProxyAddon:
112
126
  if not self.interceptor.should_intercept(host):
113
127
  return
114
128
 
115
- # Skip streaming responses - we can't buffer them without breaking UX
129
+ # Log streaming responses - we can't fully buffer SSE without breaking UX
130
+ # but we flag them as unscreened for visibility
116
131
  content_type = flow.response.headers.get("content-type", "")
117
132
  if "text/event-stream" in content_type:
118
- logger.debug(f"Skipping streaming response from {host}")
133
+ logger.warning(
134
+ f"Streaming response from {host} cannot be fully screened. "
135
+ "Tool calls in streaming responses bypass proxy screening."
136
+ )
137
+ self.stats["streaming_unscreened"] = self.stats.get("streaming_unscreened", 0) + 1
119
138
  return
120
139
 
121
140
  self.stats["responses_screened"] += 1
@@ -4,6 +4,7 @@ LLM API Interceptor - Screens requests and responses to LLM APIs.
4
4
  This module provides the core interception logic for the Tweek proxy,
5
5
  analyzing LLM API traffic for security threats.
6
6
  """
7
+ from __future__ import annotations
7
8
 
8
9
  import json
9
10
  import re
tweek/proxy/server.py CHANGED
@@ -267,8 +267,10 @@ def get_proxy_env_vars(port: int = DEFAULT_PORT) -> dict[str, str]:
267
267
  "HTTPS_PROXY": proxy_url,
268
268
  "http_proxy": proxy_url,
269
269
  "https_proxy": proxy_url,
270
- # For Node.js apps that don't respect standard env vars
271
- "NODE_TLS_REJECT_UNAUTHORIZED": "0", # Required for self-signed CA
270
+ # For Node.js: use CA cert bundle instead of disabling TLS validation
271
+ # NODE_TLS_REJECT_UNAUTHORIZED=0 would disable ALL TLS checks (insecure)
272
+ # NODE_EXTRA_CA_CERTS adds our CA to the trust store without disabling validation
273
+ "NODE_EXTRA_CA_CERTS": str(Path.home() / ".tweek" / "proxy" / "tweek-ca.pem"),
272
274
  }
273
275
 
274
276
 
tweek/sandbox/__init__.py CHANGED
@@ -64,8 +64,19 @@ __all__ = [
64
64
  "SANDBOX_AVAILABLE",
65
65
  "SANDBOX_TOOL",
66
66
  "get_sandbox_status",
67
+ # Project sandbox (Layer 2)
68
+ "IsolationLayer",
69
+ "ProjectSandbox",
70
+ "get_project_sandbox",
71
+ "ProjectRegistry",
72
+ "get_registry",
67
73
  ]
68
74
 
75
+ # Project sandbox imports (always available, no platform dependency)
76
+ from .layers import IsolationLayer
77
+ from .project import ProjectSandbox, get_project_sandbox
78
+ from .registry import ProjectRegistry, get_registry
79
+
69
80
  # Add Linux-specific exports if available
70
81
  if IS_LINUX:
71
82
  __all__.extend(["LinuxSandbox", "prompt_install_firejail", "get_sandbox"])
@@ -0,0 +1,143 @@
1
+ """
2
+ Tweek Docker Bridge
3
+
4
+ Lightweight integration with Docker for container-level isolation.
5
+ Generates docker-compose.yaml and provides simple run/status commands.
6
+ Delegates actual container management to Docker rather than reimplementing.
7
+ """
8
+
9
+ import subprocess
10
+ import textwrap
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+
15
+ TWEEK_HOME = Path.home() / ".tweek"
16
+
17
+
18
+ class DockerBridge:
19
+ """Lightweight Docker integration for project-level container isolation."""
20
+
21
+ def is_docker_available(self) -> bool:
22
+ """Check if Docker is installed and the daemon is running."""
23
+ try:
24
+ result = subprocess.run(
25
+ ["docker", "info"],
26
+ capture_output=True,
27
+ text=True,
28
+ timeout=5,
29
+ )
30
+ return result.returncode == 0
31
+ except (FileNotFoundError, subprocess.TimeoutExpired):
32
+ return False
33
+
34
+ def get_docker_version(self) -> Optional[str]:
35
+ """Get Docker version string, or None if unavailable."""
36
+ try:
37
+ result = subprocess.run(
38
+ ["docker", "version", "--format", "{{.Server.Version}}"],
39
+ capture_output=True,
40
+ text=True,
41
+ timeout=5,
42
+ )
43
+ if result.returncode == 0:
44
+ return result.stdout.strip()
45
+ except (FileNotFoundError, subprocess.TimeoutExpired):
46
+ pass
47
+ return None
48
+
49
+ def init(self, project_dir: Path) -> Path:
50
+ """Generate docker-compose.yaml for this project.
51
+
52
+ Creates a minimal Docker Compose configuration that:
53
+ - Mounts the project directory read-write at /workspace
54
+ - Mounts global Tweek config read-only
55
+ - Disables network access by default
56
+ - Sets up environment for Tweek hooks
57
+
58
+ Returns the path to the generated compose file.
59
+ """
60
+ project_dir = project_dir.resolve()
61
+ tweek_dir = project_dir / ".tweek"
62
+ tweek_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ compose_path = tweek_dir / "docker-compose.yaml"
65
+ tweek_home_str = str(TWEEK_HOME)
66
+
67
+ compose_content = textwrap.dedent(f"""\
68
+ # Tweek Docker Sandbox Configuration
69
+ # Generated by: tweek sandbox docker init
70
+ # Docs: https://github.com/your-org/tweek
71
+ #
72
+ # This file mounts your project into a container with Tweek hooks active.
73
+ # Customize as needed for your workflow.
74
+
75
+ services:
76
+ tweek-sandbox:
77
+ image: python:3.12-slim
78
+ working_dir: /workspace
79
+ volumes:
80
+ # Project directory (read-write)
81
+ - {project_dir}:/workspace:rw
82
+ # Global Tweek config (read-only)
83
+ - {tweek_home_str}:/home/tweek/.tweek-global:ro
84
+ network_mode: "none" # No network by default (security)
85
+ environment:
86
+ - TWEEK_SANDBOX_LAYER=2
87
+ - TWEEK_GLOBAL_CONFIG=/home/tweek/.tweek-global
88
+ - HOME=/home/tweek
89
+ # Install tweek and start a shell
90
+ # Customize this command for your workflow
91
+ command: >
92
+ bash -c "
93
+ pip install -q tweek 2>/dev/null;
94
+ echo 'Tweek sandbox ready. Project mounted at /workspace';
95
+ exec bash
96
+ "
97
+ stdin_open: true
98
+ tty: true
99
+ """)
100
+
101
+ compose_path.write_text(compose_content)
102
+ return compose_path
103
+
104
+ def run(self, project_dir: Path) -> int:
105
+ """Launch the project in a Docker container.
106
+
107
+ Returns the container's exit code.
108
+ """
109
+ project_dir = project_dir.resolve()
110
+ compose_path = project_dir / ".tweek" / "docker-compose.yaml"
111
+
112
+ if not compose_path.exists():
113
+ # Auto-generate if not present
114
+ self.init(project_dir)
115
+
116
+ result = subprocess.run(
117
+ ["docker", "compose", "-f", str(compose_path), "run", "--rm", "tweek-sandbox"],
118
+ cwd=str(project_dir),
119
+ )
120
+ return result.returncode
121
+
122
+ def stop(self, project_dir: Path) -> None:
123
+ """Stop any running containers for this project."""
124
+ compose_path = project_dir.resolve() / ".tweek" / "docker-compose.yaml"
125
+ if compose_path.exists():
126
+ subprocess.run(
127
+ ["docker", "compose", "-f", str(compose_path), "down"],
128
+ capture_output=True,
129
+ )
130
+
131
+ def suggest_docker(self, project_dir: Path) -> Optional[str]:
132
+ """Return a suggestion message if Docker is available but not configured."""
133
+ if not self.is_docker_available():
134
+ return None
135
+
136
+ compose = project_dir / ".tweek" / "docker-compose.yaml"
137
+ if compose.exists():
138
+ return None
139
+
140
+ return (
141
+ "Docker detected. For container-level isolation, run:\n"
142
+ " tweek sandbox docker init"
143
+ )
tweek/sandbox/executor.py CHANGED
@@ -14,6 +14,7 @@ Usage:
14
14
  """
15
15
 
16
16
  import os
17
+ import shlex
17
18
  import subprocess
18
19
  import tempfile
19
20
  import json
@@ -166,10 +167,11 @@ class SandboxExecutor:
166
167
  profile_path = self.generator.save(manifest)
167
168
 
168
169
  # Build the sandboxed command
169
- sandboxed_cmd = f'sandbox-exec -f "{profile_path}" /bin/bash -c {self._shell_quote(command)}'
170
+ sandboxed_cmd = f'sandbox-exec -f {shlex.quote(str(profile_path))} /bin/bash -c {self._shell_quote(command)}'
170
171
 
171
- # Set up environment
172
- run_env = os.environ.copy()
172
+ # Set up minimal environment (don't inherit all parent secrets)
173
+ safe_env_keys = {"PATH", "HOME", "USER", "SHELL", "TERM", "LANG", "LC_ALL", "TMPDIR"}
174
+ run_env = {k: v for k, v in os.environ.items() if k in safe_env_keys}
173
175
  if env:
174
176
  run_env.update(env)
175
177
 
@@ -327,10 +329,11 @@ class SandboxExecutor:
327
329
  profile_path = self.generator.save(manifest)
328
330
 
329
331
  # Build sandboxed command
330
- sandboxed_cmd = f'sandbox-exec -f "{profile_path}" /bin/bash -c {self._shell_quote(command)}'
332
+ sandboxed_cmd = f'sandbox-exec -f {shlex.quote(str(profile_path))} /bin/bash -c {self._shell_quote(command)}'
331
333
 
332
- # Set up environment
333
- run_env = os.environ.copy()
334
+ # Set up minimal environment (don't inherit all parent secrets)
335
+ safe_env_keys = {"PATH", "HOME", "USER", "SHELL", "TERM", "LANG", "LC_ALL", "TMPDIR"}
336
+ run_env = {k: v for k, v in os.environ.items() if k in safe_env_keys}
334
337
  if env:
335
338
  run_env.update(env)
336
339
 
@@ -0,0 +1,97 @@
1
+ """
2
+ Tweek Sandbox Layers
3
+
4
+ Defines the isolation layer hierarchy and capability matrix.
5
+
6
+ Layer 0: Bypass - No isolation, global state only (opt-in escape hatch)
7
+ Layer 1: Skills Only - Skill Isolation Chamber (already built)
8
+ Layer 2: Project State - Per-project security DB, overrides, fingerprints, patterns (DEFAULT)
9
+ Layer 3+: Deferred - Filesystem/container isolation via Docker bridge
10
+ """
11
+
12
+ from enum import IntEnum
13
+ from typing import Dict, Set
14
+
15
+
16
+ class IsolationLayer(IntEnum):
17
+ """Isolation layers ordered by increasing security."""
18
+
19
+ BYPASS = 0 # No isolation, global state only
20
+ SKILLS = 1 # Skill Isolation Chamber only
21
+ PROJECT = 2 # Per-project security state (default)
22
+
23
+ @classmethod
24
+ def from_value(cls, value: int) -> "IsolationLayer":
25
+ """Convert int to IsolationLayer, clamping to valid range."""
26
+ try:
27
+ return cls(value)
28
+ except ValueError:
29
+ if value < 0:
30
+ return cls.BYPASS
31
+ return cls.PROJECT
32
+
33
+
34
+ # Capability matrix: what each layer provides
35
+ LAYER_CAPABILITIES: Dict[IsolationLayer, Set[str]] = {
36
+ IsolationLayer.BYPASS: set(),
37
+ IsolationLayer.SKILLS: {
38
+ "skill_scanning",
39
+ "skill_fingerprints",
40
+ "skill_guard",
41
+ },
42
+ IsolationLayer.PROJECT: {
43
+ "skill_scanning",
44
+ "skill_fingerprints",
45
+ "skill_guard",
46
+ "project_security_db",
47
+ "project_overrides",
48
+ "project_fingerprints",
49
+ "project_config",
50
+ },
51
+ }
52
+
53
+
54
+ def layer_has_capability(layer: IsolationLayer, capability: str) -> bool:
55
+ """Check if a layer provides a specific capability."""
56
+ return capability in LAYER_CAPABILITIES.get(layer, set())
57
+
58
+
59
+ def get_layer_description(layer: IsolationLayer) -> str:
60
+ """Human-readable description of what a layer provides."""
61
+ descriptions = {
62
+ IsolationLayer.BYPASS: (
63
+ "No isolation. All security state uses global ~/.tweek/. "
64
+ "Skills are not scanned. Use only for trusted projects."
65
+ ),
66
+ IsolationLayer.SKILLS: (
67
+ "Skill Isolation Chamber active. Skills are scanned before install. "
68
+ "Security state uses global ~/.tweek/."
69
+ ),
70
+ IsolationLayer.PROJECT: (
71
+ "Full project isolation. Security events, overrides, skill fingerprints, "
72
+ "and configuration are scoped to this project's .tweek/ directory. "
73
+ "Project overrides are additive-only (cannot weaken global security)."
74
+ ),
75
+ }
76
+ return descriptions.get(layer, "Unknown layer")
77
+
78
+
79
+ # Severity ranking for additive-only merge
80
+ SEVERITY_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3}
81
+
82
+
83
+ def stricter_severity(a: str, b: str) -> str:
84
+ """Return the stricter (lower threshold) of two severity levels.
85
+
86
+ Severity thresholds define the MINIMUM severity to report:
87
+ - "low" = report low+medium+high+critical (screens the most, strictest)
88
+ - "critical" = report only critical (screens the least, most permissive)
89
+
90
+ Higher rank number = screens more things = stricter.
91
+ """
92
+ rank_a = SEVERITY_ORDER.get(a, 3)
93
+ rank_b = SEVERITY_ORDER.get(b, 3)
94
+ # Higher rank = stricter = screens more things
95
+ if rank_a >= rank_b:
96
+ return a
97
+ return b
tweek/sandbox/linux.py CHANGED
@@ -7,6 +7,7 @@ seccomp-bpf, and capabilities to restrict process execution.
7
7
  If firejail is not available, falls back to bubblewrap (bwrap)
8
8
  which is often installed with Flatpak.
9
9
  """
10
+ from __future__ import annotations
10
11
 
11
12
  import shutil
12
13
  import subprocess