fix(code-exec): bind execute_code tool socket to a per-session RPC token

The execute_code sandbox exposed its tool-call RPC (AF_UNIX socket and
remote file-poll transports) without any caller check, so any local
process that could reach the socket / rpc dir could dispatch
terminal-capable tool calls through the parent. Mint a per-session
HERMES_RPC_TOKEN, pass it to the sandboxed child, and require a
timing-safe match on every request in both _rpc_server_loop and
_rpc_poll_loop. Empty/missing/wrong token fails closed.

Salvaged from #44073 (per-session RPC token). Added timing-safe
secrets.compare_digest comparison and fail-closed regression tests.

Co-authored-by: Hermes Agent <agent@nousresearch.com>
This commit is contained in:
SahilRakhaiya05 2026-07-01 02:52:04 -07:00 committed by Teknium
parent 5de65624d1
commit 5178b3f056
2 changed files with 162 additions and 4 deletions

View file

@ -34,6 +34,7 @@ import json
import logging
import os
import platform
import secrets
import shlex
import socket
import subprocess
@ -381,7 +382,11 @@ def _connect():
def _call(tool_name, args):
"""Send a tool call to the parent process and return the parsed result."""
request = json.dumps({"tool": tool_name, "args": args}) + "\\n"
request = json.dumps({
"tool": tool_name,
"args": args,
"token": os.environ.get("HERMES_RPC_TOKEN", ""),
}) + "\\n"
with _call_lock:
conn = _connect()
conn.sendall(request.encode())
@ -434,7 +439,12 @@ def _call(tool_name, args):
# non-ASCII chars in tool args when encoding them as JSON.
tmp = req_file + ".tmp"
with open(tmp, "w", encoding="utf-8") as f:
json.dump({"tool": tool_name, "args": args, "seq": seq}, f)
json.dump({
"tool": tool_name,
"args": args,
"seq": seq,
"token": os.environ.get("HERMES_RPC_TOKEN", ""),
}, f)
os.rename(tmp, req_file)
# Wait for response with adaptive polling
@ -482,6 +492,7 @@ def _rpc_server_loop(
max_tool_calls: int,
allowed_tools: frozenset,
stop_event: threading.Event,
rpc_token: str,
):
"""
Accept one client connection and dispatch tool-call requests until
@ -527,6 +538,13 @@ def _rpc_server_loop(
conn.sendall((resp + "\n").encode())
continue
if not rpc_token or not secrets.compare_digest(
str(request.get("token") or ""), rpc_token
):
resp = json.dumps({"error": "Unauthorized RPC request"})
conn.sendall((resp + "\n").encode())
continue
tool_name = request.get("tool", "")
tool_args = request.get("args", {})
@ -750,6 +768,7 @@ def _rpc_poll_loop(
max_tool_calls: int,
allowed_tools: frozenset,
stop_event: threading.Event,
rpc_token: str,
):
"""Poll the remote filesystem for tool call requests and dispatch them.
@ -803,6 +822,13 @@ def _rpc_poll_loop(
env.execute(f"rm -f {quoted_req_file}", cwd="/", timeout=5)
continue
if not rpc_token or not secrets.compare_digest(
str(request.get("token") or ""), rpc_token
):
logger.debug("Unauthorized RPC request in %s", req_file)
env.execute(f"rm -f {quoted_req_file}", cwd="/", timeout=5)
continue
tool_name = request.get("tool", "")
tool_args = request.get("args", {})
seq = request.get("seq", 0)
@ -942,6 +968,8 @@ def _execute_remote(
f"mkdir -p {quoted_rpc_dir}", cwd="/", timeout=10,
)
rpc_token = secrets.token_urlsafe(32)
# Generate and ship files
tools_src = generate_hermes_tools_module(
list(sandbox_tools), transport="file",
@ -957,7 +985,7 @@ def _execute_remote(
args=(
env, f"{sandbox_dir}/rpc", effective_task_id,
tool_call_log, tool_call_counter, max_tool_calls,
sandbox_tools, stop_event,
sandbox_tools, stop_event, rpc_token,
),
daemon=True,
)
@ -966,6 +994,7 @@ def _execute_remote(
# Build environment variable prefix for the script
env_prefix = (
f"HERMES_RPC_DIR={shlex.quote(f'{sandbox_dir}/rpc')} "
f"HERMES_RPC_TOKEN={shlex.quote(rpc_token)} "
f"PYTHONDONTWRITEBYTECODE=1"
)
tz = os.getenv("HERMES_TIMEZONE", "").strip()
@ -1204,6 +1233,7 @@ def execute_code(
f.write(code)
# --- Start RPC server ---
rpc_token = secrets.token_urlsafe(32)
# Two transports:
# POSIX: AF_UNIX stream socket on sock_path, chmod 0600 for
# owner-only access. Filesystem permissions gate the socket.
@ -1230,7 +1260,7 @@ def execute_code(
target=propagate_context_to_thread(_rpc_server_loop),
args=(
server_sock, task_id, tool_call_log,
tool_call_counter, max_tool_calls, sandbox_tools, stop_event,
tool_call_counter, max_tool_calls, sandbox_tools, stop_event, rpc_token,
),
daemon=True,
)
@ -1248,6 +1278,7 @@ def execute_code(
# or spawn a subprocess. See ``_scrub_child_env`` for the rules.
child_env = _scrub_child_env(os.environ)
child_env["HERMES_RPC_SOCKET"] = rpc_endpoint
child_env["HERMES_RPC_TOKEN"] = rpc_token
child_env["PYTHONDONTWRITEBYTECODE"] = "1"
# Force UTF-8 for the child's stdio and default file encoding.
#