From ef1faa4cf810423513a8a5b6ff81afbcf215c363 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 4 Oct 2026 23:17:13 -0700 Subject: [PATCH] fix(approval): classic-CLI MCP/vault consent prompts reach the approval panel request_elicitation_consent's CLI branch called prompt_dangerous_approval without the agent thread's approval callback. Under prompt_toolkit that fails closed before any panel renders, so an untrusted MCP tool, MCP sampling or a vault card fill was auto-cancelled in hermes chat without the user ever being asked (and observers saw pre/post with no human wait). Pass the registered panel callback, as the protected-write prompt does. --- tests/tools/test_approval_plugin_hooks.py | 3 +-- tools/approval_prompt.py | 7 +++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/tools/test_approval_plugin_hooks.py b/tests/tools/test_approval_plugin_hooks.py index 605306f5cb..92f5bb96d7 100644 --- a/tests/tools/test_approval_plugin_hooks.py +++ b/tests/tools/test_approval_plugin_hooks.py @@ -378,9 +378,8 @@ class TestClassicCliPromptsFireGatewayTwinHooks: approval_module.unregister_gateway_notify(isolated_session) assert result == approved - # Classic CLI: the panel callback (protected write) or stdin (consent) answers "once". + # Classic CLI: the prompt_toolkit panel callback registered on the agent thread answers "once". monkeypatch.delenv("HERMES_GATEWAY_SESSION") - monkeypatch.setattr("builtins.input", lambda _prompt="": "o") set_approval_callback(lambda *_a, **_kw: "once") try: result, cli_hooks = _capture_hooks(run) diff --git a/tools/approval_prompt.py b/tools/approval_prompt.py index bb6915363f..decd2b18f2 100644 --- a/tools/approval_prompt.py +++ b/tools/approval_prompt.py @@ -326,7 +326,6 @@ def request_elicitation_consent(message: str, description: str, *, return "cancel" # nobody answered (timeout / prompt withdrawn) — not a user refusal return _consent(decision.get("choice"), "decline") - # allow_permanent=False: elicitation is a per-call confirmation — no pattern to remember. # Same observer payload as the gateway branch (#131876); post fires in a finally so the # wait settles for observers on the early fail-closed return too. hook_kwargs = dict(command=message, description=description, pattern_key="mcp_elicitation", @@ -334,8 +333,12 @@ def request_elicitation_consent(message: str, description: str, *, _ctx._fire_approval_hook("pre_approval_request", **hook_kwargs) hook_choice = "cancelled" try: + from tools.terminal_tool import _get_approval_callback + # allow_permanent=False: elicitation is a per-call confirmation — no pattern to remember. + # Pass the agent thread's panel callback: without it prompt_toolkit fails the prompt closed unseen. choice = prompt_dangerous_approval(message, description, timeout_seconds=timeout_seconds, - allow_permanent=False, title=title) + allow_permanent=False, title=title, + approval_callback=_get_approval_callback()) hook_choice = choice except Exception as exc: logger.error("Elicitation CLI prompt failed: %s", exc, exc_info=True)