mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-09 13:21:42 +00:00
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:
parent
5de65624d1
commit
5178b3f056
2 changed files with 162 additions and 4 deletions
|
|
@ -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.
|
||||
#
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue