dsh-ban-wait
One Host plugin that refuses tool arguments which would park the DSH agent loop.
The investigation that produced it — every blocking path in this composition,
with file:line — is in FINDINGS.md.
It exists because the shell command timeout is not a ceiling. pwsh-sandbox's
timeoutMs: 1000 promotes a long command to a background job, but the model can
pass timeoutMs: 180000 on the call itself and the executor honors it up to
maxTimeoutMs (600000 ms by default) — three minutes inside one tool call. The
same class of leak is job_output { wait: true }, clamped only by a 10-minute
default. This plugin makes "no single call parks the loop" a property of the
tool registry instead of a property of the model's discipline.
Files
| File | Role |
|---|
index.js | the plugin: policy + global ctx.tools.guard() |
check.mjs | offline self-check of the policy (30 assertions, no DSH process) |
bundle-check.mjs | pre-install check: replays what the plugin page inspects, with the harness's own patch loader |
cordis.patch.yml | the bundle's patch: inserts the Host-plane row |
package.json | dsh.bundle.patch → cordis.patch.yml; no dependencies |
FINDINGS.md | the escape inventory and root-cause analysis |
tools/wait-audit.mjs | audit every session on an instance for parked foreground time; tools/WAIT-AUDIT.md states the metric |
tools/speed-audit.mjs | per-conversation tok/s, actual vs with the parked time removed, plus the line chart; tools/SPEED-AUDIT.md states the metric |
Policy
| Config field | Default | Meaning |
|---|
maxBlockingMs | 1000 | ceiling for any per-call blocking hint the model supplies |
denyWaitTools | ["job_output"] | tools whose wait: true is refused |
timeoutHintTools | ["pwsh", "bash"] | tools whose timeoutMs / timeout_ms is checked against maxBlockingMs |
requireBackgroundTools | [] | tools that must be called with run_in_background: true |
A refusal is a Error: … tool result that names the ban, clears the background
job of blame, and states how to re-issue the call, so the model recovers in one
step. An unusable config value is logged and replaced by its default rather than
thrown: a typo here must not fail the instance's boot.
Install
Through the plugin page — the only channel that keeps the plugin visible in the
inventory with its own toggle:
- 添加插件 → paste the absolute project directory
D:\桌面\DSH与自动化\dsh-ban-wait. A local directory is recognised by
isAbsolute() before any npm-spec parsing and never touches a registry, so
the registry chooser in that dialog is irrelevant.
- The manager reads
package.json, requires a name and a dsh.bundle, adds
the dependency to the profile, then reconciles dsh.profile.bundles
automatically ("new bundle dependencies activate automatically"). The plugin
then appears under 已安装 with a toggle.
- If the result reports that a restart is required, restart the instance: a new
Host module generation is not loaded into a running process.
Do not also add a raw loader row for this plugin to the profile patch. The
bundle's cordis.patch.yml inserts row id ban-wait, and two rows sharing that
id make id-targeted patches — the toggle, any override — reach only one of them.
Companion configuration, without which the guard is the only thing standing
between the model and a three-minute call:
- id: pwsh-sandbox
name: "@deepseek-ai/dsh-pwsh-sandbox"
config:
timeoutMs: 1000 # default for a command
maxTimeoutMs: 1000 # ceiling for the model's timeoutMs argument — the line that matters
maxOutputBytes: 10000
Verify
# the policy itself (30 assertions)
D:\dsh-v2\inst-3080\node\node.exe D:\桌面\DSH与自动化\dsh-ban-wait\check.mjs
# the package the plugin page will inspect: path, package.json, name, dsh.bundle,
# `files` coverage, and a real parse of cordis.patch.yml by the harness loader
D:\dsh-v2\inst-3080\node\node.exe D:\桌面\DSH与自动化\dsh-ban-wait\bundle-check.mjs
Neither proves activation — that happens when the profile mounts the bundle's
layer, which the plugin page reports in its install result.
Live probes (each returns a refusal instead of executing):
job_output { job_id: "pwsh-1", wait: true } → Error: … (ban-wait): job_output was called with wait: true
pwsh { command: "ok", timeoutMs: 180000 } → Error: … caps how long one tool call may hold the agent loop at 1000 ms
pwsh { command: "ok" } → runs normally
workflow { script: "return 1" } → runs synchronously (not banned by default)
Verified on instance 3080: both refusals fire, plain calls and non-blocking job
reads behave exactly as before, and the row mounts and unmounts live — no
restart was needed for any of it.
Boundaries
Superseded
D:\dsh-v2\inst-3080\plugins\no-blocking-job-wait\ was the first, single-rule
version of this ban (it refused only job_output { wait: true }). This project
replaces it; the profile patch no longer references it and its files are inert.