ctx.approval (dsh-approval): request() dispatches the approval/request waterfall and always resolves a closed outcome — allowed-once / rejected / cancelled / unavailable — never rejects; zero listeners fall through to fail-closed unavailable; abort settles cancelled and discards late answers; throwing or rogue answerers are contained as unavailable; every ask lands the log-only approval/asked / approval/decided audit pair. dsh-tools routes a pre-execute ask through the seam opportunistically (ctx.get) with three distinct deny reasons, keeping the historical ask→deny degrade when the seam is absent. The per-session policy tier, the ACP bridge answerer, and the sandbox escalation asker are staged follow-ups of the approval-seam RFC.
@deepseek-ai/dsh-approval
Approval seam. Owns the ctx.approval service (ApprovalService) and the one-shot permission vocabulary the harness shares: ApprovalRequest (agent + tool identity + reason + abort signal), the closed ApprovalOutcome union (allowed-once / rejected / cancelled / unavailable), the ApprovalRequestId brand pairing the two log-only audit events (approval/asked / approval/decided), and the approval/request waterfall the answerers listen on. Depends only on cordis and the core vocabulary packages (agent, session, llm brand), never on any UI.
The contract in one line: ctx.approval.request(req) puts exactly one question — "may this specific action proceed?" — to whatever answerers the deployment composed, and always resolves to an outcome, never rejects: an aborted signal yields cancelled, a throwing or missing answerer yields unavailable, and allowed-once is a grant for the single asked-about action, never a class of future ones. The one precondition: ask from inside an open turn — the audit pair is turn-enclosed by contract (the turn is the durable log's commit/replay boundary; a bare event between turns is crash-tail garbage on reload), so an idle ask throws before appending anything.
The service is the mechanism, answerers are the policy. Answerers are approval/request waterfall listeners occupying a single decision slot: answer for an agent you own by returning an outcome without calling next(), or delegate an agent you don't recognize by calling next() — the chain's built-in default is unavailable, so a deployment with no answerer (headless, CI) fails closed with zero configuration. Registration order across sibling plugins is not load-order deterministic; compose one terminal answerer per deployment and use prepend listeners only for decide-or-delegate gates.
One seam serves both ask paths of the sandbox RFC: the tools/pre-execute ask decision (routed by @deepseek-ai/dsh-tools when this service is mounted; degrading to deny when it is not), and the sandbox post-denial escalated retry that RFC stages (the asker will live in the bash tool layer). The full design: the approval-seam RFC.
No answerer ships in this change — every ask fails closed to unavailable until one is composed (the ACP bridge answerer is the staged first one). The audit events are log-only session records — the model only ever sees the tool result the asker derives from the outcome.