Special Topic · Inside DeepSeek Harness

Approvals & Permissions: Two Knobs, One Dropdown

Sandbox mode and approval policy are two independent knobs; presets are just common combos

THE QUESTION THIS PAGE ANSWERS

ANSWER FIRST

What is the key idea behind “Approvals & Permissions: Two Knobs, One Dropdown”?

Sandbox mode and approval policy are two independent knobs; presets are just common combos

DECISION RULE

Make the claim earn its place. Use this page as a decision aid, not a definition to memorize. Connect the idea to one real task, one observable result, and one failure that would change your mind.

TRY NEXT

Write one question you could answer with evidence after trying this idea.

WATCH FOR

A conclusion that sounds complete but leaves the key assumption untested.

Course goalAfter this lesson you can explain three things: why product concepts like plan mode, auto-accept, and YOLO collapse at DSH’s bottom into two orthogonal variables — sandbox/mode and approval/policy; why the preset dropdown only names knob combos, and which events the log actually appends on a switch; and why approval is fail-closed at every step (no answer → treat as deny).
Interactive demo · Dual-knob console

Here’s a permission console you can twist. Left: two knobs, each one job. Right: the preset dropdown. Pick an operation, hit Play to watch the tool call clear the gates; then twist knobs, swap presets, run again — see how the same op’s fate changes.

Knob 1 · sandbox mode

sandbox/mode (file effects) read-only workspace-write danger

Knob 2 · approval policy

approval/policy (ask humans or not) ask never

Preset dropdown

permission preset (knob shortcut)
Events appended to the session log
Model starts a tool callwrite ./src/app.ts
Sandbox layerKnob 1 decides file effects
Approval layerAfter a block, escalate — Knob 2 decides whether to ask
OutcomeWaiting to run
Approval dialog: the Agent wants to temporarily widen the sandbox to danger-full-access for this call — allow? (authorizes this one op only)
Hit Play for the current combo, or scroll here to auto-play once.
Teaching simulation. Knob values, preset table, and escalation approval match docs/subsystems/permission-presets.zh.md, docs/subsystems/approval.zh.md, and packages/sandbox/sandbox/src/escalation.ts. While the dialog is up you can really click for the user.
First: what each knob owns

Bottom line first: DSH splits the big word “permissions” into two questions that don’t gossip with each other. Knob 1 sandbox/mode answers how far a command’s file effects may go — read-only / workspace-write / danger-full-access — filesystem only; network and process visibility aren’t in its vocabulary (docs/subsystems/sandbox.zh.md opening definition). Knob 2 approval/policy answers whether to ask when a human decision is needed — ask or never.

In the log they’re also two independent events: sandbox/mode and approval/policy. Execution, prompts, and replay only read the folded result of those two. That’s what orthogonal means in practice: twist either and the other doesn’t budge.

Knob 1 · sandbox/mode
  • read-only: backend must refuse writes; only keep sinks shells need, like /dev/null.
  • workspace-write: workspace root and backend-promised temp areas are writable; everything outside is blocked.
  • danger-full-access: bypass isolation. Consumers spawn raw commands and never call ctx.sandbox.
Knob 2 · approval/policy
  • ask (default): hand to the responder chain. No responders? Chain ends with unavailable — still treated as deny.
  • never: deterministically return rejected; dispatch no responders. The standard posture for CI and unattended runs.
  • The only allow value is allowed-once, and it authorizes only the asked operation. rejected / cancelled / unavailable — callers treat all three as deny.
Presets: just naming the cells

Then the dropdown. ctx.permissionPresets keeps a table: name → knob combo. Default table has two rows: workspace-write → workspace-write + ask; danger-full-access → danger-full-access + never (permission-presets/src/index.ts lines 167–176). It’s optional — not on the agent-loop trunk, and it owns no enforcement.

