Permission Analysis, Approval, and Monotonic Guards
Who proposes risk, who may permit, and who may only deny
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.
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
| Concept | Lifetime | Authority | Durable? |
|---|---|---|---|
| Tool visibility restriction | Live scope composition | Removes inherited schemas; it is capability presentation, not execution authorization | No |
| Sandbox mode | Deployment default or one Session override | Constrains file effects through an enforcing backend | Session override: yes |
| Approval policy | Deployment default or one Session override | ask routes to answerers; never rejects every ask | Session override: yes |
| Permission preset | Standing Session selection | Bundles sandbox mode and approval policy | Yes |
| Approval outcome | One request | Only allowed-once grants; it does not alter standing policy | Audit pair: yes |
| Monotonic guard | Live registration, evaluated per call | Final synchronous deny-only veto | No; 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.
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.
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
| Outcome | Meaning | Tool consequence |
|---|---|---|
allowed-once | One answerer grants this request | The generic gate becomes allow, or the requested wider mode is stamped onto this call |
rejected | Explicit refusal, or deterministic never policy | Deny |
cancelled | The request was withdrawn | Deny/abort; a late answer is discarded |
unavailable | No answerer, a throwing answerer, or an invalid answer | Deny |
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
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
| Event | Role | Controls execution directly? |
|---|---|---|
permission/preset | Records selected user intent and disambiguates equal bundles | No |
sandbox/mode | Standing sandbox override | Yes, folded for each confined call |
approval/policy | Standing prompt behavior | Yes, 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
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 mode | Legal one-call targets |
|---|---|
read-only | workspace-write, danger-full-access |
workspace-write | danger-full-access |
danger-full-access | None |
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
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
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
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
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
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
| Suite | What it proves at the pinned baseline |
|---|---|
| ToolRuntime policy | deny/ask mapping, missing service, Agent-less call, rogue outcomes, cancellation ordering, and guards after pre-policy |
| Approval service | open-turn requirement, audit pair, first answerer, throws/rogue values, cancellation, fresh ids, never before prepended listeners, and policy context |
| Permission presets | folds, shared-bundle identity, custom, no-op writes, load-time failures, fresh pinning, seed/resume preservation, and HMR catch-up |
| Sandbox escalation | strict ladder, paired fields, one-shot grant, non-widening rejection, missing/Agent-less channel, and every outcome |
| Web proxy | round 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 statement | Pinned source truth | Impact |
|---|---|---|
Permission README names permissionPresets/preset and /permissionPresets | Runtime uses permission/preset and /permission | Operators or extension authors can search for or integrate the wrong identifiers |
| API proxy README says the pending table handles questions only and has no approvals | The proxy has a complete approval registry, reconnect replay, response validation, abort, and teardown | The stated wire capability and recovery model are stale |
APPROVAL-DOC-PRESET-DRIFTAPPROVAL-DOC-API-DRIFT
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.
| Dimension | DeepSeek Harness at pinned source | Codex public documentation |
|---|---|---|
| Core separation | Sandbox mode and approval policy are independent durable knobs bundled by presets | Sandbox mode and approval policy are explicitly separate controls |
| Typical balanced mode | Shipped workspace-write + ask | Auto is documented as workspace-write + on-request |
| Approval policy vocabulary | ask or never | Documents untrusted, on-request, never, and granular categories |
| Reviewer | Composable answerer waterfall; shipped web and ACP answerers; no central automatic risk reviewer found | Documents user review and eligible auto_review through a separate reviewer |
| Side-effect domains | Generic pre-policy plus explicit bash/fs escalation | Documents shell/sandbox requests plus side-effecting app and MCP tool approvals |
| Fail-closed behavior | Missing/throwing/rogue answerer, no Agent, and never all reject | Documents failure to obtain required approval as a blocked action; automatic review does not expand the sandbox |
| Delegation | Child approval is pinned to never; one-shot parent grants do not transfer | Public docs say subagents inherit current sandbox policy and actions needing fresh approval fail when approval cannot be surfaced |
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
| Finding | Assessment | Priority |
|---|---|---|
| Frozen arguments + no pre rewrite | Strong audit/execution identity | Preserve |
| Guard after extensible approval | Strong monotonic veto that survives listener order | Use for every owner invariant |
| Closed outcomes and fail-closed absence | Good degradation semantics with diagnosable reasons | Preserve |
| Preset vs one-call grant separation | Clear temporal authority; children do not inherit ephemeral elevation | Preserve and document |
| Risk dialog keyed by preset name | Confirmed static UX/policy mismatch under configurable tables | High-priority hardening |
| Approval panel not self-contained for non-shell calls | Consent quality depends on surrounding call presentation | Improve typed summaries |
| Reachability without authentication | Acceptable only under the stated local single-user trust model | Revisit before multi-user exposure |
| README identifier/recovery drift | Implementation is stronger than documentation suggests | Correct promptly |
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.