dsh-sandbox-arg-guard
apex-mochen/dsh-sandbox-arg-guard
让「同级或更窄的 sandbox_permissions」不再让工具调用直接失败。会升级的工具(pwsh、bash、write、edit)都广告完整的 sandbox_permissions 枚举,但 DSH 只接受严格更宽于当前生效级别的请求——其源码自称这是「deliberately not a schema constraint」。于是反射式带上该参数的模型往往填它已经在的那个级别,调用在执行前就死掉:"sandbox escalation to \"workspace-write\" is not strictly wider than this call's current \"workspace-write\" mode",某些模型还会为此烧掉一整轮重试。本插件只注册一个 tools/execute waterfall 监听器,且仅在那一条文档化拒绝上、且参数里确实带了升级字段时,把同一个调用去掉该参数重投一次。安全性由 DSH 自己的文档保证:拒绝发生在任何执行之前("nothing has run"),且改过的参数无法再次匹配,因此重投在结构上不成环。已端到端复现并验证——改造前 isError 为 true 且命令从未执行;改造后拿到命令的真实输出、isError 为 false,会话里只有一个 tool/call 与一个 tool/result。零依赖。
安装
dsh plugin --profile web add github:apex-mochen/dsh-sandbox-arg-guardREADME
dsh-sandbox-arg-guard
Keeps a redundant sandbox-escalation argument from failing a tool call.
When an escalating tool (pwsh, bash, write, edit) is rejected because the model requested a
sandbox level it cannot ask for, this plugin re-issues the same call once without that argument — so
the call the model actually intended just runs.
⚠️ Scope narrowed by an upstream fix (2026-09-19)
The same-level case is fixed in DSH
0.1.6-alpha.2and no longer needs this plugin. Commit61c548e2(fix(sandbox): accept repeated effective permission modes, PR #4326) addedif (mode === effectiveMode) return effectiveModeatpackages/sandbox/sandbox/src/escalation.ts:155— verified first-hand onmaster. Repeating the level already in effect is now accepted silently.What remains, and what this plugin is for now: narrower or unsupported targets still throw at
:159-160(sandbox escalation to "…" is not strictly wider than this call's current "…" mode). The plugin still removes the argument in that case, so the call runs. The retry is safe for the same reason as before: the throw still precedes any execution.The sections below document the original 0.1.2-rc.1 reproduction, including its same-level A/B. They are kept as an accurate record of that version rather than rewritten to look current — see EVIDENCE.md for the update note and the verified upstream lines. The narrower-target case has not yet been re-measured on
0.1.6-alpha.2.
The failure it prevents
Every escalating tool advertises the full sandbox_permissions enum, but DSH only accepts a request
for a level strictly wider than the one already in effect. From
packages/sandbox/sandbox/src/escalation.ts:159-164:
// Strict widening is an EXECUTION check against the call's effective mode —
// deliberately not a schema constraint (the enum is the closed target
// vocabulary; the effective mode is per-call truth).
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 schema therefore cannot tell the model which values are legal for this call, and a model that reflexively passes the argument — a very common habit — picks the level it is already at. The result:
Error: sandbox escalation to "workspace-write" is not strictly wider than this call's current "workspace-write" mode
Nothing runs. The model gets an error for a call that was perfectly reasonable apart from one redundant field, and for some models that is a whole turn spent retrying. This is item 1 of the community-verified unfixed-issue list in discussion #6520 — the most-reported entry there, with eight directly related discussions and three more in the same family.
What the plugin does
Registers exactly one tools/execute waterfall listener. It calls next() exactly once and returns
what it produced. Only when the settled result is that one documented rejection and the arguments
actually carried an escalation field does it re-issue the identical call with sandbox_permissions
and justification removed.
Why retrying is safe. escalation.ts:143-152 states the ordering:
Resolve a sandbox-escalation request BEFORE anything executes … the tool registry turns the throw into the call's isError result, and nothing has run. A non-widening request never prompts a human.
Because the rejection precedes any work, re-issuing the same call minus the redundant field cannot double-apply a side effect. The retry is also loop-free by construction: the corrected arguments no longer contain an escalation field, so they cannot match the rejection pattern again.
Why this seam and no other. tools/pre-execute and agent/pre-step are waterfall hooks that return
a decision (allow / ask / deny) — neither can rewrite arguments. tools/execute is the one hook
whose return value is the execution result, and ToolRuntime.execute(input) is public with a
constructible input, so a listener may re-issue a corrected call.
Verified
BEFORE and AFTER on real session logs, same stub, same tool call — raw output in EVIDENCE.md:
| guard absent | guard installed | |
|---|---|---|
tool/result | Error: sandbox escalation to "workspace-write" is not strictly wider … | PROBE-EXECUTED |
isError | true | false |
| did the command run | no | yes |
The session contains exactly one tool/call and one tool/result in both cases: the retry is not
visible as a second call, and the command did not run twice.
Install
dsh plugin --profile web add github:apex-mochen/dsh-sandbox-arg-guard
Restart the profile afterwards.
Configuration
- id: dsh-sandbox-arg-guard
config:
verbose: false # log every repair
enabled: true # set false to keep it installed but inert
| Option | Type | Default | Meaning |
|---|---|---|---|
verbose | boolean | false | Log each repair on the diagnostic channel |
enabled | boolean | true | Turn the guard off without uninstalling |
What it deliberately does not do
- Only that one rejection. An approval refusal, a genuine permission failure, or a tool's own error
is passed through untouched — see the
isErrorcheck intest/smoke.mjs. - Never twice. A second retry is unreachable; a check asserts it.
- Fails open. If the corrected call cannot be issued at all, DSH's original and accurate error is what the caller sees.
- No dependencies. One implementation file, Node built-ins only. It never spawns, reads, writes, or fetches.
This is a workaround, not a core fix
The real fix belongs in DSH: either make the advertised enum relative to the effective mode, or treat a
non-widening request as a no-op instead of an error. The source comment shows the current shape is
deliberate, so this plugin narrows the gap in user-visible behaviour without changing core. DSH does
not accept external pull requests today (CONTRIBUTING.md), so a plugin is the reachable seam.
Compatibility
- DSH
0.1.x(peer:@deepseek-ai/cordis ^4.0.1) - Node.js 20+
- Registers exactly one waterfall listener (
tools/execute) and contributes no tools
License
MIT