Files
deepseek-harness/packages/approval/approval
kingwl 3f663c9154 feat(modes): per-session sandbox/approval switching — the session log as the store, ACP config options
effective(session) = findLast(the session own knob events)?.value ?? the
composition-config default. One log-only event per knob, owned by its
domain (bash/sandbox-mode in dsh-bash, approval/policy in dsh-approval),
each exporting the same three-piece kit: the event declaration, a pure
fold, and THE write path — a switch IS its event; no owner service, no
facts map. Restart immunity and multi-session isolation fall out of the
log replay by construction.

Execution follows the fold on both sides: the bash tool stamps
escalation grant > session override > executor default, and the approval
seam prepends the never-gate that auto-rejects before any interactive
answerer. Visibility is two layers per knob: a per-agent prompt section
states the effective value on every request (logged through
request/header*, so what-the-model-was-told replays from the log), and an
agent/pre-step narrator injects at most one coalesced delta notice with
positional attribution (user switch vs operator/config drift). The ACP
bridge advertises one capability-gated select per composable knob with
currentValue folded per session, validates set_config_option against the
closed vocabularies, and anchors idle switches at the next turn
prompt-submit under the turn-enclosure contract.
2026-07-10 15:44:38 +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.

The seam also owns the per-session POLICY tier (the sandbox RFC § Per-session mode switching): ApprovalPolicy is 'ask' (delegate to the answerers — the prior behavior exactly) or 'never' (deterministically reject without prompting anyone; the strict CI/unattended stance), with effective = fold(the session's 'approval/policy' events, last one wins) ?? Config.policy — the session log is the store, written only through setApprovalPolicy(session, policy). The service decides 'never' inside request() itself, before dispatching the waterfall ('never' → 'rejected' with the audit pair still landing; no listener registration, including a later prepend, can precede it), states 'never' — and only 'never' — in a per-agent prompt section (a "you will be prompted" promise under 'ask' would overclaim what a headless composition can do; the section scope activates only when systemPrompt is composed), and narrates a policy switch to the model in at most one coalesced agent/pre-step notice, attributed positionally (an override event after the log's last request/header* reads changed by the user; a config drift reads changed by the operator/config).

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 (the bash tool's sandbox_permissions gate in @deepseek-ai/dsh-tool-bash — the sandbox RFC § Escalation). The full design: the approval-seam RFC.

Answerers today: the ACP bridge (@deepseek-ai/dsh-acp) forwards to the editor's session/request_permission prompt for agents it owns. The audit events are log-only session records — the model only ever sees the tool result the asker derives from the outcome.