Files
deepseek-harness/packages/ui/user-approval
Tianyi Cui 56e34619d7 Merge remote-tracking branch 'origin/master' into codex/pr224-rfc-rewrite
# Conflicts:
#	docs/architecture.md
#	docs/capability-seams.md
#	docs/config-catalog.md
#	docs/cookbook/extension-cookbook.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	docs/rfc/implemented/feature/2026-06-30-interception-seams.md
#	docs/rfc/proposed/feature/2026-06-14-acp-agent-client-protocol.md
#	docs/tool-execution-pipeline.md
#	packages/cordis/tool-cordis/src/api-catalog.ts
#	packages/core/agent-core/README.md
#	packages/core/agent-loop/README.md
#	packages/core/tools/README.md
#	packages/core/tools/src/index.ts
#	packages/core/tools/tests/tools.spec.ts
#	packages/core/tools/tsconfig.json
#	packages/ui/acp/src/index.ts
#	scripts/doc-budgets.manifest.json
#	scripts/gen-cordis-catalog.ts
#	scripts/gen-doc-graphs.ts
2026-07-11 23:14:09 +08:00
..

@deepseek-ai/dsh-user-approval

User-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. It lives in the UI group because its purpose is human permission, while remaining channel-neutral: it depends only on Cordis and core vocabulary packages, never on a concrete 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. Dispatch is keyed by req.agent: a listener registered through agent.ctx receives only that agent's questions, while a plain-context listener receives every agent's. 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) 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 prose — in a per-agent prompt section, records either value with a source-owned header marker, and narrates a policy switch to the model in at most one coalesced agent/pre-step notice. The restart fallback reads the marker rather than deployment-controlled persona prose; attribution is positional (an override event after the last request/header* reads changed by the user, otherwise 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-bashthe 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.