@trac3er/oh-my-god 1.0.2
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.
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +23 -0
- package/.claude-plugin/scripts/install.sh +49 -0
- package/.claude-plugin/scripts/uninstall.sh +80 -0
- package/.claude-plugin/scripts/update.sh +84 -0
- package/.mcp.json +20 -0
- package/LICENSE +21 -0
- package/OMG-setup.sh +1093 -0
- package/README.md +335 -0
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/UPSTREAM_DIFF.md +20 -0
- package/agents/__init__.py +1 -0
- package/agents/_model_roles.yaml +26 -0
- package/agents/designer.md +67 -0
- package/agents/explore.md +60 -0
- package/agents/model_roles.py +196 -0
- package/agents/omg-api-builder.md +23 -0
- package/agents/omg-architect-mode.md +43 -0
- package/agents/omg-architect.md +13 -0
- package/agents/omg-backend-engineer.md +43 -0
- package/agents/omg-critic.md +16 -0
- package/agents/omg-database-engineer.md +43 -0
- package/agents/omg-escalation-router.md +17 -0
- package/agents/omg-executor.md +12 -0
- package/agents/omg-frontend-designer.md +42 -0
- package/agents/omg-implement-mode.md +50 -0
- package/agents/omg-infra-engineer.md +43 -0
- package/agents/omg-qa-tester.md +16 -0
- package/agents/omg-research-mode.md +43 -0
- package/agents/omg-security-auditor.md +43 -0
- package/agents/omg-testing-engineer.md +43 -0
- package/agents/plan.md +80 -0
- package/agents/quick_task.md +64 -0
- package/agents/reviewer.md +83 -0
- package/agents/task.md +71 -0
- package/commands/OMG:ccg.md +22 -0
- package/commands/OMG:compat.md +57 -0
- package/commands/OMG:crazy.md +125 -0
- package/commands/OMG:domain-init.md +11 -0
- package/commands/OMG:escalate.md +52 -0
- package/commands/OMG:health-check.md +45 -0
- package/commands/OMG:init.md +134 -0
- package/commands/OMG:mode.md +44 -0
- package/commands/OMG:project-init.md +11 -0
- package/commands/OMG:ralph-start.md +43 -0
- package/commands/OMG:ralph-stop.md +23 -0
- package/commands/OMG:teams.md +39 -0
- package/commands/ai-commit.md +113 -0
- package/commands/ccg.md +9 -0
- package/commands/create-agent.md +183 -0
- package/commands/omc-teams.md +9 -0
- package/commands/session-branch.md +85 -0
- package/commands/session-fork.md +53 -0
- package/commands/session-merge.md +134 -0
- package/commands/theme.md +44 -0
- package/config/lsp_languages.yaml +324 -0
- package/config/themes/catppuccin-frappe.yaml +14 -0
- package/config/themes/catppuccin-latte.yaml +14 -0
- package/config/themes/catppuccin-macchiato.yaml +14 -0
- package/config/themes/catppuccin-mocha.yaml +14 -0
- package/config/themes/dracula.yaml +14 -0
- package/config/themes/gruvbox-dark.yaml +14 -0
- package/config/themes/nord.yaml +14 -0
- package/config/themes/one-dark.yaml +14 -0
- package/config/themes/solarized-dark.yaml +14 -0
- package/config/themes/tokyo-night.yaml +14 -0
- package/control_plane/__init__.py +2 -0
- package/control_plane/openapi.yaml +109 -0
- package/control_plane/server.py +107 -0
- package/control_plane/service.py +148 -0
- package/crates/omg-natives/Cargo.toml +17 -0
- package/crates/omg-natives/src/clipboard.rs +5 -0
- package/crates/omg-natives/src/glob.rs +15 -0
- package/crates/omg-natives/src/grep.rs +15 -0
- package/crates/omg-natives/src/highlight.rs +15 -0
- package/crates/omg-natives/src/html.rs +14 -0
- package/crates/omg-natives/src/image.rs +5 -0
- package/crates/omg-natives/src/keys.rs +5 -0
- package/crates/omg-natives/src/lib.rs +36 -0
- package/crates/omg-natives/src/prof.rs +5 -0
- package/crates/omg-natives/src/ps.rs +5 -0
- package/crates/omg-natives/src/shell.rs +5 -0
- package/crates/omg-natives/src/task.rs +5 -0
- package/crates/omg-natives/src/text.rs +14 -0
- package/hooks/_agent_registry.py +421 -0
- package/hooks/_budget.py +31 -0
- package/hooks/_common.py +476 -0
- package/hooks/_learnings.py +126 -0
- package/hooks/_memory.py +103 -0
- package/hooks/circuit-breaker.py +270 -0
- package/hooks/config-guard.py +163 -0
- package/hooks/context_pressure.py +53 -0
- package/hooks/credential_store.py +801 -0
- package/hooks/fetch-rate-limits.py +212 -0
- package/hooks/firewall.py +48 -0
- package/hooks/hashline-formatter-bridge.py +224 -0
- package/hooks/hashline-injector.py +273 -0
- package/hooks/hashline-validator.py +216 -0
- package/hooks/idle-detector.py +95 -0
- package/hooks/intentgate-keyword-detector.py +188 -0
- package/hooks/magic-keyword-router.py +195 -0
- package/hooks/policy_engine.py +310 -0
- package/hooks/post-tool-failure.py +19 -0
- package/hooks/post-write.py +199 -0
- package/hooks/pre-compact.py +204 -0
- package/hooks/pre-tool-inject.py +98 -0
- package/hooks/prompt-enhancer.py +672 -0
- package/hooks/quality-runner.py +191 -0
- package/hooks/secret-guard.py +47 -0
- package/hooks/session-end-capture.py +137 -0
- package/hooks/session-start.py +275 -0
- package/hooks/shadow_manager.py +297 -0
- package/hooks/state_migration.py +209 -0
- package/hooks/stop-gate.py +7 -0
- package/hooks/stop_dispatcher.py +929 -0
- package/hooks/test-validator.py +138 -0
- package/hooks/todo-state-tracker.py +114 -0
- package/hooks/tool-ledger.py +126 -0
- package/hooks/trust_review.py +524 -0
- package/install.sh +9 -0
- package/omg_natives/__init__.py +186 -0
- package/omg_natives/_bindings.py +165 -0
- package/omg_natives/clipboard.py +36 -0
- package/omg_natives/glob.py +42 -0
- package/omg_natives/grep.py +61 -0
- package/omg_natives/highlight.py +54 -0
- package/omg_natives/html.py +157 -0
- package/omg_natives/image.py +51 -0
- package/omg_natives/keys.py +46 -0
- package/omg_natives/prof.py +39 -0
- package/omg_natives/ps.py +93 -0
- package/omg_natives/shell.py +58 -0
- package/omg_natives/task.py +41 -0
- package/omg_natives/text.py +50 -0
- package/package.json +26 -0
- package/plugins/README.md +82 -0
- package/plugins/advanced/commands/OMG:code-review.md +114 -0
- package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
- package/plugins/advanced/commands/OMG:handoff.md +115 -0
- package/plugins/advanced/commands/OMG:learn.md +110 -0
- package/plugins/advanced/commands/OMG:maintainer.md +31 -0
- package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
- package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
- package/plugins/advanced/commands/OMG:security-review.md +119 -0
- package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
- package/plugins/advanced/commands/OMG:ship.md +46 -0
- package/plugins/advanced/plugin.json +96 -0
- package/plugins/core/plugin.json +82 -0
- package/pytest.ini +5 -0
- package/registry/__init__.py +1 -0
- package/registry/verify_artifact.py +90 -0
- package/rules/contextual/architect-mode.md +9 -0
- package/rules/contextual/big-picture.md +20 -0
- package/rules/contextual/code-hygiene.md +26 -0
- package/rules/contextual/context-management.md +19 -0
- package/rules/contextual/context-minimization.md +32 -0
- package/rules/contextual/ddd-sdd.md +28 -0
- package/rules/contextual/dependency-safety.md +16 -0
- package/rules/contextual/doc-check.md +13 -0
- package/rules/contextual/implement-mode.md +9 -0
- package/rules/contextual/infra-safety.md +14 -0
- package/rules/contextual/outside-in.md +13 -0
- package/rules/contextual/persistent-mode.md +24 -0
- package/rules/contextual/research-mode.md +9 -0
- package/rules/contextual/security-domains.md +25 -0
- package/rules/contextual/vision-detection.md +27 -0
- package/rules/contextual/web-search.md +25 -0
- package/rules/contextual/write-verify.md +23 -0
- package/rules/core/00-truth.md +20 -0
- package/rules/core/01-surgical.md +19 -0
- package/rules/core/02-circuit-breaker.md +22 -0
- package/rules/core/03-ensemble.md +28 -0
- package/rules/core/04-testing.md +30 -0
- package/runtime/__init__.py +32 -0
- package/runtime/adapters/__init__.py +13 -0
- package/runtime/adapters/claude.py +60 -0
- package/runtime/adapters/gpt.py +53 -0
- package/runtime/adapters/local.py +53 -0
- package/runtime/business_workflow.py +220 -0
- package/runtime/compat.py +1299 -0
- package/runtime/custom_agent_loader.py +366 -0
- package/runtime/dispatcher.py +47 -0
- package/runtime/ecosystem.py +371 -0
- package/runtime/legacy_compat.py +7 -0
- package/runtime/omc_compat.py +7 -0
- package/runtime/omc_contract_snapshot.json +916 -0
- package/runtime/omg_compat_contract_snapshot.json +916 -0
- package/runtime/subagent_dispatcher.py +362 -0
- package/runtime/team_router.py +838 -0
- package/scripts/check-omc-contract-snapshot.py +12 -0
- package/scripts/check-omg-compat-contract-snapshot.py +137 -0
- package/scripts/check-omg-standalone-clean.py +102 -0
- package/scripts/legacy_to_omg_migrate.py +29 -0
- package/scripts/migrate-omc.py +464 -0
- package/scripts/omc_to_omg_migrate.py +12 -0
- package/scripts/omg.py +493 -0
- package/scripts/settings-merge.py +224 -0
- package/scripts/verify-no-omc.sh +5 -0
- package/scripts/verify-standalone.sh +21 -0
- package/templates/idea.yml +30 -0
- package/templates/policy.yaml +15 -0
- package/templates/profile.yaml +25 -0
- package/templates/runtime.yaml +12 -0
- package/templates/working-memory.md +17 -0
- package/tools/__init__.py +2 -0
- package/tools/browser_consent.py +289 -0
- package/tools/browser_stealth.py +481 -0
- package/tools/browser_tool.py +448 -0
- package/tools/changelog_generator.py +268 -0
- package/tools/commit_splitter.py +361 -0
- package/tools/config_discovery.py +151 -0
- package/tools/config_merger.py +449 -0
- package/tools/git_inspector.py +298 -0
- package/tools/lsp_client.py +275 -0
- package/tools/lsp_discovery.py +231 -0
- package/tools/lsp_operations.py +392 -0
- package/tools/python_repl.py +656 -0
- package/tools/python_sandbox.py +609 -0
- package/tools/search_providers/__init__.py +77 -0
- package/tools/search_providers/brave.py +115 -0
- package/tools/search_providers/exa.py +116 -0
- package/tools/search_providers/jina.py +104 -0
- package/tools/search_providers/perplexity.py +139 -0
- package/tools/search_providers/synthetic.py +74 -0
- package/tools/session_snapshot.py +736 -0
- package/tools/ssh_manager.py +912 -0
- package/tools/theme_engine.py +294 -0
- package/tools/theme_selector.py +137 -0
- package/tools/web_search.py +622 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Security Sandbox for OMG Python REPL
|
|
4
|
+
|
|
5
|
+
Provides a restricted execution environment that blocks dangerous operations:
|
|
6
|
+
- Dangerous imports (subprocess, socket, ctypes, etc.)
|
|
7
|
+
- File write access
|
|
8
|
+
- Network operations
|
|
9
|
+
- Sandbox escape patterns (__class__.__mro__, __subclasses__, etc.)
|
|
10
|
+
- Dangerous builtins (__import__, eval, exec, compile, etc.)
|
|
11
|
+
|
|
12
|
+
Feature flag: OMG_REPL_SANDBOX_ENABLED (default: False)
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from tools.python_sandbox import execute_sandboxed, is_safe_code, create_sandbox
|
|
16
|
+
|
|
17
|
+
result = execute_sandboxed("print('hello')")
|
|
18
|
+
# => {"stdout": "hello\\n", "stderr": "", "result": None, "error": None, "blocked": False}
|
|
19
|
+
|
|
20
|
+
safe = is_safe_code("import subprocess")
|
|
21
|
+
# => False
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import ast
|
|
25
|
+
import contextlib
|
|
26
|
+
import io
|
|
27
|
+
import os
|
|
28
|
+
import sys
|
|
29
|
+
import traceback
|
|
30
|
+
from typing import Any, Dict, List, Optional, Set
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- Lazy imports for hooks/_common.py ---
|
|
34
|
+
|
|
35
|
+
_get_feature_flag = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _ensure_imports():
|
|
39
|
+
"""Lazy import feature flag from hooks/_common.py."""
|
|
40
|
+
global _get_feature_flag
|
|
41
|
+
if _get_feature_flag is not None:
|
|
42
|
+
return
|
|
43
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
44
|
+
if repo_root not in sys.path:
|
|
45
|
+
sys.path.insert(0, repo_root)
|
|
46
|
+
try:
|
|
47
|
+
from hooks._common import get_feature_flag as _gff
|
|
48
|
+
_get_feature_flag = _gff
|
|
49
|
+
except ImportError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _is_sandbox_enabled() -> bool:
|
|
54
|
+
"""Check if sandbox feature is enabled."""
|
|
55
|
+
# Fast path: check env var directly
|
|
56
|
+
env_val = os.environ.get("OMG_REPL_SANDBOX_ENABLED", "").lower()
|
|
57
|
+
if env_val in ("0", "false", "no"):
|
|
58
|
+
return False
|
|
59
|
+
if env_val in ("1", "true", "yes"):
|
|
60
|
+
return True
|
|
61
|
+
# Fallback to hooks/_common.get_feature_flag
|
|
62
|
+
_ensure_imports()
|
|
63
|
+
if _get_feature_flag is not None:
|
|
64
|
+
return _get_feature_flag("REPL_SANDBOX", default=False)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --- Blocked imports configuration ---
|
|
69
|
+
|
|
70
|
+
_DEFAULT_BLOCKED_IMPORTS: frozenset = frozenset({
|
|
71
|
+
"subprocess",
|
|
72
|
+
"socket",
|
|
73
|
+
"ctypes",
|
|
74
|
+
"importlib",
|
|
75
|
+
"pickle",
|
|
76
|
+
"marshal",
|
|
77
|
+
"shelve",
|
|
78
|
+
"multiprocessing",
|
|
79
|
+
"threading",
|
|
80
|
+
"pty",
|
|
81
|
+
"shutil",
|
|
82
|
+
"signal",
|
|
83
|
+
"resource",
|
|
84
|
+
"code",
|
|
85
|
+
"codeop",
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _get_blocked_imports() -> Set[str]:
|
|
90
|
+
"""Get the set of blocked import names, configurable via env var."""
|
|
91
|
+
env_val = os.environ.get("OMG_SANDBOX_BLOCKED_IMPORTS", "").strip()
|
|
92
|
+
if env_val:
|
|
93
|
+
custom = frozenset(name.strip() for name in env_val.split(",") if name.strip())
|
|
94
|
+
return _DEFAULT_BLOCKED_IMPORTS | custom
|
|
95
|
+
return set(_DEFAULT_BLOCKED_IMPORTS)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# --- Blocked builtins ---
|
|
99
|
+
|
|
100
|
+
_DANGEROUS_BUILTINS: frozenset = frozenset({
|
|
101
|
+
"__import__",
|
|
102
|
+
"eval",
|
|
103
|
+
"exec",
|
|
104
|
+
"compile",
|
|
105
|
+
"globals",
|
|
106
|
+
"locals",
|
|
107
|
+
"vars",
|
|
108
|
+
"dir",
|
|
109
|
+
"getattr",
|
|
110
|
+
"setattr",
|
|
111
|
+
"delattr",
|
|
112
|
+
"hasattr",
|
|
113
|
+
"breakpoint",
|
|
114
|
+
"exit",
|
|
115
|
+
"quit",
|
|
116
|
+
"help",
|
|
117
|
+
"input",
|
|
118
|
+
"memoryview",
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --- Sandbox escape patterns ---
|
|
123
|
+
|
|
124
|
+
_ESCAPE_PATTERNS: List[str] = [
|
|
125
|
+
"__class__",
|
|
126
|
+
"__mro__",
|
|
127
|
+
"__subclasses__",
|
|
128
|
+
"__bases__",
|
|
129
|
+
"__builtins__",
|
|
130
|
+
"__globals__",
|
|
131
|
+
"__code__",
|
|
132
|
+
"__func__",
|
|
133
|
+
"__self__",
|
|
134
|
+
"__dict__",
|
|
135
|
+
"__init_subclass__",
|
|
136
|
+
"__set_name__",
|
|
137
|
+
"__class_getitem__",
|
|
138
|
+
"os.system",
|
|
139
|
+
"os.popen",
|
|
140
|
+
"os.exec",
|
|
141
|
+
"os.spawn",
|
|
142
|
+
"os.fork",
|
|
143
|
+
"sys.modules",
|
|
144
|
+
"sys._getframe",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# --- AST-based static analysis ---
|
|
149
|
+
|
|
150
|
+
class _SafetyChecker(ast.NodeVisitor):
|
|
151
|
+
"""AST visitor that checks for dangerous code patterns."""
|
|
152
|
+
|
|
153
|
+
def __init__(self, blocked_imports: Set[str]):
|
|
154
|
+
self.blocked_imports = blocked_imports
|
|
155
|
+
self.violations: List[str] = []
|
|
156
|
+
|
|
157
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
158
|
+
for alias in node.names:
|
|
159
|
+
module_name = alias.name.split(".")[0]
|
|
160
|
+
if module_name in self.blocked_imports:
|
|
161
|
+
self.violations.append(
|
|
162
|
+
f"Blocked import: '{alias.name}'"
|
|
163
|
+
)
|
|
164
|
+
self.generic_visit(node)
|
|
165
|
+
|
|
166
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
167
|
+
if node.module:
|
|
168
|
+
module_name = node.module.split(".")[0]
|
|
169
|
+
if module_name in self.blocked_imports:
|
|
170
|
+
self.violations.append(
|
|
171
|
+
f"Blocked import: 'from {node.module}'"
|
|
172
|
+
)
|
|
173
|
+
self.generic_visit(node)
|
|
174
|
+
|
|
175
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
176
|
+
# Check for __import__() calls
|
|
177
|
+
if isinstance(node.func, ast.Name):
|
|
178
|
+
if node.func.id == "__import__":
|
|
179
|
+
self.violations.append("Blocked: __import__() call")
|
|
180
|
+
elif node.func.id in ("eval", "exec", "compile"):
|
|
181
|
+
self.violations.append(
|
|
182
|
+
f"Blocked: {node.func.id}() call"
|
|
183
|
+
)
|
|
184
|
+
# Check for os.system(), os.popen() etc
|
|
185
|
+
if isinstance(node.func, ast.Attribute):
|
|
186
|
+
if isinstance(node.func.value, ast.Name):
|
|
187
|
+
full_name = f"{node.func.value.id}.{node.func.attr}"
|
|
188
|
+
if full_name in ("os.system", "os.popen", "os.execvp",
|
|
189
|
+
"os.execv", "os.execve", "os.spawnl",
|
|
190
|
+
"os.spawnle", "os.fork"):
|
|
191
|
+
self.violations.append(f"Blocked: {full_name}() call")
|
|
192
|
+
self.generic_visit(node)
|
|
193
|
+
|
|
194
|
+
def visit_Attribute(self, node: ast.Attribute) -> None:
|
|
195
|
+
# Check for sandbox escape attributes
|
|
196
|
+
if node.attr in ("__class__", "__mro__", "__subclasses__",
|
|
197
|
+
"__bases__", "__builtins__", "__globals__",
|
|
198
|
+
"__code__", "__func__", "__self__",
|
|
199
|
+
"__init_subclass__", "__set_name__",
|
|
200
|
+
"__class_getitem__"):
|
|
201
|
+
self.violations.append(
|
|
202
|
+
f"Blocked: access to '{node.attr}' (sandbox escape)"
|
|
203
|
+
)
|
|
204
|
+
self.generic_visit(node)
|
|
205
|
+
|
|
206
|
+
def visit_Name(self, node: ast.Name) -> None:
|
|
207
|
+
# Block direct access to dangerous names
|
|
208
|
+
if node.id == "__builtins__":
|
|
209
|
+
self.violations.append("Blocked: access to '__builtins__'")
|
|
210
|
+
self.generic_visit(node)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def is_safe_code(code: str) -> bool:
|
|
214
|
+
"""Static analysis check: return True if code appears safe to execute.
|
|
215
|
+
|
|
216
|
+
Parses the code into an AST and checks for:
|
|
217
|
+
- Import statements with blocked modules
|
|
218
|
+
- Call nodes invoking dangerous functions
|
|
219
|
+
- Attribute access to sandbox escape dunder methods
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
code: Python source code to check
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
True if code passes static analysis, False if dangerous patterns found
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
tree = ast.parse(code)
|
|
229
|
+
except SyntaxError:
|
|
230
|
+
# Let the actual execution report the syntax error
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
blocked_imports = _get_blocked_imports()
|
|
234
|
+
checker = _SafetyChecker(blocked_imports)
|
|
235
|
+
checker.visit(tree)
|
|
236
|
+
return len(checker.violations) == 0
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_code_violations(code: str) -> List[str]:
|
|
240
|
+
"""Return list of safety violations found in code.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
code: Python source code to check
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
List of violation description strings (empty if safe)
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
tree = ast.parse(code)
|
|
250
|
+
except SyntaxError:
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
blocked_imports = _get_blocked_imports()
|
|
254
|
+
checker = _SafetyChecker(blocked_imports)
|
|
255
|
+
checker.visit(tree)
|
|
256
|
+
return checker.violations
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# --- String-level escape detection ---
|
|
260
|
+
|
|
261
|
+
def _check_string_escapes(code: str) -> Optional[str]:
|
|
262
|
+
"""Check for sandbox escape patterns in raw code string.
|
|
263
|
+
|
|
264
|
+
This catches patterns that might not appear in the AST
|
|
265
|
+
(e.g., constructed via string manipulation).
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
code: Raw source code string
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Violation description if found, None if clean
|
|
272
|
+
"""
|
|
273
|
+
for pattern in _ESCAPE_PATTERNS:
|
|
274
|
+
if pattern in code:
|
|
275
|
+
return f"Blocked: suspicious pattern '{pattern}' detected"
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# --- Restricted open() ---
|
|
280
|
+
|
|
281
|
+
_ALLOWED_READ_MODES: frozenset = frozenset({
|
|
282
|
+
"r", "rb", "rt",
|
|
283
|
+
"", # default mode is 'r'
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _restricted_open(name, mode="r", *args, **kwargs):
|
|
288
|
+
"""Restricted open() that only allows read-mode file access.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
name: File path to open
|
|
292
|
+
mode: File open mode (only read modes allowed)
|
|
293
|
+
*args: Passed through to builtin open
|
|
294
|
+
**kwargs: Passed through to builtin open
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
File object (read-only)
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
PermissionError: If write/append mode is attempted
|
|
301
|
+
"""
|
|
302
|
+
# Normalize mode string
|
|
303
|
+
clean_mode = mode.strip().lower()
|
|
304
|
+
if clean_mode not in _ALLOWED_READ_MODES:
|
|
305
|
+
raise PermissionError(
|
|
306
|
+
f"Sandbox: write access denied (mode='{mode}'). "
|
|
307
|
+
f"Only read modes are allowed: {sorted(_ALLOWED_READ_MODES - {''})}"
|
|
308
|
+
)
|
|
309
|
+
return open(name, mode, *args, **kwargs)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# --- Restricted __import__ ---
|
|
313
|
+
|
|
314
|
+
def _make_restricted_import(blocked_imports: Set[str]):
|
|
315
|
+
"""Create a restricted __import__ function that blocks dangerous modules.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
blocked_imports: Set of module names to block
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
A replacement __import__ function
|
|
322
|
+
"""
|
|
323
|
+
_real_import = __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__
|
|
324
|
+
|
|
325
|
+
def _restricted_import(name, *args, **kwargs):
|
|
326
|
+
top_level = name.split(".")[0]
|
|
327
|
+
if top_level in blocked_imports:
|
|
328
|
+
raise ImportError(
|
|
329
|
+
f"Sandbox: import of '{name}' is blocked. "
|
|
330
|
+
f"Module '{top_level}' is on the restricted list."
|
|
331
|
+
)
|
|
332
|
+
return _real_import(name, *args, **kwargs)
|
|
333
|
+
|
|
334
|
+
return _restricted_import
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# --- Safe builtins construction ---
|
|
338
|
+
|
|
339
|
+
def _build_safe_builtins(blocked_imports: Set[str]) -> Dict[str, Any]:
|
|
340
|
+
"""Build a restricted __builtins__ dict for sandbox execution.
|
|
341
|
+
|
|
342
|
+
Removes dangerous builtins and replaces open/__import__ with
|
|
343
|
+
restricted versions.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
blocked_imports: Set of module names to block in __import__
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
Dict of safe builtin names to their values
|
|
350
|
+
"""
|
|
351
|
+
# Start from a copy of real builtins
|
|
352
|
+
if isinstance(__builtins__, dict):
|
|
353
|
+
safe = dict(__builtins__)
|
|
354
|
+
else:
|
|
355
|
+
safe = {k: getattr(__builtins__, k) for k in dir(__builtins__)
|
|
356
|
+
if not k.startswith("_") or k == "__name__"}
|
|
357
|
+
# Include common dunders that are needed
|
|
358
|
+
for attr in ("__build_class__", "__name__", "__spec__"):
|
|
359
|
+
if hasattr(__builtins__, attr):
|
|
360
|
+
safe[attr] = getattr(__builtins__, attr)
|
|
361
|
+
|
|
362
|
+
# Remove dangerous builtins
|
|
363
|
+
for name in _DANGEROUS_BUILTINS:
|
|
364
|
+
safe.pop(name, None)
|
|
365
|
+
|
|
366
|
+
# Replace open with restricted version
|
|
367
|
+
safe["open"] = _restricted_open
|
|
368
|
+
|
|
369
|
+
# Replace __import__ with restricted version
|
|
370
|
+
safe["__import__"] = _make_restricted_import(blocked_imports)
|
|
371
|
+
|
|
372
|
+
# Ensure print is available
|
|
373
|
+
safe["print"] = print
|
|
374
|
+
|
|
375
|
+
return safe
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# --- SandboxedExecutor ---
|
|
379
|
+
|
|
380
|
+
class SandboxedExecutor:
|
|
381
|
+
"""Restricted Python execution environment.
|
|
382
|
+
|
|
383
|
+
Creates an isolated namespace with restricted builtins that
|
|
384
|
+
prevents dangerous operations like system calls, network access,
|
|
385
|
+
and file writes.
|
|
386
|
+
|
|
387
|
+
Usage:
|
|
388
|
+
sandbox = SandboxedExecutor()
|
|
389
|
+
result = sandbox.execute("print('hello')")
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
def __init__(
|
|
393
|
+
self,
|
|
394
|
+
namespace: Optional[Dict[str, Any]] = None,
|
|
395
|
+
blocked_imports: Optional[Set[str]] = None,
|
|
396
|
+
extra_blocked_builtins: Optional[Set[str]] = None,
|
|
397
|
+
):
|
|
398
|
+
"""Initialize the sandboxed executor.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
namespace: Optional existing namespace to sandbox (will be modified)
|
|
402
|
+
blocked_imports: Override the default blocked imports set
|
|
403
|
+
extra_blocked_builtins: Additional builtins to block beyond defaults
|
|
404
|
+
"""
|
|
405
|
+
self._blocked_imports = blocked_imports or _get_blocked_imports()
|
|
406
|
+
|
|
407
|
+
# Build safe builtins
|
|
408
|
+
self._safe_builtins = _build_safe_builtins(self._blocked_imports)
|
|
409
|
+
|
|
410
|
+
# Remove extra builtins if requested
|
|
411
|
+
if extra_blocked_builtins:
|
|
412
|
+
for name in extra_blocked_builtins:
|
|
413
|
+
self._safe_builtins.pop(name, None)
|
|
414
|
+
|
|
415
|
+
# Initialize or adopt namespace
|
|
416
|
+
if namespace is not None:
|
|
417
|
+
self._namespace = namespace
|
|
418
|
+
self._namespace["__builtins__"] = self._safe_builtins
|
|
419
|
+
else:
|
|
420
|
+
self._namespace = {"__builtins__": self._safe_builtins}
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def namespace(self) -> Dict[str, Any]:
|
|
424
|
+
"""The execution namespace."""
|
|
425
|
+
return self._namespace
|
|
426
|
+
|
|
427
|
+
def execute(self, code: str) -> Dict[str, Any]:
|
|
428
|
+
"""Execute code in the sandbox.
|
|
429
|
+
|
|
430
|
+
Performs both static analysis and runtime restriction.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
code: Python source code to execute
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Dict with keys:
|
|
437
|
+
stdout: Captured stdout output
|
|
438
|
+
stderr: Captured stderr output
|
|
439
|
+
result: Expression result (repr) or None
|
|
440
|
+
error: Error message or None
|
|
441
|
+
blocked: True if code was blocked by safety checks
|
|
442
|
+
"""
|
|
443
|
+
# Step 1: String-level escape check
|
|
444
|
+
escape_violation = _check_string_escapes(code)
|
|
445
|
+
if escape_violation:
|
|
446
|
+
return {
|
|
447
|
+
"stdout": "",
|
|
448
|
+
"stderr": "",
|
|
449
|
+
"result": None,
|
|
450
|
+
"error": escape_violation,
|
|
451
|
+
"blocked": True,
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
# Step 2: AST-level static analysis
|
|
455
|
+
violations = get_code_violations(code)
|
|
456
|
+
if violations:
|
|
457
|
+
return {
|
|
458
|
+
"stdout": "",
|
|
459
|
+
"stderr": "",
|
|
460
|
+
"result": None,
|
|
461
|
+
"error": "Security violation: " + "; ".join(violations),
|
|
462
|
+
"blocked": True,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
# Step 3: Execute in restricted namespace
|
|
466
|
+
stdout_buf = io.StringIO()
|
|
467
|
+
stderr_buf = io.StringIO()
|
|
468
|
+
result = None
|
|
469
|
+
error = None
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
with contextlib.redirect_stdout(stdout_buf), \
|
|
473
|
+
contextlib.redirect_stderr(stderr_buf):
|
|
474
|
+
# Try expression eval first
|
|
475
|
+
try:
|
|
476
|
+
tree = ast.parse(code, mode="eval")
|
|
477
|
+
compiled = compile(tree, "<sandbox>", "eval")
|
|
478
|
+
result_val = eval(compiled, self._namespace) # noqa: S307
|
|
479
|
+
if result_val is not None:
|
|
480
|
+
result = repr(result_val)
|
|
481
|
+
except SyntaxError:
|
|
482
|
+
# Fall back to exec for statements
|
|
483
|
+
tree = ast.parse(code, mode="exec")
|
|
484
|
+
compiled = compile(tree, "<sandbox>", "exec")
|
|
485
|
+
exec(compiled, self._namespace) # noqa: S102
|
|
486
|
+
except ImportError as e:
|
|
487
|
+
if "blocked" in str(e).lower() or "restricted" in str(e).lower():
|
|
488
|
+
return {
|
|
489
|
+
"stdout": stdout_buf.getvalue(),
|
|
490
|
+
"stderr": stderr_buf.getvalue(),
|
|
491
|
+
"result": None,
|
|
492
|
+
"error": str(e),
|
|
493
|
+
"blocked": True,
|
|
494
|
+
}
|
|
495
|
+
error = traceback.format_exc()
|
|
496
|
+
except PermissionError as e:
|
|
497
|
+
if "sandbox" in str(e).lower():
|
|
498
|
+
return {
|
|
499
|
+
"stdout": stdout_buf.getvalue(),
|
|
500
|
+
"stderr": stderr_buf.getvalue(),
|
|
501
|
+
"result": None,
|
|
502
|
+
"error": str(e),
|
|
503
|
+
"blocked": True,
|
|
504
|
+
}
|
|
505
|
+
error = traceback.format_exc()
|
|
506
|
+
except Exception:
|
|
507
|
+
error = traceback.format_exc()
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
"stdout": stdout_buf.getvalue(),
|
|
511
|
+
"stderr": stderr_buf.getvalue(),
|
|
512
|
+
"result": result,
|
|
513
|
+
"error": error,
|
|
514
|
+
"blocked": False,
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
# --- Module-level convenience functions ---
|
|
519
|
+
|
|
520
|
+
def create_sandbox(
|
|
521
|
+
namespace: Optional[Dict[str, Any]] = None,
|
|
522
|
+
blocked_imports: Optional[Set[str]] = None,
|
|
523
|
+
) -> SandboxedExecutor:
|
|
524
|
+
"""Create a sandboxed executor with restricted execution environment.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
namespace: Optional existing namespace to use (will be restricted)
|
|
528
|
+
blocked_imports: Optional override for blocked imports set
|
|
529
|
+
|
|
530
|
+
Returns:
|
|
531
|
+
SandboxedExecutor instance ready for use
|
|
532
|
+
"""
|
|
533
|
+
return SandboxedExecutor(
|
|
534
|
+
namespace=namespace,
|
|
535
|
+
blocked_imports=blocked_imports,
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def execute_sandboxed(
|
|
540
|
+
code: str,
|
|
541
|
+
namespace: Optional[Dict[str, Any]] = None,
|
|
542
|
+
) -> Dict[str, Any]:
|
|
543
|
+
"""Execute code in a one-shot sandbox.
|
|
544
|
+
|
|
545
|
+
Convenience function that creates a temporary sandbox and executes code.
|
|
546
|
+
|
|
547
|
+
Args:
|
|
548
|
+
code: Python source code to execute
|
|
549
|
+
namespace: Optional namespace dict to execute in
|
|
550
|
+
|
|
551
|
+
Returns:
|
|
552
|
+
Dict with keys: stdout, stderr, result, error, blocked
|
|
553
|
+
"""
|
|
554
|
+
sandbox = create_sandbox(namespace=namespace)
|
|
555
|
+
return sandbox.execute(code)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
# --- CLI Interface ---
|
|
559
|
+
|
|
560
|
+
def _cli_main():
|
|
561
|
+
"""CLI entry point for python_sandbox.py."""
|
|
562
|
+
import argparse
|
|
563
|
+
|
|
564
|
+
parser = argparse.ArgumentParser(
|
|
565
|
+
description="OMG Python Sandbox — restricted execution environment",
|
|
566
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
567
|
+
)
|
|
568
|
+
parser.add_argument("--exec", dest="code", help="Execute Python code in sandbox")
|
|
569
|
+
parser.add_argument(
|
|
570
|
+
"--check", dest="check_code", help="Static safety check only (no execution)"
|
|
571
|
+
)
|
|
572
|
+
parser.add_argument(
|
|
573
|
+
"--status", action="store_true", help="Show sandbox status and configuration"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
args = parser.parse_args()
|
|
577
|
+
|
|
578
|
+
if args.status:
|
|
579
|
+
import json
|
|
580
|
+
status = {
|
|
581
|
+
"sandbox_enabled": _is_sandbox_enabled(),
|
|
582
|
+
"blocked_imports": sorted(_get_blocked_imports()),
|
|
583
|
+
"dangerous_builtins": sorted(_DANGEROUS_BUILTINS),
|
|
584
|
+
"escape_patterns_count": len(_ESCAPE_PATTERNS),
|
|
585
|
+
}
|
|
586
|
+
print(json.dumps(status, indent=2))
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
if args.check_code:
|
|
590
|
+
import json
|
|
591
|
+
violations = get_code_violations(args.check_code)
|
|
592
|
+
result = {
|
|
593
|
+
"safe": len(violations) == 0,
|
|
594
|
+
"violations": violations,
|
|
595
|
+
}
|
|
596
|
+
print(json.dumps(result, indent=2))
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
if args.code:
|
|
600
|
+
import json
|
|
601
|
+
result = execute_sandboxed(args.code)
|
|
602
|
+
print(json.dumps(result, indent=2))
|
|
603
|
+
return
|
|
604
|
+
|
|
605
|
+
parser.print_help()
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
if __name__ == "__main__":
|
|
609
|
+
_cli_main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
OMG Search Providers Package
|
|
4
|
+
|
|
5
|
+
Auto-registers all bundled providers with the module-level WebSearchManager
|
|
6
|
+
from tools.web_search when this package is imported.
|
|
7
|
+
|
|
8
|
+
Providers:
|
|
9
|
+
- SyntheticProvider: Mock results for testing/dry-run (no API key needed)
|
|
10
|
+
- ExaProvider: Exa AI semantic search
|
|
11
|
+
- BraveProvider: Brave Search API
|
|
12
|
+
- PerplexityProvider: Perplexity AI chat completions
|
|
13
|
+
- JinaProvider: Jina Reader URL-based content extraction
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
# Ensure tools dir is on path
|
|
20
|
+
_tools_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
21
|
+
if _tools_dir not in sys.path:
|
|
22
|
+
sys.path.insert(0, _tools_dir)
|
|
23
|
+
|
|
24
|
+
from search_providers.synthetic import SyntheticProvider
|
|
25
|
+
from search_providers.exa import ExaProvider
|
|
26
|
+
from search_providers.brave import BraveProvider
|
|
27
|
+
from search_providers.perplexity import PerplexityProvider
|
|
28
|
+
from search_providers.jina import JinaProvider
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"SyntheticProvider",
|
|
32
|
+
"ExaProvider",
|
|
33
|
+
"BraveProvider",
|
|
34
|
+
"PerplexityProvider",
|
|
35
|
+
"JinaProvider",
|
|
36
|
+
"register_all",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# Provider registry: name -> class mapping
|
|
40
|
+
PROVIDER_CLASSES = {
|
|
41
|
+
"synthetic": SyntheticProvider,
|
|
42
|
+
"exa": ExaProvider,
|
|
43
|
+
"brave": BraveProvider,
|
|
44
|
+
"perplexity": PerplexityProvider,
|
|
45
|
+
"jina": JinaProvider,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def register_all(manager=None):
|
|
50
|
+
"""Register all bundled providers with a WebSearchManager.
|
|
51
|
+
|
|
52
|
+
If no manager is given, uses the module-level singleton from web_search.
|
|
53
|
+
|
|
54
|
+
Only instantiates providers that either:
|
|
55
|
+
- Don't require an API key (SyntheticProvider)
|
|
56
|
+
- Have a resolvable API key (env var or credential store)
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
manager: A WebSearchManager instance. Defaults to web_search.manager.
|
|
60
|
+
"""
|
|
61
|
+
if manager is None:
|
|
62
|
+
from web_search import manager as _mgr
|
|
63
|
+
manager = _mgr
|
|
64
|
+
|
|
65
|
+
for name, cls in PROVIDER_CLASSES.items():
|
|
66
|
+
schema = getattr(cls, "CONFIG_SCHEMA", {})
|
|
67
|
+
api_key_required = schema.get("api_key", {}).get("required", False)
|
|
68
|
+
|
|
69
|
+
if not api_key_required:
|
|
70
|
+
# No API key required — always register (e.g., SyntheticProvider)
|
|
71
|
+
manager.register_provider(name, cls())
|
|
72
|
+
else:
|
|
73
|
+
# Only register if API key is available
|
|
74
|
+
from web_search import get_api_key
|
|
75
|
+
key = get_api_key(name)
|
|
76
|
+
if key:
|
|
77
|
+
manager.register_provider(name, cls(api_key=key))
|