Expand description
P5-4’s three load-bearing interactive handlers — the THIS-MODULE side of
the three deferred chains P5-1/P5-2/P5-3 each explicitly left for tui:
TuiApprovalHandlerimplementscrate::permissions::PermissionsApprovalHandler(P5-1’s seam): installed viacrate::agent::Agent::set_permissions_approval_handler.TuiElicitationHandlerimplementscrate::mcp::McpElicitationHandler(P5-2’s seam): installed viacrate::mcp::McpClient::set_elicitation_handler.TuiChildApprovalHandler— also aPermissionsApprovalHandler, installed via the factorycrate::agent::Agent::set_child_approval_handler_factory(P5-4’s OWN new seam, added specifically to close P5-3’s §2.2 C6 “queued, never-blocking” chain into a genuinely answerable one).
All three follow the same shape: push a Pending* request (from
crate::tui::bridge) onto a channel the render loop polls, then block
(synchronously for the two PermissionsApprovalHandlers — ask is a
sync trait method by design, see that trait’s doc comment; via .await
for the async McpElicitationHandler) for the reply. If the reply
channel is ever dropped without a reply (the render loop panicked, or
the TUI process is shutting down mid-request), every handler here
resolves to the FAIL-CLOSED outcome (Deny/decline) — never a hang,
and never an implicit allow.
Security invariant (repeated at each handler below). None of these
handlers can escalate what the permissions/elicitation engines already
decided: crate::permissions::approval::resolve_ask only ever calls a
PermissionsApprovalHandler::ask when the rule engine already resolved
the call to Ask (Deny short-circuits before any handler runs;
Allow never needs one) — a TUI “allow” here can only grant what the
policy already routed to a human prompt. Likewise an elicitation answer
is exactly what the user typed into the modal — never fabricated,
never auto-accepted.
Structs§
- TuiApproval
Handler - P5-1’s interactive ask-UI, closing the deferred chain
crate::permissions::approval’s module doc comment names.askpushes the request ontotxand blocks on a fresh one-shot reply channel;Self::txbeing closed (the render loop is gone) makes the blockingrecv()return anErr, which resolves toApprovalOutcome::Deny— fail-closed, matching the trait’s own “no handler ⇒ deny” default posture for the “handler installed but unreachable” case too. - TuiBridge
- The aggregate wiring point a
crates/cliembedder uses: constructs every channel pair once, installs the SENDING halves onto theAgent/McpClients that need them, and hands the RECEIVING halves to the render loop to poll each frame. Seecrate::tui::should_activate’s doc comment for why this is only ever built when the TUI is confirmed active — installing these on an agent that no render loop is draining would hang the firstAsk-tier prompt or elicitation forever. - TuiChild
Approval Handler - P5-3’s answerable child-approval handler, closing the §2.2 C6 deferred
chain — see
crate::agent::Agent::set_child_approval_handler_factory’s doc comment for how this REPLACES (only when installed) the default never-blockingcrate::subagents::ParentQueueApprovalHandler. Also records every request into the sharedqueue(the SAMEAgent::pending_child_approvalsaudit trailParentQueueApprovalHandleritself writes to), socrate::agent::Agent::pending_child_approvalsstays a complete audit log regardless of which handler answered a given request. - TuiElicitation
Handler - P5-2’s interactive elicitation UI, closing the deferred chain
crate::mcp::HeadlessElicitationHandler’s doc comment names.handleis ASYNC (the MCP trait’s own shape), so this uses atokio::sync:: oneshotreply rather than blocking a thread — awaiting it yields the executor to other work while the modal is up. A dropped reply sender (render loop gone) resolves tocrate::mcp::ElicitationAction::Cancel(the MCP spec’s own “dismissed without a decision” outcome — the honest shape for “nobody answered”, distinct from an explicitDecline).