mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-04 12:33:08 +00:00
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:
parent
f5eb4c307b
commit
62b9fb6623
3 changed files with 106 additions and 16 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue