DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Tool System·Chapter 19

Permission Analysis, Approval, and Monotonic Guards

Who proposes risk, who may permit, and who may only deny

VerifiedUpstream 47f943859bScope: Analyze pre-execute policy, approval waterfalls, fail-closed absence, monotonic guards, presets, and per-call authorization.

Conclusion: Authorization Is Layered, and Only the Final Vetoes Are Monotonic

DeepSeek Harness does not have one central “risk engine.” At the pinned baseline, risk is proposed in two places: an extensible tools/pre-execute listener may return ask, and a sandbox-aware shell (Bash/PowerShell) or filesystem call may request a strictly wider mode in its own frozen arguments. A durable permission preset supplies standing sandbox and prompt policy; an approval answer grants at most one call; and synchronous guards retain a final, irreversible veto. These are intentionally different powers.

Core security property

The registry detaches, snapshots, and deep-freezes parsed arguments before policy. Pre, guards, and the tool body share that immutable graph, while approval correlates through callId to an earlier durable call record: Native tool/call keeps the model's raw JSON, while Code Mode tool/code-dispatch-start keeps normalized JSON. A pre decision can allow, deny, or ask but cannot rewrite arguments. Approval never bypasses guards: runtime checks global and scoped layers in order until the first denial or universal abstention, and guards have no allow result. Policy judgment and execution therefore retain argument identity, while listener ordering cannot undo an owner's veto. APPROVAL-EXEC-SNAPSHOTAPPROVAL-PRE-DECISIONAPPROVAL-PREPARE-ORDERAPPROVAL-GUARD-CONTRACT

1. Six Concepts That Similar Product Labels Tend to Collapse

ConceptLifetimeAuthorityDurable?
Tool visibility restrictionLive scope compositionRemoves inherited schemas; it is capability presentation, not execution authorizationNo
Sandbox modeDeployment default or one Session overrideConstrains file effects through an enforcing backendSession override: yes
Approval policyDeployment default or one Session overrideask routes to answerers; never rejects every askSession override: yes
Permission presetStanding Session selectionBundles sandbox mode and approval policyYes
Approval outcomeOne requestOnly allowed-once grants; it does not alter standing policyAudit pair: yes
Monotonic guardLive registration, evaluated per callFinal synchronous deny-only vetoNo; the resulting tool error is logged by the caller

The most counterintuitive row is approval/policy = never: it means “never prompt; reject actions that require approval,” not “approve without asking.” A full-access preset works without prompts because the sandbox is already open, not because never is a grant.

2. The End-to-End Decision Graph Has Two Approval Entrances

model tool call
  → durable tool/call already exists
  → detach + freeze arguments
  → tools/pre-execute waterfall ── deny ──────────────→ error result
              │
              ├─ ask → ApprovalService → allowed-once / deny
              │
              └─ allow
                   ↓
             global + scoped monotonic guards ───────→ error result
                   ↓
             around wrappers → tool body
                                  │
                                  └─ shell (bash/pwsh) or fs sandbox_permissions
                                     → strict-wider check
                                     → ApprovalService
                                     → stamp wider mode on this call only
                                  ↓
                           effectful backend execution
                                  ↓
                         post-policy → durable result

The generic entrance asks before guards and before the body. The sandbox entrance lives inside a shell (bash/pwsh) or fs body, but still before a process starts or a mutation executes. Consequently, a generic pre listener and a tool-specific escalation request can both ask about one call; composition should avoid redundant policy. Conversely, guards can reject the frozen escalation arguments before the tool-specific prompt is reached.

3. Risk Is Proposed, Not Centrally Scored

PreToolDecision contains only allow, deny(reason), and ask(reason?). There is no built-in severity scale, numerical risk score, command classifier, or privileged “approve” token. The default end of the waterfall is allow, so deployments that want broad risk classification must compose a listener; the built-in sandbox-aware tools instead use an explicit argument pair: sandbox_permissions plus a nonblank justification.

What the model may propose

The bash guidance constrains escalation to one exact retry after a real sandbox denial, asks for the narrowest wider mode, and says not to retry after rejection. Those instructions improve behavior, but the enforcement boundary is stricter: argument pairing, the runtime wider-mode table, approval outcome, and the sandbox backend. Prompt text is not counted as authority.

4. Why tools/pre-execute Is Extensible Policy, Not a Non-bypassable Gate

The pre stage is an asynchronous, scope-routed waterfall. A listener can delegate to next, replace the downstream decision, or short-circuit it. Registration order and prepend therefore matter. That is useful for hooks, deployment policy, and machine reviewers, but it also means a listener-shaped “absolute deny” can be bypassed by an earlier listener that returns allow without delegating.

The separate guard registry closes that hole

ctx.tools.guard() registers synchronous deny-only checks. The runtime evaluates the global layer first and then every applicable Agent-scope layer from farthest to nearest; the first reason wins. No guard can return allow, and no pre listener runs after guards. APPROVAL-GUARD-REGISTRY

5. The Approval Service Is a Closed, Fail-closed Question Boundary

OutcomeMeaningTool consequence
allowed-onceOne answerer grants this requestThe generic gate becomes allow, or the requested wider mode is stamped onto this call
rejectedExplicit refusal, or deterministic never policyDeny
cancelledThe request was withdrawnDeny/abort; a late answer is discarded
unavailableNo answerer, a throwing answerer, or an invalid answerDeny
Absence never elevates

The vocabulary is a closed union. Before dispatch, never returns rejected inside the service itself, so even a prepended answerer cannot override it. Under ask, the answerer waterfall defaults to unavailable; synchronous throws, asynchronous rejection, and rogue return values normalize to unavailable. APPROVAL-OUTCOMESAPPROVAL-DECIDE-WATERFALL

At the ToolRuntime seam, a missing ApprovalService and an Agent-less call also deny because there is no auditable Session and no routing identity. Only the exact allowed-once branch proceeds; rejected, cancelled, and unavailable preserve distinct messages for diagnosis. APPROVAL-ASK-MAPPING

6. The Audit Pair Is Durable, but the Live Wait Is Not

open turn
  → append approval/asked { fresh id, toolName, callId?, reason? }
  → await answerer waterfall
  → append approval/decided { same id, closed outcome }
  → return outcome

An approval request is accepted only inside an open turn, because the turn is the Session log's commit/replay boundary. The request deliberately omits arguments: callId links it to the already-recorded and already-presented tool call, avoiding two copies that could drift. Both audit events are log-only rather than model transcript blocks. The model learns standing policy from runtime context and receives the final tool result. APPROVAL-AUDIT-EVENTSAPPROVAL-REQUEST-SHAPEAPPROVAL-AUDIT-REQUEST

Replay boundary

Replaying the log reconstructs past asks, outcomes, and the latest standing policy. It does not resurrect an in-flight JavaScript Promise after a host-process restart. The invariant permits an unmatched approval/asked crash tail, while rejecting an orphan decision, duplicate open id, out-of-turn event, or unknown vocabulary. Browser reconnect recovery is a separate, same-process pending registry described below. APPROVAL-INVARIANT

7. Cancellation Has Explicit Race Semantics

If the request signal is already aborted, approval resolves cancelled before answerer dispatch. Otherwise the service races the answerer against the signal; cancellation settles immediately and any later answer becomes a no-op. The answerer computation is contained, not forcibly killed.

Tool preparation checks caller cancellation before policy, after approval, and again before dispatch. If cancellation is what cancelled the approval, the result is ABORTED_BEFORE_DISPATCH; a completed policy denial may remain the more informative result. Once a same-process tool body starts, ToolRuntime waits for it to reach quiescence and then converts an otherwise successful outcome to aborted. Tool authors must propagate the signal to subprocesses or providers. APPROVAL-CANCEL-DISPATCH

8. Presets Are Durable Standing Intent Over Two Independent Knobs

A preset table entry contains a machine key, sandbox mode, approval policy, and optional presentation text. custom is reserved for the derived state in which the two effective knobs match no entry; it cannot be configured or selected. The service fails at load when the shell does not advertise confinement, the default does not resolve, or the composed defaults match no preset without an explicit default. APPROVAL-PRESET-CONFIG

EventRoleControls execution directly?
permission/presetRecords selected user intent and disambiguates equal bundlesNo
sandbox/modeStanding sandbox overrideYes, folded for each confined call
approval/policyStanding prompt behaviorYes, checked for each ask

Current value is derived from actual knob state. A still-matching recorded selection wins when two names share a bundle; otherwise the first matching table row wins, then custom. Selecting a preset appends intent first and only writes knobs whose effective values change. APPROVAL-PRESET-DERIVEAPPROVAL-PRESET-WRITE

9. “Default for Future Sessions” and “Current Session” Are Separate Mutations

A genuinely fresh Session reads the latest user setting and pins all three permission facts during synchronous session/created publication. This occurs before creation returns, Agent publication, and the first prompt, but not before SessionStore insertion. A seeded, resumed, or partially initialized Session preserves its effective permission and receives only missing facts. Later settings changes therefore affect future Sessions, not existing ones. APPROVAL-PRESET-PIN

The optional permissions projection exposes table options plus the effective current value; absence of the projection key means the capability was not composed, so clients hide the control. The bare /permission command reports current and available values; /permission <key> validates the key and uses the same service write path as the UI. APPROVAL-PRESET-PROJECTION

Shipped composition, not library default

The base bundle ships three rows: read-only + ask, workspace-write + ask, and danger-full-access + never. Its environment-derived default is normally workspace-write + ask. The permission service's standalone schema default has only the latter two rows; those two facts should not be conflated. APPROVAL-SHIPPED-PRESETS

10. A Per-call Sandbox Grant Is Strictly Wider, Explicit, and Ephemeral

Effective modeLegal one-call targets
read-onlyworkspace-write, danger-full-access
workspace-writedanger-full-access
danger-full-accessNone

The schema advertises the closed target vocabulary, but runtime checks strict widening against this call's effective standing mode. Both escalation fields must occur together and the justification must be nonblank. A same-level or narrowing request fails before prompting. APPROVAL-ESCALATION-LADDER

The resolver then requires an ApprovalService and Agent, records the target and justification in the reason, and accepts only allowed-once. The returned mode is a local value for one call; it never appends sandbox/mode or changes the preset. APPROVAL-ESCALATION-FLOW

Precedence

At execution, an approved explicit mode outranks the Session's last sandbox/mode, which outranks the deployment default. The Session cwd supplies the workspace root. APPROVAL-SANDBOX-PRECEDENCE

11. Shell and Filesystem Share Choreography, Then Enforce Through Different Backends

Bash and PowerShell both validate arguments, resolve standing policy, await escalation approval when requested, stamp the returned mode onto the request, and only then start a foreground process or commit a background-job starter. Unadvertised escalation keys are still rejected when the mounted executor cannot sandbox, because the object schema is open and presentation alone is not enforcement. APPROVAL-BASH-EXECUTIONAPPROVAL-PWSH-EXECUTION

Filesystem mutation tools use a shared controller that performs the same pairing, standing-policy resolution, approval, and one-call stamp. A provider sandbox denial is converted to a structured error marker plus an exact-retry hint; other provider errors pass through. APPROVAL-FS-ESCALATION

12. Delegated Children Do Not Inherit a Parent's One-shot Grant

At child creation, the runtime synchronously captures only the parent's explicit Session sandbox override—never the deployment default and never a per-call grant—and pins the child's approval policy to never whenever approval exists. Those delegation-sourced events are appended after any fork seed, so stale seed state cannot reopen the child. The child also receives model-facing text saying that approval-requiring operations are rejected. APPROVAL-CHILD-PIN

Authority consequence

A parent can delegate work within the child's fixed sandbox scope, but the child model cannot widen itself by asking. A later trusted runtime/user switch can append newer policy events; this is not a cryptographic forever-lock. The boundary prevents model-originated escalation through the normal approval path, not all higher-authority administration.

13. Web Approval Is a Stable Server Request, Not a New Unary RPC

The host emits approval/requested with a stable rpcId, Session id, approval id, tool name, optional call id, and reason. A client response echoes the same rpcId and may supply only allowed-once or rejected; cancellation and unavailability remain host-owned outcomes. APPROVAL-WIRE-CONTRACT

The proxy answerer claims the newest matching, undecided, unclaimed approval/asked event. If it cannot find that audit fact, it delegates instead of inventing an answerable request. Pending entries survive browser disconnects; opening a new mux replays them with the same id. Abort and proxy teardown settle them cancelled. APPROVAL-WEB-REGISTRYAPPROVAL-WEB-REPLAY

Responses are routed by rpcId, parsed, and required to echo the exact Session and approval ids. The first valid response removes the pending entry; malformed, foreign, stale, and late responses are rejected. APPROVAL-WEB-RESPOND

14. The Approval Panel Is One-shot, but Its Evidence Is Not Self-contained

While a request is pending, the conversation UI replaces the composer with a scrollable card containing the reason, optional command, Refuse, and Allow once. Buttons latch after the first click, re-arm on transport failure, and disappear when the resolved frame arrives. The command is parsed only for a root running tool call whose JSON arguments contain a string command; malformed, nested, unpaired, and non-shell calls show no command line. APPROVAL-UI-PANEL

Consent tradeoff

Not duplicating arguments in ApprovalRequest prevents audit drift, but it makes the existing tool-call presentation part of informed consent. The panel itself may show only a free-text reason and tool name for filesystem or other generic asks. A stronger product surface could render a typed, immutable summary from the already-recorded call without making a second authoritative copy.

15. Permission Selectors Are Convenience and Acknowledgement, Not the Enforcement Boundary

The current-Session selector filters out custom and submits /permission <key>. The optional command popup does the same. The Settings row writes defaultPreset with an expected revision for future Sessions. APPROVAL-PERMISSION-SETTINGS-WRITE All three consume server-advertised options, but their risk dialog is triggered by one hard-coded machine key: danger-full-access. APPROVAL-PERMISSION-COMPOSERAPPROVAL-PERMISSION-DECORATIONAPPROVAL-PERMISSION-SETTINGS

Confirmed static mismatch

The server allows arbitrary preset names and arbitrary sandbox/approval bundles. Therefore a differently named preset that maps to danger-full-access + never does not trigger this confirmation, while a harmless bundle named danger-full-access does. A direct command or Settings API mutation also has no checkbox. The dialog is a useful UX warning for the shipped table, but it is not a generic authorization check. Hardening should derive risk from the advertised bundle or a server-owned risk field, and server policy—not a key name—must remain authoritative. APPROVAL-PRESET-CONFIG

16. Browser Request Trust Defends Origin Confusion; It Is Explicitly Not Authentication

Every /api request must pass Host validation, explicit cross-site Fetch Metadata rejection, and same-authority Origin validation when Origin exists. This blocks DNS rebinding and ordinary malicious-page cross-site requests. The source explicitly leaves network reachability and authentication out of scope. APPROVAL-WEB-TRUST

Deployment boundary

Any client that can legitimately reach the local API through an accepted authority can observe the mux and race to answer a pending approval; the first exact valid response wins. That matches the documented single-user local-service assumption. A carrier exposed to mutually untrusted users would need authenticated principal-to-Session authorization in addition to the Host/Origin fence.

17. ACP Is a Machine Answerer with Exact Ownership and One-shot Choices

The ACP bridge claims an approval only when it owns the exact Agent and the request has a call id. It presents two choices—allow once and reject once—and maps cancellation explicitly; every unknown non-allow option becomes rejection. It never converts a response into a durable grant. APPROVAL-ACP-ANSWERER

This illustrates the intended answerer architecture: web UI, ACP, or another provider can occupy the same waterfall seam, while the ApprovalService owns vocabulary, durable audit, cancellation, and the unbypassable never policy.

18. Tests Encode the Negative Paths, Not Just the Happy Prompt

SuiteWhat it proves at the pinned baseline
ToolRuntime policydeny/ask mapping, missing service, Agent-less call, rogue outcomes, cancellation ordering, and guards after pre-policy
Approval serviceopen-turn requirement, audit pair, first answerer, throws/rogue values, cancellation, fresh ids, never before prepended listeners, and policy context
Permission presetsfolds, shared-bundle identity, custom, no-op writes, load-time failures, fresh pinning, seed/resume preservation, and HMR catch-up
Sandbox escalationstrict ladder, paired fields, one-shot grant, non-widening rejection, missing/Agent-less channel, and every outcome
Web proxyround trip, reconnect replay, malformed/foreign/stale responses, abort, teardown, parallel pairing, and call-id-less isolation

