Files
deepseek-harness/packages/approval/approval
kingwl ef35007d75 feat(approval): the approval seam — one-shot permission decisions over a waterfall of answerers
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.
2026-07-10 15:43:02 +08:00
..

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