What happens on a preset switch? Three events, fixed order: first append a log-only permission/preset recording intent; then write sandbox/mode and approval/policy via each knob’s canonical setter — only when the effective value actually changes. Re-select the current preset? Append nothing. On replay the execution layer only honors knob events; the preset event’s sole job is remembering which name you picked when two presets share one combo.

Then custom. It’s reserved — the table mustn’t contain that name (plugin load throws). Only when you twist knobs to a combo missing from the table does current() derive custom for the client. Outbound only: it can be current state, never a switch target, never in an event payload. That’s why the demo dropdown greys it out — matching the source.

fail-closed end to end

No responders, responder throws, out-of-vocab return, user closes the UI mid-dialog — all normalize to unavailable or cancelled; callers treat as deny. Miss a clear allowed-once anywhere → no pass.

Approvals stay out of model context

approval/asked and approval/decided pair into the session log for audit only. The model sees tool results and runtime context snapshots. ApprovalRequest deliberately omits tool args, pointing via callId at the already-streamed call so you don’t render a drifting copy.

Escalation is one-shot

After the sandbox blocks a write, the model may retry with sandbox_permissions escalation; approval yields allowed-once; that retry resolves policy once under an explicitly wider mode, then discards it. Session knob settings don’t change.

Key evidence · never blocks before the cascade

A boundary worth pressing: if a plugin mounts after the approval service and prepends an always-allow responder at the front of the chain, can it bypass never? No — because never never runs in the chain. Look at the top of decide():

packages/interaction/user-approval/src/index.tslines 304–312 excerpt
  private async decide(req: ApprovalRequest, session: Session): Promise<ApprovalOutcome> {
    const signal = req.signal
    if (signal?.aborted) return 'cancelled'
    // The 'never' policy is decided HERE, before any dispatch: a listener
    // registered with `prepend: true` after this service mounts would sit
    // ahead of any gate LISTENER, so a listener-shaped gate cannot keep the
    // documented promise that 'never' rejects deterministically regardless
    // of registration order — only the service's own request path can.
    if (this.effectivePolicy(session) === 'never') return 'rejected'
Source snapshot note:Based on the local deepseek-harness-master repo; verified against packages/interaction/user-approval/src/index.ts, verified on 2026-08-13. Code blocks keep the original source text.

The comment finishes the design intent: never’s verdict sits on the service’s own request path — no listener-shaped gate can keep the “registration-order-independent” promise. Further down, lines 317–329, ask’s fallback is equally airtight: empty-chain default is unavailable; throwing responders fold to unavailable; weird returns normalize to unavailable.

Full write path for switching presets

“Presets are just shortcuts” is implemented as a fourteen-line private apply(). Three steps, fixed order. 1) resolve(name) table lookup — unknown name throws, listing known keys. 2) Compare to current()’s derived result: only if your name differs from the effective preset do we append permission/preset; re-selecting current writes nothing. 3) Check each knob: if the preset’s sandbox mode differs from the folded effective value (deployment default if missing), call setSandboxMode(); same for approval via the injected setApproval. Skip whichever effective value didn’t change.

So switching presets has no third execution path and no hidden state: one intent log plus at most two knob writes through each canonical setter — what the log appends is what execution folds. Derived custom lives in the same file’s derive(): keep the last-chosen preset if it still matches current knobs; else first matching table row in declaration order; else custom.

Source:packages/interaction/permission-presets/src/index.ts lines 379–392 (apply()), lines 309–321 (derive()), verified on 2026-08-13。

Side-by-side · Single mode axis vs dual knobs
Claude Code: one mode axis + rule table

Claude Code turns the same territory into a single permission-mode axis: default, plan, acceptEdits, bypassPermissions, dontAsk (restored restored-src/src/utils/permissions/PermissionMode.ts lines 44–91, study ch. 7), plus an allow/deny/ask rule table that can go as fine as Bash(git commit:*) prefixes.

