Flow: permission request
What happens between "the agent wants to run a command" and "you tapped Allow". This flow is the safety critical one: its invariant is that a permission request always terminates — approved, denied, or cancelled — nev...
What happens between "the agent wants to run a command" and "you tapped Allow". This flow is the safety-critical one: its invariant is that a permission request always terminates — approved, denied, or cancelled — never silently dropped.
Hop by hop
agent ──ACP requestPermission──► bridge handler
│ register oneshot (request_id)
▼
PermissionRequest output ──► plugin ──► card in chat
│ tap
agent ◄──ACP response── bridge handler ◄──oneshot── forwarder ◄── callback with request_id1. Agent asks. Mid-turn, the agent CLI sends ACP session/request_permission with the options (allow once, always, reject…). The agent's turn is now blocked on the reply.
→ src/core/src/agent/runtime.rs (client handler trait)
2. Bridge handler registers a oneshot. The thread's ChannelBridgeHandler generates a fresh request_id, stores (channel_kind, oneshot::Sender) in PluginHost::pending_permissions, and emits ChannelOutput::PermissionRequest { request_id, payload } to the routes attached to the thread. There is deliberately no timeout — the UX contract is "the user takes as long as they need".
→ src/core/src/channels/bridge_handler.rs (request_permission), plugin_host.rs (pending_permissions)
3. Card renders. The plugin turns the payload into a platform-native interactive card (Feishu V2 card, Slack block actions, Telegram inline keyboard). Permission requests are outbox-durable: if the plugin is down, the card is delivered after respawn. On the web, the chat renders the card component and tracks it as pending.
→ plugin repos; src/core/src/channels/outbox.rs; src/server/src/web_server/ws_chat.rs (web cards)
4. The tap comes back. Two return paths into the same table:
- Stdio plugins: the tap arrives as an ACP response through the plugin bridge's forwarder, which pops
pending_permissions[request_id]and fires the oneshot. - Web chat: the browser sends a typed
PermissionResponseover/ws/chat; the handler callsrespond_permission(channel_kind, request_id, response), which validates that the request belongs to that channel before firing. →src/core/src/channels/transport_stdio/(forwarder),plugin_host.rs(respond_permission)
5. Agent resumes. The bridge handler's rx.await completes with the chosen option and returns it as the ACP response. The agent continues (or aborts) the tool call accordingly.
Why it can't hang forever
The no-timeout design needs cleanup guarantees instead:
| Situation | Guarantee |
|---|---|
| Plugin process dies while a card is pending | The dying bridge calls cancel_channel_permissions(kind) exactly once — pending senders are dropped, rx.await errors, the handler answers the agent with Cancelled |
| Daemon shutdown | PluginHost::shutdown_all clears the whole table first, same cancellation path |
| Tap for an already-resolved request | respond_permission finds nothing and returns "no longer pending" — the second tap is a no-op, not a double-approve |
| Tap from the wrong channel | Channel-kind check re-inserts the entry and rejects the response |
The net invariant: every registered oneshot is consumed exactly once — by the tap, by channel cancellation, or by shutdown. An agent turn can wait indefinitely on a human, but never on a dead process.
Known gap: the pending entry is tagged with the first attached route's channel kind only, while the card fans out to every attached route — on a multi-surface thread (handover), taps from the other surfaces are rejected by the channel check, and the wrong-channel re-insert can race the plugin-death drain. Tracked as H13 in the remediation plan.
Source anchors: src/core/src/channels/bridge_handler.rs (request_permission), src/core/src/channels/plugin_host.rs (pending_permissions, respond_permission, cancel_channel_permissions, shutdown_all), src/core/src/channels/transport_stdio/ (forwarder), src/core/src/channels/outbox.rs (durability), src/server/src/web_server/ws_chat.rs (web response path).
Last verified: v0.7.11
Flow: PTY terminal
The web terminal path: a browser xterm talking to a real pseudo terminal on your machine. This flow is intentionally dumb in the middle — bytes in, bytes out — with the intelligence at the edges (xterm.js rendering, P...
Flow: bridge request
One model API call from a launched agent CLI, followed through the local bridge to the provider and back. Concepts in Local API and bridge; this page is the request path itself.