mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-03 12:23: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
|
|
@ -48,6 +48,47 @@ _approval_tool_call_id: contextvars.ContextVar[str] = contextvars.ContextVar(
|
|||
default="",
|
||||
)
|
||||
|
||||
# Interactive-CLI flag. Concurrent ACP sessions run on a shared
|
||||
# ThreadPoolExecutor (acp_adapter/server.py), so mutating the process-global
|
||||
# os.environ["HERMES_INTERACTIVE"] races: one session's restore in `finally`
|
||||
# can clobber another session's set mid-run, dropping it onto the
|
||||
# non-interactive auto-approve path so a dangerous command executes without
|
||||
# the approval callback firing (GHSA-96vc-wcxf-jjff). A contextvar is
|
||||
# thread/task-local, so each executor worker (or asyncio task) sees only its
|
||||
# own value. None = unset → fall back to the env var for legacy
|
||||
# single-threaded CLI callers that still export HERMES_INTERACTIVE.
|
||||
_hermes_interactive_ctx: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
||||
"hermes_interactive",
|
||||
default=None,
|
||||
)
|
||||
|
||||
|
||||
def set_hermes_interactive_context(interactive: bool) -> contextvars.Token:
|
||||
"""Bind interactive mode for the current context (thread or asyncio task).
|
||||
|
||||
Use this instead of mutating ``os.environ["HERMES_INTERACTIVE"]`` from
|
||||
concurrent executor threads. When unset (default), interactive detection
|
||||
falls back to the ``HERMES_INTERACTIVE`` env var for legacy callers.
|
||||
"""
|
||||
return _hermes_interactive_ctx.set("1" if interactive else "")
|
||||
|
||||
|
||||
def reset_hermes_interactive_context(token: contextvars.Token) -> None:
|
||||
"""Restore the prior value from :func:`set_hermes_interactive_context`."""
|
||||
_hermes_interactive_ctx.reset(token)
|
||||
|
||||
|
||||
def _is_interactive_cli() -> bool:
|
||||
"""True when running an interactive CLI/ACP session.
|
||||
|
||||
Prefers the context-local flag (set by concurrent ACP sessions) and falls
|
||||
back to the ``HERMES_INTERACTIVE`` env var for single-threaded callers.
|
||||
"""
|
||||
ctx_val = _hermes_interactive_ctx.get()
|
||||
if ctx_val is not None:
|
||||
return is_truthy_value(ctx_val)
|
||||
return env_var_enabled("HERMES_INTERACTIVE")
|
||||
|
||||
|
||||
def _fire_approval_hook(hook_name: str, **kwargs) -> None:
|
||||
"""Invoke a plugin lifecycle hook for the approval system.
|
||||
|
|
@ -1352,7 +1393,7 @@ def check_dangerous_command(command: str, env_type: str,
|
|||
if is_approved(session_key, pattern_key):
|
||||
return {"approved": True, "message": None}
|
||||
|
||||
is_cli = env_var_enabled("HERMES_INTERACTIVE")
|
||||
is_cli = _is_interactive_cli()
|
||||
is_gateway = _is_gateway_approval_context()
|
||||
|
||||
if not is_cli and not is_gateway:
|
||||
|
|
@ -1612,7 +1653,7 @@ def check_all_command_guards(command: str, env_type: str,
|
|||
if _command_matches_permanent_allowlist(command):
|
||||
return {"approved": True, "message": None}
|
||||
|
||||
is_cli = env_var_enabled("HERMES_INTERACTIVE")
|
||||
is_cli = _is_interactive_cli()
|
||||
is_gateway = _is_gateway_approval_context()
|
||||
is_ask = env_var_enabled("HERMES_EXEC_ASK")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue