cordis-plugin-sandbox-grant-advisor
Verified@argszero/cordis-plugin-sandbox-grant-advisor · v0.6.0 · MIT
Turns three sandbox environment failures that name neither their cause nor a remedy into a diagnosis with a path forward. Family 1, the Windows workspace ACL: nine reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816) of one signature —
Install
dsh plugin add @argszero/cordis-plugin-sandbox-grant-advisor Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Readme
@argszero/cordis-plugin-sandbox-grant-advisor
Turns a sandbox environment failure that has no path forward into a diagnosis the model — and the user reading the transcript — can act on. Three signatures, one mechanism:
SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws) # Windows workspace ACL
PTY shell exited during startup # persistent shell × confining mode
[exit code: -1073741502] (0xC0000142) # a confined child that never started
This plugin is the stopgap for "the error does not name the outstanding condition". It repairs nothing: no ACL is written, no privilege is requested, nothing is elevated, no environment variable is set for another process, no preset is installed and no mode is changed.
The three failures it recognizes
The first two are recognized on the public tools/post-execute waterfall
(@deepseek-ai/dsh-tools) from the failure text. That seam is the one that has
all three of what a diagnosis needs: the failure
(providers propagate their error unchanged and the tool pipeline settles it as an
isError result), an agent identity to attribute it to (exec.agent), and a
channel that speaks to the model in the same step (PostToolDecision's
additionalContexts, which the agent loop turns into a durable user-role message
— packages/core/agent-loop/src/tool-calls.ts). The third is recognized at the
same seam from the canonical value of a result the pipeline calls a
success, for a reason §3 gives in full.
That seam, not ctx.sandbox.confine: confine(argv, policy, signal) sees the
confinement failure too, but its signature carries no agent, so a wrapper could
detect the condition and never deliver a word about it to the session that is
stuck.
1. Workspace provisioning — the Windows ACL failure (acl-provisioning)
Six reports describe this exact line: discussion #7538, discussion #7622,
discussion #7646, discussion #7720, discussion #7750, discussion #7735
(the last two on data-volume workspaces, where no ACE names the caller at all —
the inherited Authenticated Users: Modify is the whole of their access). In each
one every sandboxed command fails the same way, before it runs, and the error names
neither the missing right nor a remedy.
#7720 is worth reading for where the failure lands: the grant is materialized
at sandbox initialization, so this is not one refused operation but every
shell tool at once — the reporter could not run netstat or even icacls to
diagnose the error they were staring at (on 0.1.5-rc.3 the same directory
worked, because confinement was skipped silently rather than failing closed).
They also report the two remedies that look right and are not
(takeown /F <dir> /R /D Y, icacls <dir> /reset /T /C), which is why the
advisory names them with the reason each fails instead of leaving the reader to
discover it.
The Windows backend provisions a workspace by writing the directory's DACL and
its mandatory-integrity label in one SetNamedSecurityInfoW call
(packages/sandbox/sandbox-windows-acl/src/acl.ts):
if (applyResult !== abi.ERROR_SUCCESS) throwWin32(api, 'SetNamedSecurityInfoW', applyResult, `${label}(${path})`)
Two consequences follow from that one line:
- The label lives in the SACL, and its half is what gets refused. The
owner's implicit rights cover only
READ_CONTROLandWRITE_DAC, so the combined apply additionally needsWRITE_OWNERon the directory — an object right, which a Full-control directory (the normal workspace case) has and amkdir-created one inheriting "Authenticated Users: Modify" (0x1301bf) does not. This is the backend's own documented prerequisite ("granted directories must be caller-owned and grantWRITE_OWNER"). - It is not
SeSecurityPrivilege, and elevation is the wrong lever. That is the token-privilege form of the same idea, and it is the hypothesis the reports naturally reach for —whoami /privcannot tell the two apart, becauseWRITE_OWNERis an object right and never appears in that table. GrantingWRITE_OWNERon the workspace root needs no elevation.#7735settles the gate with an isolation table on one machine and one unprivileged account: the label write succeeds with Full control or with Take-ownership alone, and fails withChangePermissions,ReadPermissionsorModifyalone — so the gate isWRITE_OWNERand nothing else. - The remedy is the narrowest form of that right. The advisory recommends
icacls <dir> /grant "<user>:(OI)(CI)(WO)"—WOisWRITE_OWNER, i.e. literally the right the documented prerequisite names, so the one-liner grants nothing the harness did not ask for — and offers Full control second, as the same line withFin place of(WO). Both assume the caller owns the directory: owner-implicit rights cover the DACL half of the merged write. - The version boundary is stated, because the error cannot carry it. Up to
0.1.6-alpha.xthe backend'sSetNamedSecurityInfoWwrote the DACL only (flag 4) and a Modify-only workspace provisioned fine; the mandatory label — and with it the SACL, flag 20 — arrives in0.1.7-alpha.1. So the same string on an older line belongs to a different cause space, and the natural reach (downgrade to the build that "worked") is refused with its reason: the label is what confines deletes to the workspace, and reverting it reintroduces the escape it closed.#7750asks for exactly this and explains why it is a usability regression traded for a security fix.
The grant is materialized lazily, on the first confined call, and nothing is cached when it throws — so the same failure repeats per command (850 calls across 39 sessions in #7622; 52,588 output tokens with no output in #7538), which is why the loop cannot separate it from ordinary command noise.
On the first recognized failure, the result is enriched with:
Sandbox provisioning failed — no sandboxed command can run in this workspace until its ACL applies.
What was reported:
SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws)
Why it is refused while the directory looks writable: that call is a MERGED write ...
... the label half additionally needs WRITE_OWNER on the directory. ...
A version boundary worth knowing before reaching for an older build: the label half is new to this package.
Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4) ... `0.1.7-alpha.1` is where the mandatory
label — and with it the SACL, flag 20 — arrives. ... Rolling back is not the fix either: the label is what
confines deletes to the workspace, and reverting it reintroduces the escape it closed.
Confirm the cause (unelevated) — `icacls` is a normal user command:
icacls "D:\ws"
Ownership decides which of the two commands below can work, so read it first — PowerShell 5.1 or later:
(Get-Acl "D:\ws").Owner # compare with: whoami
IF YOU OWN THE DIRECTORY — the usual workspace, on a data volume as much as on C::
one unelevated line, then run the command again:
PowerShell: icacls "D:\ws" /grant "$env:USERNAME:(OI)(CI)(WO)"
cmd: icacls "D:\ws" /grant "%USERNAME%:(OI)(CI)(WO)"
... Full control works just as well — the same line with `F` in place of `(WO)`
IF YOU DO NOT OWN IT — a directory an installer or another account created, e.g. owner
`BUILTIN\Administrators`:
the line above cannot run at all. Changing a DACL takes WRITE_DAC, which you hold neither as owner nor
through any ACE, so `icacls /grant` is refused with `Access is denied` — for the very command that would
fix it. ... Run the grant once from an account that already holds both — that is, from an ELEVATED prompt:
icacls "D:\ws" /grant "<your-account>:(OI)(CI)F"
... or take ownership first (also elevated; it wants SeTakeOwnership), after which the unelevated `(WO)`
line above applies: icacls "D:\ws" /setowner "<your-account>"
... or sidestep the ACL: create the workspace under `%USERPROFILE%`.
What will NOT fix it on its own — both look like the right move, and both were tried and reported:
takeown /F "D:\ws" /R /D Y
makes you the owner, and ownership's implicit rights are READ_CONTROL and WRITE_DAC only — so it
supplies the DACL half and still not WRITE_OWNER, the right this call needs.
icacls "D:\ws" /reset /T /C
restores inheritance, and inheritance is what supplied the Modify-only ACE above.
Why the remedy forks (added in 0.5.0). The same error covers two different
rights situations, and one command cannot serve both. Where the caller owns
the directory, the owner's implicit WRITE_DAC satisfies the DACL half of the
merged write, WRITE_OWNER is the single missing right, and the unelevated
icacls /grant that supplies it can itself run — #7750 measured exactly that
fix working. Where the caller does not own it (#7771: owner
BUILTIN\Administrators, held deny-only for that token), WRITE_DAC is missing
too, so the very same command is refused before it does anything, and (WO)
alone would not be enough even if it went through. The failure text is identical
in both, so the classifier cannot pick a branch — the advisory hands over the
ownership check as the selector instead of guessing, which is also the
actionable-guidance half of what #7771 asked for. Until 0.5.0 a single
unconditional one-liner was printed, with a sentence noting it assumed
ownership; that would have sent the second environment to a command that is
denied — the same defect this plugin exists to answer.
What is deliberately not shipped: icacls ... /grant "<user>:(OI)(CI)(WD,WO)",
the two needed rights named explicitly. It is the tighter form and it is
plausibly correct syntax, but this project has no Windows host to run it on, and
shipping an unverified command in a remedy whose whole point is that it works is
the failure mode being fixed. F (verified by #7804's reporter) and
/setowner (named by both reports) are given instead.
2. Persistent shell startup (pty-startup)
Discussion #7638 reports the second shape: with the minimal preset on
Windows, and under a confining sandbox mode (workspace-write / read-only,
not danger-full-access), every shell call dies instantly with
PTY shell exited during startup
The terminal backend spawns the shell through the sandbox
(packages/terminal/terminal-bash/src/index.ts; the throw is in
src/session.ts and src/index.ts, both on the same waitReason === 'session_exit' branch), and there the pseudo-console cannot be created at all,
so the child exits before its first prompt. Retrying never helps; the message
points at no cause.
The reporter's own three-arm control makes the sandbox mode the discriminator:
minimal × confining fails, minimal × danger-full-access succeeds, standard
(one-shot shell) × confining succeeds. That is why the advisory is only ever
built with the resolved mode the failing call actually ran under — from
ctx.sandboxPolicy.resolve({ session }), the same resolver the terminal layer
calls before spawning, with the same session.
The advisory that follows is addressed to two different readers:
Persistent shell failed to start — command execution is unavailable in this session, and retrying cannot fix it.
What was reported:
PTY shell exited during startup
The `bash` tool is a PERSISTENT PTY session (a shell that stays alive between calls), and this session's sandbox mode is
`workspace-write` — not `danger-full-access`. A confining mode spawns the shell through the sandbox, and there the
terminal backend cannot create the pseudo-console at all, so the child exits before its first prompt. ...
Do NOT retry, and do not look for a command that fixes it: every attempt will fail identically, and there is no
shell to run a command in. Use your file read/write tools instead, and hand the choice below to the user.
What unblocks the session — the user's decision, not the model's:
1. switch the agent preset to `standard`, whose shell tool is a one-shot subprocess (no PTY) and works
under the sandbox; or
2. override the `preset-minimal` row in your profile patch — `$DSH_HOME/profiles/<profile>/cordis.patch.yml`, or
`$DSH_HOME/cordis.patch.yml` for every profile — replacing its `persistent-shell` group with
`@deepseek-ai/dsh-tool-pwsh` (a one-shot subprocess, no PTY); the patch layer is yours, so an upgrade
will not overwrite it; or
3. run the session with `danger-full-access`, which drops the very confinement the sandbox exists to give.
Prefer 1 or 2.
The model's instruction is to stop — not to run a command (there is no shell
to run it in) and not to call a fallback shell tool (minimal mounts exactly
one platform-selected persistent shell and no one-shot shell, by design:
.agents/notes/implemented/simplification/2026-09-03-minimal-profiles-persistent-shell-only.md).
Naming a tool the failing composition does not mount would be a wrong remedy,
which is the main risk this family's text is written to avoid.
The remedy is a patch layer, not a directory. The pre-declarative
$DSH_HOME/.agent-presets/<id>/ preset folder is a plausible-looking trap: it
still reads as the natural place to put a preset, and nothing in the harness
reads it any more (@deepseek-ai/dsh-agent-preset-registry: the registry
"neither scans directories nor accepts preset paths"). Preset changes are
@deepseek-ai/dsh-agent-preset rows — an insert for a new one, a patch keyed
by row id (preset-minimal) for a change to a shipped one. A test arm asserts
the advisory never names the dead directory.
3. A confined child that never started (native-init)
Two reports of one exit code: [#7876] (the packaged desktop app) and [#7877]
(MSYS2 / Git Bash). Both are 0xC0000142 STATUS_DLL_INIT_FAILED — the Windows
loader terminated the process while it was initializing its native images, i.e.
before the program's entry point. A command that ran and then failed exits
with its own status and prints its own output; this one produced neither.
| what was run | what came out | exit code |
|---|---|---|
cmd.exe /c "echo cmd-ok" |
cmd-ok |
0 |
pwsh -NoLogo -NoProfile -Command "Write-Output pwsh-ok" |
pwsh-ok |
0 |
D:\Git\bin\bash.exe -c "echo bash-ok" |
couldn't create signal pipe, Win32 error 5 |
-1073741502 |
Two producers have been measured under a confining mode:
- An MSYS2 / Git-Bash program ([
#7877]). The restricted token's runtime cannot create the pipe it uses for signals, so bash aborts in the loader phase, whilecmd.exeandpwshrun fine in the same workspace under the same mode. The plugin's own composition has no way around this:tool-bashandbash-sandboxaredisabledon win32 (@deepseek-ai/dsh-base/cordis.patch.yml), so the combination is likely never covered upstream. The one conversion a model can make itself is to write the same work as a PowerShell orcmdcommand. - The packaged desktop app's sandbox runner ([
#7876]).dsh-sandbox-locallaunches the runner as[process.execPath, entry], and in the packaged buildprocess.execPathis the Electron executable, which starts as an application unless the child's environment carriesELECTRON_RUN_AS_NODE=1— so the runner never runs and every confined command reports this code with no output at all. The unpacked node host (node apps/cli/lib/bin.js web) is unaffected. The plugin reports whether this process is an Electron binary (process.versions.electron) as a measured fact rather than assuming it, because that is the check the discriminator turns on.
Why this family is read from a successful result. The producer never marks
it an error, and that is a fact about upstream rather than a choice here:
RUNNER_FAILURE_RULES['windows-acl'] admits exactly one code —
[{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }]
(packages/sandbox/sandbox-local/src/index.ts) — and classifyRunnerFailure
skips any other code before it looks at stderr
(packages/sandbox/sandbox/src/diagnostics.ts), so 0xC0000142 is never a
runner failure and SandboxUnavailableError is never thrown. The renderer then
reports it the way it reports any finished command — "Non-zero exits are
reported, not errored … only infrastructure failures (spawn errors, aborts)
surface as isError results" (packages/shell/tool-pwsh/src/render.ts) — as
[exit code: …]. Every version of this plugin before 0.6.0 read error results
only and was structurally blind to it, which is exactly why the model retries a
command that can never start.
The read is ToolExecutionSuccess.value — the tool's own canonical output,
documented as "Execution-local canonical value; deliberately omitted from
durable events" and not carried on failure results at all — so the code
arrives structurally rather than as a line of text. A command that prints
[exit code: -1073741502] is not this failure, and neither is a value some other
tool happens to build with an exitCode field: the classifier requires the
shipped shell projection (kind: 'foreground' plus an integer exitCode).
The advisory that follows:
Sandboxed command never started — the process died while its native libraries were loading.
What was reported:
[exit code: -1073741502] (0xC0000142 STATUS_DLL_INIT_FAILED)
The call ran under sandbox mode `workspace-write`, where the harness starts every command through its
restricted-token runner.
0xC0000142 is STATUS_DLL_INIT_FAILED: ... this is BEFORE the program's entry point. ...
Nothing in the code says "sandbox" by itself; what makes the sandbox a candidate is the mode above ...
Two producers have been measured under a confining Windows mode. Check which one this is:
1. An MSYS2 / Git-Bash program ... (#7877)
If that is what could not start: write the same work as a PowerShell or `cmd` command instead
2. The packaged desktop application's sandbox runner ... (#7876)
This process is NOT an Electron binary (`process.versions.electron` is unset), so that producer does not apply here.
Do not retry this call: the environment has not changed, and the identical call produces the identical code.
Honest boundary — 0xC0000142 has producers this list does not have: a program that cannot load one of
its own DLLs dies this way too, and the sandbox backend's own source records that a child started with
a hidden console window does as well ... This is not a claim that the sandbox caused the failure.
What it does not claim. The code is a loader status, and the loader reports
the same status for causes that have nothing to do with the sandbox (a missing
DLL, a program's own initialization failure, the hidden-console child the
backend's own source avoids CREATE_NO_WINDOW for). So the advisory diagnoses
the class ("the process never reached its entry point") and enumerates the
producers measured under a confining mode, each with the check that separates
them — one of which the reader answers (what program failed to start) and one of
which the plugin answers (is this host the packaged desktop binary). It never
offers danger-full-access as a fix and never suggests a sandbox setting be
relaxed.
What it does with a recognized failure
One durable advisory per agent, per family. An agent that hits two families is told about both, once each. The notice carries its own producer-owned
source.kind(sandbox-grant-advisor) — not the retiredpluginwrapper, which the current session format refuses — and a bounded one-linesummaryfor the transcript row. The host log gets one matchingwarnline, so the fact survives outside the transcript too.A disclosure when it withholds. The PTY and native-init advisories are only sent when the resolved mode actually confines. If the mode is
danger-full-access, or cannot be resolved at all (nosandboxPolicyservice mounted, no agent session, a resolver that throws), the failure is left exactly as it was and the host log says so once. Silence alone would make "the sandbox is not the cause" and "this plugin could not tell" indistinguishable from the outside. Withholding is never a guess: an unresolvable mode is not an invitation to fall back to the deployment default.An optional, bounded fail-fast half (
enforceAfter, default 0 = off) — ACL family only. It refuses a call before dispatch (tools/pre-execute) only when both hold: the environment has failed provisioning at leastenforceAftertimes, and this exact call (tool + canonical arguments) is one this plugin watched fail. The budget ismaxDenials(default 2), after which the call proceeds again. The budget is per episode of brokenness: a call that finally succeeds stops being a denial target and re-arms it, so an environment that breaks twice can be refused twice — while a session can always make progress by spending the budget it has.Why the blocking half does not extend to the two mode-gated families (it is ACL-only by construction, in the parameter type): the ACL remedy is a command the user can run while the session continues, so refusing further identical calls cannot make the session unfinishable — spending the budget always lets the call through, and a repaired environment is discovered by exactly that. The PTY remedy is a preset swap, which happens between sessions, and the native-init remedy is a launch fix on the user's side — whose one in-session part, rewriting an MSYS2 command, the model does by calling a different command, which has a different call key and is therefore never the call being refused. Refusing calls in those families could only pad a session that is already unable to do the thing being refused.
Install
npm install @argszero/cordis-plugin-sandbox-grant-advisor
Mount it by adding the patch to your profile, or apply the shipped
cordis.patch.yml:
- insert:
- id: sandbox-grant-advisor
name: '@argszero/cordis-plugin-sandbox-grant-advisor'
Configuration
| option | default | meaning |
|---|---|---|
enforceAfter |
0 |
Provisioning failures in one agent after which an identical, already-failing call is refused before dispatch. 0 disables the blocking half entirely. |
maxDenials |
2 |
Denials one agent may spend per episode of brokenness. Bounded on purpose; re-armed when a watched call finally succeeds. |
include |
[] |
Tool-name wildcard patterns to watch; empty means every tool. |
exclude |
[] |
Tool-name wildcard patterns never watched. |
href |
— | URL quoted in the advisory as the upstream thread, instead of the discussion numbers. |
- set:
- id: sandbox-grant-advisor
config:
enforceAfter: 3
include / exclude narrow both families: an untracked call is
transparent to the plugin entirely, so a watched-out shell call produces no PTY
advisory (and no withholding note either — the configuration said "not our
story", which is different from "we could not tell").
What it deliberately refuses to explain
Recognition is narrow, because a classifier that names the wrong cause is worse than one that stays silent.
- Only the two
...NamedSecurityInfoWoperations are classified.SetEntriesInAclWmerges access entries in process memory — there is no object and no rights involved — so its failure is not an ACL-permission problem, and neither are theLocalFree,LockFileEx,SetConsoleCtrlHandlerorSetEnvironmentVariableWfailures thrown by the same package. - The PTY family is matched on a whole line, not a substring. The producer's
message is the sentence (
PTY shell exited during startup) with no detail field at all, so any longer line that merely contains it is something quoting it — a transcript, a log a failing command printed, a pasted issue body — and the harness is not the producer. - The sibling throw is not classified.
PTY shell did not reach readiness before startup timeoutmeans the shell started and then did not reach a prompt: a different cause space (a slow or blocked shell) with a different remedy. - The Win32 code is kept, not flattened.
ERROR_ACCESS_DENIED(5) is the case the documented prerequisite explains; another code gets a different paragraph that says so instead of borrowing the same sentence. - A successful command whose output contains the line is not a failure. The gate is the result's error state, not the presence of the text — reading a log file that quotes the error must not trigger advice.
- Only
STATUS_DLL_INIT_FAILEDis classified, and only from the canonical value.0xC0000409is the Cygwin/MSYS2 runtime's deliberate fast-fail (a different mechanism with a different story) and0xC0000135is a missing DLL (a packaging problem, not a sandbox one); both are refused, as is exit127, which upstream's runner-failure rule already owns. And because the code is compared as a number inToolExecutionSuccess.value, a command printing[exit code: -1073741502], or any value that is not the shipped shell projection (kind: 'foreground'), is not this family — a line of text can never be mistaken for a loader status. - Only one advisory per agent, per family. The environment is explained once; repeating it per failed command would be noise competing with the failure itself.
Honest boundaries
- The Windows path itself cannot be witnessed on macOS, where this plugin
was built. What the test suite proves is the decision layer — classification
of all three families (the third from the producer's own canonical value,
built by the suite with the reported
-1073741502and the report's stderr line), the once-per-agent-per-family rule, the sandbox-mode gate and its fail-closed behaviour, the fail-fast budget and its self-feeding guard, and the wiring to a real cordisContextand the realToolRuntime— driven by fixtures that throw the producers' exact error shapes (Win32Error,packages/subprocess/win32-process/src/errors.ts; the terminal throws,packages/terminal/terminal-bash/src/{index,session}.ts). It does not prove thaticacls ... :(OI)(CI)Ffixes a given machine, nor that a given Windows host reproduces the PTY startup failure; those are the user's one-line experiment and the reporter's own control, and both advisories say where they stop. The native-init family is the one that needs no Windows to be faithful, because what it reads is a number inside a JSON value. - It repairs nothing and elevates nothing. If the directory really is
Full-control for the caller, the remaining ACL hypothesis is
SeSecurityPrivilege— i.e. the backend's documented prerequisite would be wrong. That is an upstream question; the advisory states the discriminator rather than assuming the answer. - Delivery to the model is the agent loop's.
additionalContextsare ferried on the settled result here and appended as durable user-role events byagent-loop; a directctx.tools.execute()caller with no agent gets no advisory (and no agent to explain anything to). - It complements
@argszero/cordis-plugin-repeat-guard-escalation, it does not replace it. That guard keys on call identity (identical arguments retried); this one keys on the environment signature, which is how several different commands share one cause. Mounting both is sensible. - The plugin cannot see the launcher half of
#7735. The Low label's other side effect — the shell's publisher confirmation before launching a Low-integrity.bat/.cmd/.exe— never appears in a tool result, so it is outside the seam this plugin subscribes to. The report and its proposed fix stay with the maintainers; all this plugin can do is explain the provisioning failure that shares its root. - The real fix is upstream, in all three families. For the ACL failure,
grantWritealready computeshasExactGrant/hasExactDeny/hasExactLabeland discards which one was false, so the diagnostic that turns a 52-minute detour into one line belongs at that site. For the PTY failure, the startup path should either report "this sandbox mode is incompatible with the PTY backend" or fall back to a one-shot shell. For the native-init death, the runner should be launched with the environment its own execution needs (ELECTRON_RUN_AS_NODE=1whenargv[0]is an Electron binary — [#7876]'s three candidate fixes) or refuse, in a checkable way, an MSYS2 program under a restricted token. This plugin is the stopgap for all three.
Compatibility
Harness peers — all three carry the same range, quoted in full on purpose (a partially quoted range admits fewer lines than the published one):
@deepseek-ai/dsh-llm@>=0.1.2-rc.1 <0.2.0 || >=0.1.3-alpha.2 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0 || >=0.1.7-alpha.1 <0.2.0— the only runtime import:createUserMessageandboundContextSummary.@deepseek-ai/dsh-agent@>=0.1.2-rc.1 <0.2.0 || >=0.1.3-alpha.2 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0 || >=0.1.7-alpha.1 <0.2.0and@deepseek-ai/dsh-tools@>=0.1.2-rc.1 <0.2.0 || >=0.1.3-alpha.2 <0.2.0 || >=0.1.5-alpha.1 <0.2.0 || >=0.1.6-alpha.1 <0.2.0 || >=0.1.7-alpha.1 <0.2.0— imported for their types only (Agent,ToolExecution,PostToolDecision, …). Nothing is loaded from them at runtime, but the shippedlib/types/index.d.tsstill names them, so a consumer on a line outside this range fails to typecheck against this package's own declarations; that is a compatibility claim, and it is declared as a peer for that reason.@deepseek-ai/cordis@^4.0.2.
@deepseek-ai/dsh-sandbox-policy is not a peer: the PTY family's mode
lookup is a guarded, structural one (ctx.get('sandboxPolicy')) precisely so a
composition that does not mount the service degrades to silence instead of
failing to load. See src/mode.ts.
Probed at the newest build of every line the range admits — 0.1.2-rc.1,
0.1.3-alpha.2, 0.1.5-rc.3, 0.1.6-alpha.2, 0.1.7-rc.2 (the newest build of
the line the later Windows reports ran on) — with npm run test:probe-lines,
which derives those builds from this range, installs each one from the registry
into a scratch tree and runs the suite against it. 0.1.7-rc.1, the build the
third report ran, is admitted by the same || segment and was probed while it was
the newest of that line.
The whole set is re-probed whenever this package's source changes rather than
carried over from an earlier version: the range is a claim about this build of
the plugin, so 0.6.0 re-ran all five lines above. A line whose probe fails is
removed from the range rather than left claimed. The scratch tree's resolved
versions are the ones to read back when a probe is quoted as evidence — the probe
script pins them by exact version, and --keep leaves the tree in place to check.
The mode lookup stays guarded through this version too: the native-init family
needs the same resolved mode as the PTY family, and it takes it from the same
ctx.get('sandboxPolicy') capability guard, so a composition without the service
degrades to a disclosed silence rather than failing to load.
Development
npm install
npm test # tsc, then the suite (real cordis + real ToolRuntime)
npm run test:inject # defect injection: mutate the source, rebuild, require the suite to go red
npm run test:probe-lines # install the newest build of each admitted line and run the suite against it
npm run test:probe-lines -- 0.1.7-rc.1 # one line only
The suite is mostly control arms: a guard that explains the wrong failure, or refuses a call that would have worked, is worse than one that stays silent.
test:inject exists because an arm nobody has seen fail proves nothing. It
mutates the decision layer one defect at a time — the mode gate removed, the
PTY message matched as a substring, the preset remedy pointed back at the dead
legacy directory, two families collapsed into one bookkeeping slot, the advisory
delivered per call instead of per agent, the loader-status read moved onto the
error path, the family keyed on the rendered text — and requires that specific
arms fail. It reports SILENT ARMS: none when every arm bites, restores the
source in a finally, and prints EQUIVALENT (with the reason) for a mutation
the current runtime cannot distinguish rather than counting it as a pass.