fix(acp): thread-safe interactive approval via contextvars

Concurrent ACP sessions run on a shared ThreadPoolExecutor (max_workers=4).
Each _run_agent mutated the process-global os.environ["HERMES_INTERACTIVE"]
and restored it in finally, so one session's restore could clobber another's
set mid-run — dropping the second session onto the non-interactive
auto-approve path, executing a dangerous command without the approval
callback firing (GHSA-96vc-wcxf-jjff).

Replace the env-var flag with a thread/task-local contextvar in
tools.approval. The two HERMES_INTERACTIVE read sites in approval.py now go
through _is_interactive_cli() (contextvar-first, env fallback for legacy
single-threaded CLI callers). The ACP executor sets the contextvar instead
of os.environ; the existing contextvars.copy_context() wrapper isolates each
session's write.

Co-authored-by: Hermes Agent <127238744+teknium1@users.noreply.github.com>
This commit is contained in:
georgex8001 2026-06-30 03:12:10 -07:00 committed by Teknium
parent f5eb4c307b
commit 62b9fb6623
3 changed files with 106 additions and 16 deletions

View file

@ -241,3 +241,46 @@ class TestAcpExecAskGate:
"GHSA-96vc-wcxf-jjff"
)
assert result["approved"] is True
def test_interactive_context_var_routes_to_callback_without_env(
self, monkeypatch,
):
"""Context-local interactive flag must work without touching os.environ.
Concurrent ACP sessions run on a shared ThreadPoolExecutor, so the
interactive flag is now a contextvar instead of a process-global env
var one session can no longer clobber another's flag mid-run
(GHSA-96vc-wcxf-jjff).
"""
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
monkeypatch.delenv("HERMES_YOLO_MODE", raising=False)
from tools.approval import (
check_all_command_guards,
reset_hermes_interactive_context,
set_hermes_interactive_context,
)
called_with = []
def fake_cb(command, description, *, allow_permanent=True):
called_with.append((command, description))
return "once"
tok = set_hermes_interactive_context(True)
try:
result = check_all_command_guards(
"rm -rf /tmp/test-context-interactive",
"local",
approval_callback=fake_cb,
)
finally:
reset_hermes_interactive_context(tok)
assert called_with, (
"set_hermes_interactive_context(True) should route dangerous "
"commands through the callback without HERMES_INTERACTIVE in env"
)
assert result["approved"] is True