fix(approval): classify CLI/TUI approval timeouts separately from explicit denials

When an approval prompt expired without a response, every CLI-side path
collapsed the timeout into the same 'deny' choice as an explicit user
refusal, so the agent was told the user denied the action when the user
simply never answered. The gateway wait already distinguished the two
('timed out without user response... Silence is not consent.'); this
brings the CLI/TUI/ACP surfaces to parity.

- prompt_dangerous_approval(): input()-path expiry now returns a distinct
  'timeout' choice (still fail-closed).
- cli.py _approval_callback + hermes_cli/callbacks.py approval_callback:
  deadline expiry returns 'timeout' instead of 'deny'.
- check_all_command_guards / _run_approval_gate CLI tails: 'timeout' maps
  to outcome='timeout' with a 'timed out without user response... Silence
  is not consent.' BLOCKED message (matching the gateway wording);
  explicit deny keeps outcome='denied' and gains user_consent=False for
  shape parity.
- computer_use: 'timeout' verdict threads through the CLI adapter and
  yields a 'prompt timed out — the user did not respond' error instead of
  'denied by user'.
- ACP permissions bridge: FutureTimeout returns 'timeout' (other failures
  still 'deny'); elicitation maps 'timeout' to 'cancel' like the gateway's
  unresolved outcome; codex wire mapping documents deny/timeout→decline.
- write_approval already treats unknown choices as 'stage, not drop', so
  a timeout now stages the memory write instead of silently refusing it.

Every timeout path remains fail-closed — the action never runs; only the
classification reported to the agent changes.
This commit is contained in:
Teknium
2026-08-02 18:51:19 -07:00
parent f4604f89de
commit aac74be2f1
8 changed files with 183 additions and 8 deletions
+5 -1
View File
@@ -13229,7 +13229,10 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self._approval_deadline = 0
self._paint_now()
_cprint(f"\n{_DIM} ⏱ Timeout — denying command{_RST}")
return "deny"
self._persist_prompt_summary(
"⚠", "Approval", command, "timed out (no response)",
)
return "timeout"
def _approval_choices(self, command: str, *, allow_permanent: bool = True,
smart_denied: bool = False) -> list[str]:
@@ -13261,6 +13264,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
"session": "approve_session",
"always": "always_approve",
"deny": "deny",
"timeout": "timeout",
}.get(verdict, "deny")
def _handle_approval_selection(self) -> None: