Skip to content

[Tracking] sandbox escalation "not strictly wider" failures — root cause in DSH core, not this plugin (consolidates #3, #4) #7

Description

@V1ki

Summary

This consolidates #3 and #4 — the same underlying problem seen from two angles. After a full source-level analysis of DSH core and a fully captured failing session, the conclusion is:

The sandbox escalation … is not strictly wider … failures are produced entirely inside DSH core (deepseek-harness), not by this plugin. This plugin's adapters forward the tool schemas DSH assembles, verbatim; there is nothing to fix here. This issue documents the verified root cause, an end-to-end reproduced trace, and the proposed upstream fix, and serves as the canonical tracker.

Two compounding design gaps in DSH core:

  1. Escalation fields are advertised statically, ignoring the session's current mode. Whenever a confining executor/filesystem is mounted, every sandbox-enforcing tool (bash, pwsh, write, edit) advertises sandbox_permissions with the full enum ["workspace-write", "danger-full-access"] — baked at composition time. A session already standing at workspace-write still sees "workspace-write" as an option; a session at danger-full-access (issue Error: sandbox escalation to "danger-full-access" is not strictly wider than this call's current "danger-full-access" mode #3's case) still sees both. Escalation must be strictly wider than the current mode, so these values are guaranteed-invalid in those states — the schema itself hands the model a trap.

  2. The rejection is not self-correcting. An equal-or-narrower request fails with:

    sandbox escalation to "X" is not strictly wider than this call's current "X" mode
    

    True, but it never says nothing was denied — retry without the fields. Models read it as "my operation was rejected → I need more permission" and escalate harder.

Reproduced end-to-end trace

Captured from a local DSH web session log (session-c69ec652…), provider codex, model gpt-5.6-luna, reasoning effort medium — notably not a weak model.

Session state: cwd /Users/v1ki/Desktop/factory, preset workspace-write (sandbox workspace-write, approval ask). The user asked for a prompt text to be saved to a file inside the workspace — a bare write would have succeeded on step 1 with no escalation whatsoever.

Step Tool Escalation args the model sent Result
1 write sandbox_permissions: "workspace-write", justification: “在当前工作区创建文本文件,保存刚才使用的图片提示词。” not strictly wider error
2 write identical retry, byte-for-byte same error
3 bash (printf redirect, same file) still sandbox_permissions: "workspace-write" same error
4 bash escalated to sandbox_permissions: "danger-full-access", justification: “前一次工作区写入调用被运行环境拒绝,需要使用更宽权限…” approval/asked fired — a real danger-full-access approval prompt shown to the user

Key observations:

  • The sandbox never denied anything in the entire session. All four failures were self-inflicted by the schema; the [sandbox: file access denied …] marker never appeared.
  • The step-1 justification shows the model (mis)read sandbox_permissions as “declare the mode this operation runs under”, and picked exactly the value the runtime context had just advertised (“Current DSH file policy: workspace-write …”).
  • The step-4 justification is factually false — no write was ever rejected by the runtime; only the escalation request was rejected. The cascade ends with a needless full-access approval prompt for an operation that was always permitted at the standing mode. A reflexive “Approve” runs a plain in-workspace write at danger-full-access: a real (if mild) security widening caused purely by schema UX.
The write schema as actually advertised in this session (from the logged request/header)
{
  "name": "write",
  "description": "Create or fully replace a UTF-8 text file.",
  "parameters": {
    "type": "object",
    "properties": {
      "file_path": { "type": "string", "description": "Path to write, resolved by the filesystem backend." },
      "content": { "type": "string", "description": "Full UTF-8 text content to write." },
      "sandbox_permissions": {
        "type": "string",
        "enum": ["workspace-write", "danger-full-access"],
        "description": "The wider sandbox mode this file operation needs. Only valid as a one-shot retry of an operation the sandbox just denied; requires justification and user approval."
      },
      "justification": { "type": "string", "description": "Required with sandbox_permissions: one sentence for the user explaining why this exact file operation needs the wider access." }
    },
    "required": ["file_path", "content"]
  }
}

Note the enum contains "workspace-write" — the session's own standing mode, which can never be a valid escalation target in this state.

Root cause, verified in DSH source

All references are to current main of deepseek-ai/deepseek-harness.

Advertising is composition-time, not session-time. packages/shell/tool-bash/src/index.ts (same in tool-pwsh):

const defaultMode = ctx.shell.sandboxMode
const escalationModes: readonly SandboxMode[] = defaultMode === undefined ? [] : ESCALATION_TARGETS

and packages/fs/tool-fs/src/sandbox.ts (FsSandboxController.schemaFields(), shared by write/edit) spreads the same two fields with enum: [...ESCALATION_TARGETS]. The decision is made once at plugin load from the capability fact (“is a confining backend mounted?”), never from the session's standing mode.

The strict-wider check lives only at execution. packages/sandbox/sandbox/src/escalation.ts:

export const WIDER_MODES: Record<string, readonly SandboxMode[]> = {
  'read-only': ['workspace-write', 'danger-full-access'],
  'workspace-write': ['danger-full-access'],
}
// in approveEscalation():
if (!(WIDER_MODES[effectiveMode] ?? []).includes(mode as SandboxMode)) {
  throw new Error(`sandbox escalation to "${mode}" is not strictly wider than this call's current "${effectiveMode}" mode`)
}

The code comments explain the enum is deliberately not cut down by the composition default (a narrower-switched session would be stranded) — a sound argument that nevertheless does not cover the session's standing mode, which is per-call truth the advertised schema currently contradicts on every request.

The trap is armed by DSH's own runtime context. SandboxPolicyService injects per-request context naming the current mode (“Current DSH file policy: workspace-write. Any available operation … may modify files under the session workspace …”), which is exactly the string a confused model copies into sandbox_permissions.

Why this plugin is the wrong layer (re: #4)

  • The premise of [Bug] Published bundle lib/index.js is stale: omitSandboxEscalation fix never reaches the runtime #4 (stale bundle missing an omitSandboxEscalation fix) was disproven by inspecting the published npm artifact: no such code ever existed in any released version, the repo, or its history.
  • The adapter receives GenerateOptions.tools already assembled by DSH. It has no session handle and cannot know the standing mode, so it cannot narrow the enum correctly.
  • Unconditionally stripping the fields would break the one sanctioned mechanism DSH gives models to request wider access after a real denial in read-only/workspace-write sessions.

Proposed upstream fix

1. Narrow the advertised enum per assembly (structural fix). Prompt assembly runs before every model step, AssembleContext carries the agent (see packages/core/agent/src/runtime-types.ts), and the system-prompt/assemble waterfall's return value is authoritative — packages/core/agent/src/model-selection.ts is an in-tree precedent. SandboxPolicyService (which already owns per-session mode resolution and the sandbox:policy runtime context) can register a listener that rewrites every advertised sandbox_permissions (matched by parameter name — covers bash/pwsh/write/edit at once) to WIDER_MODES[standingMode]:

standing mode advertised enum
read-only ["workspace-write", "danger-full-access"] (unchanged)
workspace-write ["danger-full-access"]
danger-full-access drop sandbox_permissions + justification entirely

Per-assembly recomputation means a mid-session /permission switch is reflected on the next step; the registry stays untouched, so the “schemas are registry-global” doctrine holds.

2. Make the rejection self-correcting (behavioral safety net). Schema validation only checks advertised keys, so a model can still send the fields from conversational memory. In approveEscalation, split the “requested ⊆ current” case:

sandbox escalation to "workspace-write" is unnecessary: this call already runs under
"workspace-write" and nothing was denied — retry the same command without
sandbox_permissions and justification

3. (Optional) One sentence in renderPolicyContext for workspace-write: “Operations inside the workspace need no sandbox_permissions.” — removes the ambiguity that armed step 1.

Counterfactual against the trace above: with (1), step 1 cannot pick "workspace-write" and the natural bare write succeeds immediately; even if the fields are hallucinated from memory, (2) ends the cascade at step 1 instead of step 4's needless full-access prompt.

Workaround until upstream ships

If you hit … is not strictly wider …: nothing was denied — retry the same call without sandbox_permissions/justification. A standing user rule like “Never set sandbox_permissions unless the exact same call was just denied with a [sandbox: …] marker” suppresses the pattern.

Action items

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions