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.
This commit is contained in:
Teknium
2026-10-04 23:17:13 -07:00
parent 6ce6c1aa55
commit ef1faa4cf8
2 changed files with 6 additions and 4 deletions

View File

@@ -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)

View File

@@ -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)