APPROVAL-TOOLS-TESTSAPPROVAL-GUARD-TESTSAPPROVAL-SERVICE-TESTSAPPROVAL-PRESET-TESTSAPPROVAL-ESCALATION-TESTSAPPROVAL-WEB-TESTS

19. Two README Statements Have Drifted Behind Production Source

Documented statementPinned source truthImpact
Permission README names permissionPresets/preset and /permissionPresetsRuntime uses permission/preset and /permissionOperators or extension authors can search for or integrate the wrong identifiers
API proxy README says the pending table handles questions only and has no approvalsThe proxy has a complete approval registry, reconnect replay, response validation, abort, and teardownThe stated wire capability and recovery model are stale

APPROVAL-DOC-PRESET-DRIFTAPPROVAL-DOC-API-DRIFT

Why this matters

Permission code is unusually sensitive to naming and lifecycle misunderstandings. The source and tests are internally coherent; the public package READMEs should be corrected so security review begins from the actual event and command paths.

20. Narrow Comparison with Codex: Similar Separation, Different Policy Richness

This comparison uses only OpenAI's public Agent approvals & security and Subagents documentation, retrieved on 2026-08-13. The DeepSeek side is source-level at the pinned commit; the Codex side is documentation-level and current to that retrieval date. No claim is made about private services or undocumented implementation.

DimensionDeepSeek Harness at pinned sourceCodex public documentation
Core separationSandbox mode and approval policy are independent durable knobs bundled by presetsSandbox mode and approval policy are explicitly separate controls
Typical balanced modeShipped workspace-write + askAuto is documented as workspace-write + on-request
Approval policy vocabularyask or neverDocuments untrusted, on-request, never, and granular categories
ReviewerComposable answerer waterfall; shipped web and ACP answerers; no central automatic risk reviewer foundDocuments user review and eligible auto_review through a separate reviewer
Side-effect domainsGeneric pre-policy plus explicit bash/fs escalationDocuments shell/sandbox requests plus side-effecting app and MCP tool approvals
Fail-closed behaviorMissing/throwing/rogue answerer, no Agent, and never all rejectDocuments failure to obtain required approval as a blocked action; automatic review does not expand the sandbox
DelegationChild approval is pinned to never; one-shot parent grants do not transferPublic docs say subagents inherit current sandbox policy and actions needing fresh approval fail when approval cannot be surfaced
Transferable lesson

Both systems treat containment and human/machine consent as separate layers. DeepSeek Harness's particularly reusable idea is the explicit deny-only guard after extensible policy; Codex's documented advantage is a richer approval-policy vocabulary and an explicit automatic-review role. The comparison is architectural, not a feature score: source evidence and product documentation have different confidence levels.

21. Design Assessment, Hardening Priorities, and Verification Status

FindingAssessmentPriority
Frozen arguments + no pre rewriteStrong audit/execution identityPreserve
Guard after extensible approvalStrong monotonic veto that survives listener orderUse for every owner invariant
Closed outcomes and fail-closed absenceGood degradation semantics with diagnosable reasonsPreserve
Preset vs one-call grant separationClear temporal authority; children do not inherit ephemeral elevationPreserve and document
Risk dialog keyed by preset nameConfirmed static UX/policy mismatch under configurable tablesHigh-priority hardening
Approval panel not self-contained for non-shell callsConsent quality depends on surrounding call presentationImprove typed summaries
Reachability without authenticationAcceptable only under the stated local single-user trust modelRevisit before multi-user exposure
README identifier/recovery driftImplementation is stronger than documentation suggestsCorrect promptly
Verification scope

This chapter traced ToolRuntime preparation and cancellation; approval types, service, events, and invariant; permission configuration, projection, command, settings, and shipped bundle; shared sandbox escalation and shell/fs consumers; child delegation; web wire/registry/UI; ACP; and the focused production tests. This delivery also ran 15 targeted Vitest files spanning approval, presets, escalation, filesystem, process, Job, and Terminal behavior: 508 tests passed and one was skipped. Static findings remain labeled as such; passing tests do not constitute runtime penetration testing, OS-sandbox validation, authenticated multi-user validation, or knowledge of undocumented Codex internals.

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.