@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.
Files changed (229) hide show
  1. package/.claude-plugin/marketplace.json +36 -0
  2. package/.claude-plugin/plugin.json +23 -0
  3. package/.claude-plugin/scripts/install.sh +49 -0
  4. package/.claude-plugin/scripts/uninstall.sh +80 -0
  5. package/.claude-plugin/scripts/update.sh +84 -0
  6. package/.mcp.json +20 -0
  7. package/LICENSE +21 -0
  8. package/OMG-setup.sh +1093 -0
  9. package/README.md +335 -0
  10. package/THIRD_PARTY_NOTICES.md +24 -0
  11. package/UPSTREAM_DIFF.md +20 -0
  12. package/agents/__init__.py +1 -0
  13. package/agents/_model_roles.yaml +26 -0
  14. package/agents/designer.md +67 -0
  15. package/agents/explore.md +60 -0
  16. package/agents/model_roles.py +196 -0
  17. package/agents/omg-api-builder.md +23 -0
  18. package/agents/omg-architect-mode.md +43 -0
  19. package/agents/omg-architect.md +13 -0
  20. package/agents/omg-backend-engineer.md +43 -0
  21. package/agents/omg-critic.md +16 -0
  22. package/agents/omg-database-engineer.md +43 -0
  23. package/agents/omg-escalation-router.md +17 -0
  24. package/agents/omg-executor.md +12 -0
  25. package/agents/omg-frontend-designer.md +42 -0
  26. package/agents/omg-implement-mode.md +50 -0
  27. package/agents/omg-infra-engineer.md +43 -0
  28. package/agents/omg-qa-tester.md +16 -0
  29. package/agents/omg-research-mode.md +43 -0
  30. package/agents/omg-security-auditor.md +43 -0
  31. package/agents/omg-testing-engineer.md +43 -0
  32. package/agents/plan.md +80 -0
  33. package/agents/quick_task.md +64 -0
  34. package/agents/reviewer.md +83 -0
  35. package/agents/task.md +71 -0
  36. package/commands/OMG:ccg.md +22 -0
  37. package/commands/OMG:compat.md +57 -0
  38. package/commands/OMG:crazy.md +125 -0
  39. package/commands/OMG:domain-init.md +11 -0
  40. package/commands/OMG:escalate.md +52 -0
  41. package/commands/OMG:health-check.md +45 -0
  42. package/commands/OMG:init.md +134 -0
  43. package/commands/OMG:mode.md +44 -0
  44. package/commands/OMG:project-init.md +11 -0
  45. package/commands/OMG:ralph-start.md +43 -0
  46. package/commands/OMG:ralph-stop.md +23 -0
  47. package/commands/OMG:teams.md +39 -0
  48. package/commands/ai-commit.md +113 -0
  49. package/commands/ccg.md +9 -0
  50. package/commands/create-agent.md +183 -0
  51. package/commands/omc-teams.md +9 -0
  52. package/commands/session-branch.md +85 -0
  53. package/commands/session-fork.md +53 -0
  54. package/commands/session-merge.md +134 -0
  55. package/commands/theme.md +44 -0
  56. package/config/lsp_languages.yaml +324 -0
  57. package/config/themes/catppuccin-frappe.yaml +14 -0
  58. package/config/themes/catppuccin-latte.yaml +14 -0
  59. package/config/themes/catppuccin-macchiato.yaml +14 -0
  60. package/config/themes/catppuccin-mocha.yaml +14 -0
  61. package/config/themes/dracula.yaml +14 -0
  62. package/config/themes/gruvbox-dark.yaml +14 -0
  63. package/config/themes/nord.yaml +14 -0
  64. package/config/themes/one-dark.yaml +14 -0
  65. package/config/themes/solarized-dark.yaml +14 -0
  66. package/config/themes/tokyo-night.yaml +14 -0
  67. package/control_plane/__init__.py +2 -0
  68. package/control_plane/openapi.yaml +109 -0
  69. package/control_plane/server.py +107 -0
  70. package/control_plane/service.py +148 -0
  71. package/crates/omg-natives/Cargo.toml +17 -0
  72. package/crates/omg-natives/src/clipboard.rs +5 -0
  73. package/crates/omg-natives/src/glob.rs +15 -0
  74. package/crates/omg-natives/src/grep.rs +15 -0
  75. package/crates/omg-natives/src/highlight.rs +15 -0
  76. package/crates/omg-natives/src/html.rs +14 -0
  77. package/crates/omg-natives/src/image.rs +5 -0
  78. package/crates/omg-natives/src/keys.rs +5 -0
  79. package/crates/omg-natives/src/lib.rs +36 -0
  80. package/crates/omg-natives/src/prof.rs +5 -0
  81. package/crates/omg-natives/src/ps.rs +5 -0
  82. package/crates/omg-natives/src/shell.rs +5 -0
  83. package/crates/omg-natives/src/task.rs +5 -0
  84. package/crates/omg-natives/src/text.rs +14 -0
  85. package/hooks/_agent_registry.py +421 -0
  86. package/hooks/_budget.py +31 -0
  87. package/hooks/_common.py +476 -0
  88. package/hooks/_learnings.py +126 -0
  89. package/hooks/_memory.py +103 -0
  90. package/hooks/circuit-breaker.py +270 -0
  91. package/hooks/config-guard.py +163 -0
  92. package/hooks/context_pressure.py +53 -0
  93. package/hooks/credential_store.py +801 -0
  94. package/hooks/fetch-rate-limits.py +212 -0
  95. package/hooks/firewall.py +48 -0
  96. package/hooks/hashline-formatter-bridge.py +224 -0
  97. package/hooks/hashline-injector.py +273 -0
  98. package/hooks/hashline-validator.py +216 -0
  99. package/hooks/idle-detector.py +95 -0
  100. package/hooks/intentgate-keyword-detector.py +188 -0
  101. package/hooks/magic-keyword-router.py +195 -0
  102. package/hooks/policy_engine.py +310 -0
  103. package/hooks/post-tool-failure.py +19 -0
  104. package/hooks/post-write.py +199 -0
  105. package/hooks/pre-compact.py +204 -0
  106. package/hooks/pre-tool-inject.py +98 -0
  107. package/hooks/prompt-enhancer.py +672 -0
  108. package/hooks/quality-runner.py +191 -0
  109. package/hooks/secret-guard.py +47 -0
  110. package/hooks/session-end-capture.py +137 -0
  111. package/hooks/session-start.py +275 -0
  112. package/hooks/shadow_manager.py +297 -0
  113. package/hooks/state_migration.py +209 -0
  114. package/hooks/stop-gate.py +7 -0
  115. package/hooks/stop_dispatcher.py +929 -0
  116. package/hooks/test-validator.py +138 -0
  117. package/hooks/todo-state-tracker.py +114 -0
  118. package/hooks/tool-ledger.py +126 -0
  119. package/hooks/trust_review.py +524 -0
  120. package/install.sh +9 -0
  121. package/omg_natives/__init__.py +186 -0
  122. package/omg_natives/_bindings.py +165 -0
  123. package/omg_natives/clipboard.py +36 -0
  124. package/omg_natives/glob.py +42 -0
  125. package/omg_natives/grep.py +61 -0
  126. package/omg_natives/highlight.py +54 -0
  127. package/omg_natives/html.py +157 -0
  128. package/omg_natives/image.py +51 -0
  129. package/omg_natives/keys.py +46 -0
  130. package/omg_natives/prof.py +39 -0
  131. package/omg_natives/ps.py +93 -0
  132. package/omg_natives/shell.py +58 -0
  133. package/omg_natives/task.py +41 -0
  134. package/omg_natives/text.py +50 -0
  135. package/package.json +26 -0
  136. package/plugins/README.md +82 -0
  137. package/plugins/advanced/commands/OMG:code-review.md +114 -0
  138. package/plugins/advanced/commands/OMG:deep-plan.md +221 -0
  139. package/plugins/advanced/commands/OMG:handoff.md +115 -0
  140. package/plugins/advanced/commands/OMG:learn.md +110 -0
  141. package/plugins/advanced/commands/OMG:maintainer.md +31 -0
  142. package/plugins/advanced/commands/OMG:ralph-start.md +43 -0
  143. package/plugins/advanced/commands/OMG:ralph-stop.md +23 -0
  144. package/plugins/advanced/commands/OMG:security-review.md +119 -0
  145. package/plugins/advanced/commands/OMG:sequential-thinking.md +20 -0
  146. package/plugins/advanced/commands/OMG:ship.md +46 -0
  147. package/plugins/advanced/plugin.json +96 -0
  148. package/plugins/core/plugin.json +82 -0
  149. package/pytest.ini +5 -0
  150. package/registry/__init__.py +1 -0
  151. package/registry/verify_artifact.py +90 -0
  152. package/rules/contextual/architect-mode.md +9 -0
  153. package/rules/contextual/big-picture.md +20 -0
  154. package/rules/contextual/code-hygiene.md +26 -0
  155. package/rules/contextual/context-management.md +19 -0
  156. package/rules/contextual/context-minimization.md +32 -0
  157. package/rules/contextual/ddd-sdd.md +28 -0
  158. package/rules/contextual/dependency-safety.md +16 -0
  159. package/rules/contextual/doc-check.md +13 -0
  160. package/rules/contextual/implement-mode.md +9 -0
  161. package/rules/contextual/infra-safety.md +14 -0
  162. package/rules/contextual/outside-in.md +13 -0
  163. package/rules/contextual/persistent-mode.md +24 -0
  164. package/rules/contextual/research-mode.md +9 -0
  165. package/rules/contextual/security-domains.md +25 -0
  166. package/rules/contextual/vision-detection.md +27 -0
  167. package/rules/contextual/web-search.md +25 -0
  168. package/rules/contextual/write-verify.md +23 -0
  169. package/rules/core/00-truth.md +20 -0
  170. package/rules/core/01-surgical.md +19 -0
  171. package/rules/core/02-circuit-breaker.md +22 -0
  172. package/rules/core/03-ensemble.md +28 -0
  173. package/rules/core/04-testing.md +30 -0
  174. package/runtime/__init__.py +32 -0
  175. package/runtime/adapters/__init__.py +13 -0
  176. package/runtime/adapters/claude.py +60 -0
  177. package/runtime/adapters/gpt.py +53 -0
  178. package/runtime/adapters/local.py +53 -0
  179. package/runtime/business_workflow.py +220 -0
  180. package/runtime/compat.py +1299 -0
  181. package/runtime/custom_agent_loader.py +366 -0
  182. package/runtime/dispatcher.py +47 -0
  183. package/runtime/ecosystem.py +371 -0
  184. package/runtime/legacy_compat.py +7 -0
  185. package/runtime/omc_compat.py +7 -0
  186. package/runtime/omc_contract_snapshot.json +916 -0
  187. package/runtime/omg_compat_contract_snapshot.json +916 -0
  188. package/runtime/subagent_dispatcher.py +362 -0
  189. package/runtime/team_router.py +838 -0
  190. package/scripts/check-omc-contract-snapshot.py +12 -0
  191. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  192. package/scripts/check-omg-standalone-clean.py +102 -0
  193. package/scripts/legacy_to_omg_migrate.py +29 -0
  194. package/scripts/migrate-omc.py +464 -0
  195. package/scripts/omc_to_omg_migrate.py +12 -0
  196. package/scripts/omg.py +493 -0
  197. package/scripts/settings-merge.py +224 -0
  198. package/scripts/verify-no-omc.sh +5 -0
  199. package/scripts/verify-standalone.sh +21 -0
  200. package/templates/idea.yml +30 -0
  201. package/templates/policy.yaml +15 -0
  202. package/templates/profile.yaml +25 -0
  203. package/templates/runtime.yaml +12 -0
  204. package/templates/working-memory.md +17 -0
  205. package/tools/__init__.py +2 -0
  206. package/tools/browser_consent.py +289 -0
  207. package/tools/browser_stealth.py +481 -0
  208. package/tools/browser_tool.py +448 -0
  209. package/tools/changelog_generator.py +268 -0
  210. package/tools/commit_splitter.py +361 -0
  211. package/tools/config_discovery.py +151 -0
  212. package/tools/config_merger.py +449 -0
  213. package/tools/git_inspector.py +298 -0
  214. package/tools/lsp_client.py +275 -0
  215. package/tools/lsp_discovery.py +231 -0
  216. package/tools/lsp_operations.py +392 -0
  217. package/tools/python_repl.py +656 -0
  218. package/tools/python_sandbox.py +609 -0
  219. package/tools/search_providers/__init__.py +77 -0
  220. package/tools/search_providers/brave.py +115 -0
  221. package/tools/search_providers/exa.py +116 -0
  222. package/tools/search_providers/jina.py +104 -0
  223. package/tools/search_providers/perplexity.py +139 -0
  224. package/tools/search_providers/synthetic.py +74 -0
  225. package/tools/session_snapshot.py +736 -0
  226. package/tools/ssh_manager.py +912 -0
  227. package/tools/theme_engine.py +294 -0
  228. package/tools/theme_selector.py +137 -0
  229. 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))