Files
wehub-resource-sync b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
Build Skills Index / trigger-deploy (push) Waiting to run
CI / Deny unrelated histories (push) Has been skipped
CI / CI timing report (push) Blocked by required conditions
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / All required checks pass (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 11:56:03 +08:00

121 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Propagate agent-turn context into worker threads that dispatch Hermes tools.
A bare ``threading.Thread`` / ``ThreadPoolExecutor`` worker starts with an
empty ``contextvars.Context`` and no thread-local approval/sudo callbacks.
Tool dispatch inside such a thread therefore silently loses:
* the approval *session/platform* ContextVars (``tools.approval`` /
``gateway.session_context``) — so gateway sessions fall into
``check_dangerous_command``'s non-interactive auto-approve branch and
dangerous commands run without prompting (#33057, #30882);
* the thread-local CLI approval/sudo callbacks (``tools.terminal_tool``) —
so ``prompt_dangerous_approval`` cannot reach the user
(GHSA-qg5c-hvr5-hjgr, #15216).
This helper factors out that capture/install/clear lifecycle so the several
places that fan tool dispatch onto worker threads (``agent.tool_executor`` and
the ``execute_code`` RPC threads) share one audited implementation instead of
divergent copies.
Usage — call :func:`propagate_context_to_thread` **on the parent thread**
(it snapshots the parent's ContextVars and callbacks at call time) and use the
returned callable as the worker's target::
t = threading.Thread(target=propagate_context_to_thread(loop_fn), args=(...))
# or
executor.submit(propagate_context_to_thread(worker_fn), *args)
Approval/sudo callbacks are installed for the worker's lifetime and **always
cleared on exit**, so a recycled thread never holds a stale reference to a
disposed CLI instance.
"""
from __future__ import annotations
import contextvars
import logging
from typing import Callable
logger = logging.getLogger(__name__)
def _callback_api():
"""Resolve the terminal_tool callback getters/setters.
Imported lazily: ``tools.terminal_tool`` imports ``tools.approval`` at
module load, so a top-level import here would risk an import cycle for
callers that live in ``tools.approval``.
"""
from tools.terminal_tool import (
_get_approval_callback,
_get_sudo_password_callback,
set_approval_callback,
set_sudo_password_callback,
)
return (
_get_approval_callback,
_get_sudo_password_callback,
set_approval_callback,
set_sudo_password_callback,
)
def propagate_context_to_thread(target: Callable) -> Callable:
"""Wrap *target* for execution on a worker thread with the *current*
thread's ContextVars and approval/sudo callbacks propagated.
Call this on the parent thread; pass the returned callable as the
thread/executor target. The returned callable forwards its positional
and keyword arguments to *target* and returns its result.
Fail-closed: if callback installation raises, the callbacks are left
unset (``None``). That is the safe outcome — ``prompt_dangerous_approval``
denies dangerous commands when no callback is registered in an interactive
context, and the gateway approval queue blocks when its notify callback is
absent.
"""
ctx = contextvars.copy_context()
parent_approval_cb = parent_sudo_cb = None
setters = None
try:
get_approval, get_sudo, set_approval, set_sudo = _callback_api()
parent_approval_cb = get_approval()
parent_sudo_cb = get_sudo()
setters = (set_approval, set_sudo)
except Exception:
logger.debug("Could not capture parent approval/sudo callbacks", exc_info=True)
def _runner(*args, **kwargs):
def _inner():
if setters is not None:
set_approval, set_sudo = setters
try:
if parent_approval_cb is not None:
set_approval(parent_approval_cb)
if parent_sudo_cb is not None:
set_sudo(parent_sudo_cb)
except Exception:
logger.debug(
"Failed to install propagated approval/sudo callbacks; "
"dangerous-command approval will fail closed",
exc_info=True,
)
try:
return target(*args, **kwargs)
finally:
if setters is not None:
set_approval, set_sudo = setters
try:
set_approval(None)
set_sudo(None)
except Exception:
logger.debug(
"Failed to clear propagated approval/sudo callbacks",
exc_info=True,
)
return ctx.run(_inner)
return _runner