2026-08-08 23:24:58 -03:00
|
|
|
"""Hermes tool for running Python in a separate, credential-free pod."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from urllib.error import HTTPError, URLError
|
|
|
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
|
|
|
|
from tools.registry import registry, tool_error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sandbox_url() -> str:
|
|
|
|
|
"""Return the tenant-specific sandbox endpoint injected at startup."""
|
|
|
|
|
return os.environ.get("HERMES_CODE_SANDBOX_URL", "").strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sandbox_available() -> bool:
|
|
|
|
|
"""Expose the tool only when this tenant has an isolated endpoint."""
|
|
|
|
|
return _sandbox_url().startswith("http://hermes-chat-sandbox-")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def execute_python_sandbox(code: str) -> str:
|
|
|
|
|
"""Execute Python remotely and return the sandbox's structured result."""
|
|
|
|
|
if not isinstance(code, str) or not code.strip():
|
|
|
|
|
return tool_error("Python code is required.")
|
|
|
|
|
if len(code.encode("utf-8")) > 128 * 1024:
|
|
|
|
|
return tool_error("Python code exceeds the 128 KiB request limit.")
|
|
|
|
|
|
|
|
|
|
request = Request(
|
|
|
|
|
_sandbox_url(),
|
|
|
|
|
data=json.dumps({"code": code}).encode("utf-8"),
|
|
|
|
|
headers={"Content-Type": "application/json"},
|
|
|
|
|
method="POST",
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with urlopen(request, timeout=55) as response:
|
|
|
|
|
payload = response.read(256 * 1024)
|
|
|
|
|
return payload.decode("utf-8", errors="replace")
|
|
|
|
|
except HTTPError as exc:
|
|
|
|
|
detail = exc.read(4096).decode("utf-8", errors="replace")
|
|
|
|
|
return tool_error(f"Python sandbox rejected the request: {detail or exc.code}")
|
|
|
|
|
except (URLError, TimeoutError, OSError) as exc:
|
|
|
|
|
return tool_error(f"Python sandbox is unavailable: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
registry.register(
|
|
|
|
|
name="python_sandbox",
|
|
|
|
|
toolset="python_sandbox",
|
|
|
|
|
schema={
|
|
|
|
|
"name": "python_sandbox",
|
|
|
|
|
"description": (
|
2026-08-10 00:42:37 -03:00
|
|
|
"Run Python in this user's credential-free computation sandbox. "
|
|
|
|
|
"Its /workspace and /opt/data/workspace paths expose the same private "
|
|
|
|
|
"workspace, and /tmp is writable for temporary verifiers. "
|
|
|
|
|
"Use it for file verification, statistics, probability, Monte Carlo "
|
|
|
|
|
"simulation, data transforms, and calculations. The sandbox has no "
|
|
|
|
|
"Kubernetes, Vault, model-provider credentials, or access to other "
|
|
|
|
|
"users. Public research belongs in web_search."
|
2026-08-08 23:24:58 -03:00
|
|
|
),
|
|
|
|
|
"parameters": {
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"code": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "A self-contained Python 3 program that prints its result.",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["code"],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
handler=lambda args, **_: execute_python_sandbox(args.get("code", "")),
|
|
|
|
|
check_fn=_sandbox_available,
|
|
|
|
|
emoji="🧮",
|
|
|
|
|
max_result_size_chars=200_000,
|
|
|
|
|
)
|