Side by side, the cost of collapsing shows. dontAsk ≈ DSH never; bypassPermissions ≈ danger-full-access + never — but five notches slide on one axis, so sandbox tightness and “ask humans?” are sold glued together. DSH’s dual knobs can say read-only + ask: sandbox read-only floor, popup once when a write is truly needed. CC’s axis has no direct slot for that. Conversely CC has what dual knobs can’t: a rule table at tool/command-prefix granularity; DSH’s knobs are session-global, so tool-grain gates need the hook layer. Each side parks complexity elsewhere. Grok Build’s auth chain is yet another path (tool request → constrained execution, layer by layer) — see Grok’s full authorization chain.

Classroom Exercise
01

Walk two nasty scenarios

Scenario 1: approval dialog up, user kills the browser; UI responder is disposed away. What outcome settles the request — allow or deny the tool call? Scenario 2: policy is never; a plugin prepends a responder that always returns allowed-once. Trace from request() to the return value and explain why that responder is never called. (Hint: both answers sit in this page’s source panel and the paragraph after it.)

Takeaway:At DSH’s bottom, permissions are two orthogonal variables: sandbox/mode for file effects, approval/policy for asking humans; presets are table keys naming combos; custom is outbound-only. Approval is fail-closed end to end: the only allow is allowed-once for that one op; never blocks inside the service before cascade dispatch — nobody cuts the line.

The handoffs inside “Interactive demo · Dual-knob console”

“Here’s a permission console you can twist.” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.

Write the state before adding capability

Starting from “Bottom line first: DSH splits the big word “permissions” into two questions that don’t gossip with each other.”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.

  • read-only: backend must refuse writes; only keep sinks shells need, like /dev/null
  • workspace-write: workspace root and backend-promised temp areas are writable; everything outside is blocked
  • danger-full-access: bypass isolation. Consumers spawn raw commands and never call ctx.sandbox

A happy path is not reliability

Use “Scenario 1: approval dialog up, user kills the browser;” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.

From “Interactive demo · Dual-knob console” to “First: what each knob owns”

“Interactive demo · Dual-knob console” grounds the problem in “Here’s a permission console you can twist. Left: two knobs, each one job. Right: the preset dropdown. Pick an operation, hit Play to watch the tool call clear the gates; then twist knobs, swap presets, run agai…”. “First: what each knob owns” then moves it toward “Bottom line first: DSH splits the big word “permissions” into two questions that don’t gossip with each other. Knob 1 sandbox/mode answers how far a command’s file effects may go — read-only / workspace-write /…”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.

Carry the judgment into the next situation

When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.

  • “Interactive demo · Dual-knob console”: Here’s a permission console you can twist. Left: two knobs, each one job. Right: the preset dropdown. Pick an operation, hit Play to watch the tool call clear the gates; then twist knobs, swap presets, run agai…
  • “First: what each knob owns”: Bottom line first: DSH splits the big word “permissions” into two questions that don’t gossip with each other. Knob 1 sandbox/mode answers how far a command’s file effects may go — read-only / workspace-write /…
  • “The closing point”: never: deterministically return rejected; dispatch no responders. The standard posture for CI and unattended runs

The final “The closing point” brings the discussion to “never: deterministically return rejected; dispatch no responders. The standard posture for CI and unattended runs”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.

Mark as learned Your reading progress updates automatically
← PreviousNext →

Keep reading

The next useful article in the thread.

ARTICLE DISCUSSION

Leave one useful thought here.

Keep the idea that clicked, the question that stayed open, or a small note for the next learner.

Discussing Approvals & Permissions: Two Knobs, One Dropdown Inside DeepSeek Harness
3discussionsArticle discussion · synced with the Circle
View in the learning circle
AM
Asha MorganContent editor
INSIGHTField note

I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.

ARTICLE DISCUSSION7 helpful
LH
Lin HarperIndie developer
INSIGHTInsight

After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.

ARTICLE DISCUSSION5 helpful
KM
Kiki MooreProduct operations
QUESTIONQuestion

When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.

ARTICLE DISCUSSION4 helpful