@argszero/cordis-plugin-sandbox-grant-advisor 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +287 -24
- package/cordis.patch.yml +82 -4
- package/lib/advice.js +322 -12
- package/lib/index.js +128 -24
- package/lib/mode.js +60 -8
- package/lib/signature.js +202 -1
- package/lib/types/advice.d.ts +107 -9
- package/lib/types/index.d.ts +48 -9
- package/lib/types/mode.d.ts +34 -0
- package/lib/types/signature.d.ts +189 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @argszero/cordis-plugin-sandbox-grant-advisor
|
|
2
2
|
|
|
3
3
|
Turns a sandbox environment failure that has **no path forward** into a
|
|
4
|
-
diagnosis the model — and the user reading the transcript — can act on.
|
|
4
|
+
diagnosis the model — and the user reading the transcript — can act on. Four
|
|
5
5
|
signatures, one mechanism:
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws) # Windows workspace ACL
|
|
9
9
|
PTY shell exited during startup # persistent shell × confining mode
|
|
10
10
|
[exit code: -1073741502] (0xC0000142) # a confined child that never started
|
|
11
|
+
[exit code: 1] sandbox: { mode: "workspace-write", denied: true }
|
|
12
|
+
# denied INSIDE the workspace
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
**This plugin is the stopgap for "the error does not name the outstanding
|
|
@@ -15,7 +17,7 @@ condition".** It repairs nothing: no ACL is written, no privilege is requested,
|
|
|
15
17
|
nothing is elevated, no environment variable is set for another process, no
|
|
16
18
|
preset is installed and no mode is changed.
|
|
17
19
|
|
|
18
|
-
## The
|
|
20
|
+
## The four failures it recognizes
|
|
19
21
|
|
|
20
22
|
The first two are recognized on the public **`tools/post-execute`** waterfall
|
|
21
23
|
(`@deepseek-ai/dsh-tools`) from the failure text. That seam is the one that has
|
|
@@ -24,9 +26,9 @@ all three of what a diagnosis needs: the failure
|
|
|
24
26
|
`isError` result), an agent identity to attribute it to (`exec.agent`), and a
|
|
25
27
|
channel that speaks to the model in the same step (`PostToolDecision`'s
|
|
26
28
|
`additionalContexts`, which the agent loop turns into a durable user-role message
|
|
27
|
-
— `packages/core/agent-loop/src/tool-calls.ts`). The third
|
|
28
|
-
**same seam** from the canonical value of a result the pipeline
|
|
29
|
-
*success*, for
|
|
29
|
+
— `packages/core/agent-loop/src/tool-calls.ts`). The third and fourth are
|
|
30
|
+
recognized at the **same seam** from the canonical value of a result the pipeline
|
|
31
|
+
calls a *success*, for reasons §3 and §4 give in full.
|
|
30
32
|
|
|
31
33
|
That seam, not `ctx.sandbox.confine`: `confine(argv, policy, signal)` sees the
|
|
32
34
|
confinement failure too, but its signature carries no agent, so a wrapper could
|
|
@@ -44,6 +46,11 @@ inherited `Authenticated Users: Modify` is the whole of their access). In each o
|
|
|
44
46
|
every sandboxed command fails the same way, before it runs, and the error names
|
|
45
47
|
neither the missing right nor a remedy.
|
|
46
48
|
|
|
49
|
+
Two further reports — [discussion #8312] and [discussion #8314] — describe the
|
|
50
|
+
other end of the same backend: what its grant leaves behind *after* it applies.
|
|
51
|
+
The advisory states that too (see [What the grant leaves behind](#what-the-grant-leaves-behind-added-in-0100)),
|
|
52
|
+
because the advisory is the thing handing over the command that applies the grant.
|
|
53
|
+
|
|
47
54
|
`#8232` contributes two facts about the *shape* of the failure rather than its
|
|
48
55
|
cause, and both are in the advisory now. One is that the failure belongs to the
|
|
49
56
|
**workspace, not the command**: there a `Get-Date` failed exactly like anything
|
|
@@ -229,7 +236,27 @@ What will NOT fix it on its own — both look like the right move, and both were
|
|
|
229
236
|
makes you the owner, and ownership's implicit rights are READ_CONTROL and WRITE_DAC only — so it
|
|
230
237
|
supplies the DACL half and still not WRITE_OWNER, the right this call needs.
|
|
231
238
|
icacls "D:\ws" /reset /T /C
|
|
232
|
-
restores inheritance, and inheritance is what supplied the Modify-only ACE above.
|
|
239
|
+
restores inheritance, and inheritance is what supplied the Modify-only ACE above. Where it strips the last
|
|
240
|
+
entry naming you, the directory ends up in exactly the state the diagnosis above describes — its only access
|
|
241
|
+
the inherited `Authenticated Users:(M)` (#8314 measured that follow-on failure).
|
|
242
|
+
|
|
243
|
+
What the grant leaves behind, once it applies — worth knowing before you run the command above, because the
|
|
244
|
+
backend does not take it back:
|
|
245
|
+
The three entries are STANDING, deliberately, and nothing revokes them. ... They outlive the session and the
|
|
246
|
+
harness exiting (`sandbox-windows-acl/src/grant.ts`, `src/index.ts`).
|
|
247
|
+
The Low integrity label is INHERITABLE (`(OI|CI)`) and it lives in the SACL — which is why the `icacls /reset`
|
|
248
|
+
above does not take it off: that command rebuilds the DACL. Windows starts a process at the minimum of the
|
|
249
|
+
user's and the program's integrity, so anything started from a tree the harness has written to runs at LOW
|
|
250
|
+
integrity, and none of the symptoms names DSH (#8312 collects them): ... (#7709), ... (#8175), ... (#7735).
|
|
251
|
+
It can also leave the workspace. An NTFS hard link is a SECOND NAME for one file object, so both names share
|
|
252
|
+
one security descriptor — and a pnpm workspace is largely hard links (`node_modules` pointing into a
|
|
253
|
+
content-addressed store on the same volume). ... `vite build` unable to remove its own temp file, `pnpm
|
|
254
|
+
install` unable to replace a hook (#8314 measured the whole chain). ...
|
|
255
|
+
It is also why the label is not simply removable here: ... This advisory hands over no removal command: the
|
|
256
|
+
maintainers' own skill does not, and an unverified one would be the defect this plugin exists to answer.
|
|
257
|
+
None of this makes the command above the wrong move — without it, nothing sandboxed runs in this workspace. It
|
|
258
|
+
is what the harness does to a directory it has been pointed at, and it is worth knowing before rather than
|
|
259
|
+
discovering it as a broken build in some other project later.
|
|
233
260
|
|
|
234
261
|
One thing to know before asking for a weaker grant, because that is the next idea after this diagnosis —
|
|
235
262
|
and it is not a smaller version of the same grant:
|
|
@@ -256,6 +283,54 @@ unconditional one-liner was printed, with a sentence noting it assumed
|
|
|
256
283
|
ownership; that would have sent the second environment to a command that is
|
|
257
284
|
denied — the same defect this plugin exists to answer.
|
|
258
285
|
|
|
286
|
+
#### What the grant leaves behind (added in 0.10.0)
|
|
287
|
+
|
|
288
|
+
[Discussion #8312] and [discussion #8314] report the other half of this backend,
|
|
289
|
+
and the advisory now states it — before the reader runs the command it is being
|
|
290
|
+
handed, because that command is what makes the backend's grant apply:
|
|
291
|
+
|
|
292
|
+
- **The three entries are standing, by design, and nothing revokes them.** The
|
|
293
|
+
workspace grant is a *reuse cache*: the backend's dispose path revokes the
|
|
294
|
+
revocable (temp) grants and leaves the workspace edits, and its fail-closed
|
|
295
|
+
cleanup says the same in plainer words — standing ACEs "are NOT revoked — they
|
|
296
|
+
are the intended end state (the reuse cache), not an error artifact"
|
|
297
|
+
(`src/grant.ts`, `src/index.ts`). They outlive the session and the harness
|
|
298
|
+
exiting. ([#8312] arrived at this from the README's own description of the
|
|
299
|
+
cache; the source is quoted in the advisory.)
|
|
300
|
+
- **The Low integrity label is inheritable, and it lives in the SACL.** That is
|
|
301
|
+
why `icacls /reset` — already listed as a non-fix for a different reason — does
|
|
302
|
+
not remove it: the command rebuilds the DACL. Windows starts a process at
|
|
303
|
+
`min(user, image)` integrity, so anything started from a tree the harness has
|
|
304
|
+
written to runs at Low integrity, and none of the symptoms points at DSH:
|
|
305
|
+
an Electron/Chromium app exiting `0x80000003` with no output ([#7709]),
|
|
306
|
+
msbuild / dotnet / npm refusing or warning about the files as if they came from
|
|
307
|
+
the Internet when no `Zone.Identifier` exists ([#8175]), a double-clicked
|
|
308
|
+
`.exe` / `.cmd` reporting "publisher could not be verified" ([#7735]).
|
|
309
|
+
- **It can leave the workspace.** An NTFS hard link is a second name for one
|
|
310
|
+
file object, so both names share one security descriptor — and a pnpm workspace
|
|
311
|
+
is largely hard links (`node_modules` pointing into a content-addressed store
|
|
312
|
+
on the same volume). The inheritable label therefore lands on the *store's*
|
|
313
|
+
objects and stays there, after which every project building from that store
|
|
314
|
+
gets executables that start at Low integrity and failures that name the build
|
|
315
|
+
tool ([#8314] measured the whole chain: `vite build` unable to remove its own
|
|
316
|
+
temp file, `pnpm install` unable to replace a hook). The backend's own suite
|
|
317
|
+
pins the reach as a known boundary — *"a workspace hard link lets the grant
|
|
318
|
+
reach an external file object"* (`tests/runner.spec.ts`) — and its README calls
|
|
319
|
+
refusing multiply-linked files unviable for ordinary pnpm installs, which
|
|
320
|
+
leaves the out-of-tree reach open rather than unknown.
|
|
321
|
+
- **No removal command is shipped.** The maintainers' own
|
|
322
|
+
`diagnose-windows-sandbox-acl` skill *reports* `LOW_LABEL` and by design does
|
|
323
|
+
not remove it, and removing an integrity label needs `WRITE_OWNER` — the same
|
|
324
|
+
right this whole failure is about. This project has no Windows host to verify a
|
|
325
|
+
line on, so shipping one would be exactly the defect the rest of this module
|
|
326
|
+
exists to answer; the section says where it stops instead.
|
|
327
|
+
|
|
328
|
+
The section is emitted for **every** ACL class, like the version boundary and for
|
|
329
|
+
the same reason: it is a fact about the package's grant, and that grant is the
|
|
330
|
+
remedy the advisory hands over in all three classes. It also closes with what the
|
|
331
|
+
fact does *not* mean — the command is still the right move, because without it
|
|
332
|
+
nothing sandboxed runs at all.
|
|
333
|
+
|
|
259
334
|
**What is deliberately *not* shipped**: `icacls ... /grant "<user>:(OI)(CI)(WD,WO)"`,
|
|
260
335
|
the two needed rights named explicitly. It is the tighter form and it is
|
|
261
336
|
plausibly correct syntax, but this project has no Windows host to run it on, and
|
|
@@ -287,6 +362,27 @@ built with the **resolved** mode the failing call actually ran under — from
|
|
|
287
362
|
`ctx.sandboxPolicy.resolve({ session })`, the same resolver the terminal layer
|
|
288
363
|
calls before spawning, with the same session.
|
|
289
364
|
|
|
365
|
+
**Inside a confining mode, the host binary decides** (added in 0.10.0).
|
|
366
|
+
[Discussion #8322] ran the control one level deeper — same runner, same ConPTY,
|
|
367
|
+
every arm — and separated what the mode alone does not: with the runner hosted by
|
|
368
|
+
a plain console-subsystem `node.exe`, the confined shell starts and its prompt and
|
|
369
|
+
shell-integration marks are correct; with the runner hosted by the packaged
|
|
370
|
+
desktop's GUI-subsystem Electron executable, the child dies **silently** — zero
|
|
371
|
+
bytes on stdout *and* stderr, and the non-interactive arm exits 0 with everything
|
|
372
|
+
it printed lost. It is the same rule the `0xC0000142` family states for its own
|
|
373
|
+
case: under the restricted token a console can be **inherited but not created**,
|
|
374
|
+
so the binary that owns one (or owns none) is what the arms turn on. That is also
|
|
375
|
+
why the same version behaves differently depending on how it was started: the
|
|
376
|
+
desktop app fails where the Web UI launched from a terminal — whose
|
|
377
|
+
`process.execPath` is a real `node.exe` — is reported working under the same
|
|
378
|
+
confining mode ([#8313], whose sibling report is the `0xC0000142` shape of the
|
|
379
|
+
same host difference). The advisory therefore names the host alongside the mode,
|
|
380
|
+
says the outcome is **deterministic per (session mode × host)** rather than
|
|
381
|
+
intermittent, and adds that the mode which counts is the one the **session
|
|
382
|
+
records**, not the one the environment now holds — a session that recorded the
|
|
383
|
+
confining mode keeps failing after the app is restarted with another mode in its
|
|
384
|
+
environment, while switching it inside the session takes effect at once.
|
|
385
|
+
|
|
290
386
|
The advisory that follows is addressed to **two different readers**:
|
|
291
387
|
|
|
292
388
|
```
|
|
@@ -299,6 +395,15 @@ The `bash` tool is a PERSISTENT PTY session (a shell that stays alive between ca
|
|
|
299
395
|
`workspace-write` — not `danger-full-access`. A confining mode spawns the shell through the sandbox, and there the
|
|
300
396
|
terminal backend cannot create the pseudo-console at all, so the child exits before its first prompt. ...
|
|
301
397
|
|
|
398
|
+
Which sessions fail inside that combination is not chance — it is deterministic per (session mode × the host
|
|
399
|
+
binary carrying the sandbox runner) ... Measured against the desktop build with the same runner and the same
|
|
400
|
+
ConPTY in every arm (#8322):
|
|
401
|
+
- runner hosted by a plain console-subsystem `node.exe` → the confined shell starts ...;
|
|
402
|
+
- runner hosted by the packaged desktop's GUI-subsystem Electron executable ... → the child dies silently ...
|
|
403
|
+
The rule behind both this and the `0xC0000142` family ... is the one stated there for its own case: under the
|
|
404
|
+
restricted token a console can be INHERITED but not CREATED ... And the mode that decides is the one the SESSION
|
|
405
|
+
records, not the one the environment now holds ...
|
|
406
|
+
|
|
302
407
|
Do NOT retry, and do not look for a command that fixes it: every attempt will fail identically, and there is no
|
|
303
408
|
shell to run a command in. Use your file read/write tools instead, and hand the choice below to the user.
|
|
304
409
|
|
|
@@ -309,8 +414,11 @@ What unblocks the session — the user's decision, not the model's:
|
|
|
309
414
|
`$DSH_HOME/cordis.patch.yml` for every profile — replacing its `persistent-shell` group with
|
|
310
415
|
`@deepseek-ai/dsh-tool-pwsh` (a one-shot subprocess, no PTY); the patch layer is yours, so an upgrade
|
|
311
416
|
will not overwrite it; or
|
|
312
|
-
3. run the session
|
|
313
|
-
|
|
417
|
+
3. run the session from a host that owns a console instead of the packaged desktop app — the Web UI started
|
|
418
|
+
from a terminal (`process.execPath` is a real `node.exe` there) was reported working under the same
|
|
419
|
+
confining mode and the same version (#8313); or
|
|
420
|
+
4. run the session with `danger-full-access`, which drops the very confinement the sandbox exists to give.
|
|
421
|
+
Prefer 1 to 3.
|
|
314
422
|
```
|
|
315
423
|
|
|
316
424
|
The model's instruction is to **stop** — not to run a command (there is no shell
|
|
@@ -331,9 +439,10 @@ the advisory never names the dead directory.
|
|
|
331
439
|
|
|
332
440
|
### 3. A confined child that never started (`native-init`)
|
|
333
441
|
|
|
334
|
-
|
|
335
|
-
app)
|
|
336
|
-
the
|
|
442
|
+
Five reports of one exit code: [`#7876`] and [`#8193`] (the packaged desktop
|
|
443
|
+
app), [`#8313`] (the same version and the same mode run as the desktop app versus
|
|
444
|
+
the Web UI launched from a terminal, which works) and [`#7877`] (MSYS2 / Git
|
|
445
|
+
Bash) — and [`#8208`], which found the mechanism the console cases share. All are `0xC0000142`
|
|
337
446
|
`STATUS_DLL_INIT_FAILED` — the Windows
|
|
338
447
|
loader terminated the process while it was initializing its native images, i.e.
|
|
339
448
|
**before the program's entry point**. A command that ran and then failed exits
|
|
@@ -514,6 +623,110 @@ the single shape that happened to be measured first. It never
|
|
|
514
623
|
offers `danger-full-access` as a fix and never suggests a sandbox setting be
|
|
515
624
|
relaxed.
|
|
516
625
|
|
|
626
|
+
### 4. Denied inside the workspace (`workspace-denial`, added in 0.11.0)
|
|
627
|
+
|
|
628
|
+
[#423] is one report and its own follow-up, and it is the **other end of the
|
|
629
|
+
backend §1 is about**. There the workspace grant could not be applied at all and
|
|
630
|
+
every command died before it ran; here the grant **was** applied — on the
|
|
631
|
+
workspace root, once — and part of the tree still refuses writes, forever. The
|
|
632
|
+
report's shape: under `workspace-write` on Windows, a command writing into a
|
|
633
|
+
subdirectory that was created or **moved in from outside** the session (an
|
|
634
|
+
installer, an editor, another harness running under its own account) is denied,
|
|
635
|
+
while the same command against a directory the harness itself created succeeds.
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
[exit code: 1] sandbox: { mode: "workspace-write", denied: true }
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**The signature is a value, not a message**, and this family is invisible from
|
|
642
|
+
the error path for the same structural reason §3 is: a denied command exits
|
|
643
|
+
nonzero, and the shipped shell tools report a nonzero exit as a finished run
|
|
644
|
+
rather than as `isError`
|
|
645
|
+
(`packages/shell/tool-pwsh/src/render.ts` reports `[exit code: N]` and drops a
|
|
646
|
+
denial marker). The fact therefore arrives in `ToolExecutionSuccess.value`,
|
|
647
|
+
where `tool-bash` / `tool-pwsh` project what the sandbox executor stamped
|
|
648
|
+
(`packages/shell/bash-sandbox/src/index.ts`, `pwsh-sandbox`): the mode the call
|
|
649
|
+
actually ran under, whether the backend's own refusal dialect appears in the
|
|
650
|
+
**captured stderr**, and the enforcement that applied. Reading the executors'
|
|
651
|
+
own stamp rather than a sentence means this plugin is reporting the harness's
|
|
652
|
+
reading of its own sandbox, not a guess about a line of output.
|
|
653
|
+
|
|
654
|
+
**What the advisory says.** Four things, and one it refuses:
|
|
655
|
+
|
|
656
|
+
- **That retrying is provably useless.** The host-side grant is written once, on
|
|
657
|
+
the workspace **root**, and relies on Windows ACE inheritance to reach the
|
|
658
|
+
tree beneath it. Writing an inherited ACE into an *already-existing* child
|
|
659
|
+
needs `WRITE_DAC` on that child; where the caller does not hold it, Windows
|
|
660
|
+
skips the child silently — no error, no return value, no log line. The backend
|
|
661
|
+
then checks only the root (`hasExactGrant(workspaceRoot)` in
|
|
662
|
+
`packages/sandbox/sandbox-windows-acl/src/acl.ts`) and returns early once the
|
|
663
|
+
grant is there, which it is from the first call onwards. The descendants that
|
|
664
|
+
missed the propagation are never revisited — not later in this session, not in
|
|
665
|
+
any later one.
|
|
666
|
+
- **Which objects miss it, and how many.** It is a fact about *who created
|
|
667
|
+
them*: objects the harness creates inherit the ACE, and objects that already
|
|
668
|
+
existed do not. [#423] measured 170 of 729 objects missing it, **including
|
|
669
|
+
root-level files** — so "write at the workspace root instead" is not a safe
|
|
670
|
+
move either.
|
|
671
|
+
- **The discriminator, and its second half.** The advisory prints the path it
|
|
672
|
+
keyed on beside the root it tested it against, so the reader can audit the
|
|
673
|
+
claim instead of taking a statement about two strings on faith. Then the
|
|
674
|
+
measurement a single check gets wrong: reading and listing use the **normal**
|
|
675
|
+
token while writing and deleting use the **restricted** (low-integrity) one,
|
|
676
|
+
and both sides must pass — so an object whose DACL names only
|
|
677
|
+
`Administrators`/`SYSTEM` plus the capability SID is refused on the read side
|
|
678
|
+
too, and looks fine to a check that merely greps for the capability SID.
|
|
679
|
+
[#423] measured exactly that on a `.cache` directory.
|
|
680
|
+
- **The second measured variant of the same shape.** The coverage that is
|
|
681
|
+
missing can be the mandatory-integrity **label** rather than a DACL entry —
|
|
682
|
+
reported in the same thread on 2026-09-29, against a `0.2.0-rc.1` install,
|
|
683
|
+
under the same root-only short-circuit. From inside a session the two are
|
|
684
|
+
indistinguishable; from outside, the repository's own diagnosis skill
|
|
685
|
+
separates them (`diagnose-windows-sandbox-acl`, 0.2.0 and later, reports
|
|
686
|
+
`hasExactDeny()` for the DACL half and `LOW_LABEL` — `S-1-16-4096` — for the
|
|
687
|
+
label half).
|
|
688
|
+
- **No repair command.** The obvious one, a recursive `icacls /grant` for the
|
|
689
|
+
capability SID, is refused by Windows itself with `ERROR_NONE_MAPPED` (1332) —
|
|
690
|
+
the tool cannot map that SID to a name, so a grant that must name it never
|
|
691
|
+
reaches the child. The line that *would* work needs `WRITE_DAC` on the object,
|
|
692
|
+
which is the right in question. The advisory states the mechanism, names the
|
|
693
|
+
ceiling, and says out loud that it prints no command because this project has
|
|
694
|
+
no Windows host to verify one on — the same standard §1's standing-grant
|
|
695
|
+
section is held to, and for the same reason.
|
|
696
|
+
|
|
697
|
+
**What it refuses to explain, and why that is the design.** A denial is the
|
|
698
|
+
*sanctioned* outcome in three other situations, and advising about a missing
|
|
699
|
+
inherited grant in any of them would be a confidently wrong cause:
|
|
700
|
+
|
|
701
|
+
- **Outside the workspace** — a confining sandbox denying a path outside its
|
|
702
|
+
writable roots is the whole point, and the denial surface's one-shot escalation
|
|
703
|
+
offer is correct there.
|
|
704
|
+
- **Under `read-only`** — that mode denies every write by construction, so an
|
|
705
|
+
in-workspace denial under it is the mode working.
|
|
706
|
+
- **A runner failure** — the executor refuses to call a run denied when the
|
|
707
|
+
runner itself failed, and such a value is left alone for the same reason.
|
|
708
|
+
|
|
709
|
+
What is left is exactly the anomaly: a denial under `workspace-write` of a path
|
|
710
|
+
inside the session's own workspace. That is why the family reads the mode off
|
|
711
|
+
the **value** (the executor stamped the mode it actually ran under, so no policy
|
|
712
|
+
lookup can disagree with it), tests containment against the root the policy
|
|
713
|
+
resolver reports, and speaks only when **every** absolute path the command's own
|
|
714
|
+
arguments name lies under that root. A command that names an outside path as
|
|
715
|
+
well — an interpreter under `C:\Program Files`, an output directory on another
|
|
716
|
+
volume — is refused rather than guessed at, and so is a command naming only
|
|
717
|
+
relative paths: in both cases the plugin cannot say *which* path was denied, and
|
|
718
|
+
silence is the fail-closed direction. The platform gate is real here and cannot
|
|
719
|
+
be dropped: the mechanism is ACE inheritance, which no other backend has.
|
|
720
|
+
|
|
721
|
+
**On a denial it cannot finish placing** — a root the policy resolver does not
|
|
722
|
+
report, or no mounted policy service at all — the plugin withholds the advisory
|
|
723
|
+
and says so once on the host log. That is the disclosure rule §1's mode gate
|
|
724
|
+
follows as well: silence alone would make "the sandbox is not the cause"
|
|
725
|
+
indistinguishable from "this plugin could not tell".
|
|
726
|
+
|
|
727
|
+
[#423] is a Discussion (the repository has issues disabled), and the reply
|
|
728
|
+
covering this family is posted there.
|
|
729
|
+
|
|
517
730
|
## What it does with a recognized failure
|
|
518
731
|
|
|
519
732
|
1. **One durable advisory per agent, per family.** An agent that hits two
|
|
@@ -524,10 +737,12 @@ relaxed.
|
|
|
524
737
|
`warn` line, so the fact survives outside the transcript too.
|
|
525
738
|
2. **A disclosure when it withholds.** The PTY and native-init advisories are
|
|
526
739
|
only sent when the
|
|
527
|
-
resolved mode actually confines
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
740
|
+
resolved mode actually confines, and the workspace-denial advisory only when
|
|
741
|
+
the workspace root is resolvable — the fact its containment claim is tested
|
|
742
|
+
against. If the mode is `danger-full-access`, or either fact cannot be
|
|
743
|
+
resolved at all (no `sandboxPolicy` service mounted, no agent session, a
|
|
744
|
+
resolver that throws, a policy without a root), the failure is left exactly as
|
|
745
|
+
it was **and the host log says so once**. Silence alone would make "the sandbox is
|
|
531
746
|
not the cause" and "this plugin could not tell" indistinguishable from the
|
|
532
747
|
outside. Withholding is never a guess: an unresolvable mode is *not* an
|
|
533
748
|
invitation to fall back to the deployment default.
|
|
@@ -542,8 +757,8 @@ relaxed.
|
|
|
542
757
|
refused twice — while a session can always make progress by spending the
|
|
543
758
|
budget it has.
|
|
544
759
|
|
|
545
|
-
**Why the blocking half
|
|
546
|
-
|
|
760
|
+
**Why the blocking half covers one family only** (it is ACL-only by
|
|
761
|
+
construction, in the parameter type): the ACL remedy is a
|
|
547
762
|
command
|
|
548
763
|
the user can run *while the session continues*, so refusing further identical
|
|
549
764
|
calls cannot make the session unfinishable — spending the budget always lets
|
|
@@ -552,8 +767,11 @@ relaxed.
|
|
|
552
767
|
native-init remedy is a launch fix on the user's side — whose one in-session
|
|
553
768
|
part, rewriting an MSYS2 command, the model does by calling a *different*
|
|
554
769
|
command, which has a different call key and is therefore never the call being
|
|
555
|
-
refused.
|
|
556
|
-
|
|
770
|
+
refused. The workspace-internal denial is not covered either, and for a
|
|
771
|
+
stronger version of the same reason: its remedy is not a command at all — the
|
|
772
|
+
object was skipped when the grant was written and nothing revisited it — so
|
|
773
|
+
refusing calls could only pad a session that is already unable to do the thing
|
|
774
|
+
being refused.
|
|
557
775
|
|
|
558
776
|
## Install
|
|
559
777
|
|
|
@@ -627,6 +845,15 @@ than one that stays silent.
|
|
|
627
845
|
`[exit code: -1073741502]`, or any value that is not the shipped shell
|
|
628
846
|
projection (`kind: 'foreground'`), is not this family — a line of text can
|
|
629
847
|
never be mistaken for a loader status.
|
|
848
|
+
- **A denial is not this family unless the plugin can place it.** A denial under
|
|
849
|
+
`read-only`, under `danger-full-access`, or on a host that is not Windows; a
|
|
850
|
+
value the executor marked as a runner failure (`denied` is not set when the
|
|
851
|
+
runner itself failed); a command naming a path outside the workspace; a command
|
|
852
|
+
naming an outside path as well as an inside one; and a command naming only
|
|
853
|
+
relative paths — all six are refused, and the first three are the *sanctioned*
|
|
854
|
+
outcomes rather than puzzles. The plugin reads the executors' own stamp rather
|
|
855
|
+
than a sentence, so a command whose output contains `denied: true` is not this
|
|
856
|
+
family either.
|
|
630
857
|
- **Only one advisory per agent, per family.** The environment is explained
|
|
631
858
|
once; repeating it per failed command would be noise competing with the
|
|
632
859
|
failure itself.
|
|
@@ -635,9 +862,11 @@ than one that stays silent.
|
|
|
635
862
|
|
|
636
863
|
- **The Windows path itself cannot be witnessed on macOS**, where this plugin
|
|
637
864
|
was built. What the test suite proves is the decision layer — classification
|
|
638
|
-
of all
|
|
865
|
+
of all four families (the third from the producer's own canonical value,
|
|
639
866
|
built by the suite with the reported `-1073741502` and the report's stderr
|
|
640
|
-
line
|
|
867
|
+
line; the fourth from the executors' own stamp, built by the suite with the
|
|
868
|
+
mode, the `denied` flag and the refusal dialect the executor matches), the
|
|
869
|
+
once-per-agent-per-family rule, the sandbox-mode gate
|
|
641
870
|
and its fail-closed behaviour, the fail-fast budget and its self-feeding
|
|
642
871
|
guard, and the wiring to a real cordis `Context` and the real `ToolRuntime` —
|
|
643
872
|
driven by fixtures that throw the producers' exact error shapes
|
|
@@ -647,7 +876,25 @@ than one that stays silent.
|
|
|
647
876
|
a given Windows host reproduces the PTY startup failure; those are the user's
|
|
648
877
|
one-line experiment and the reporter's own control, and both advisories say
|
|
649
878
|
where they stop. The native-init family is the one that needs no Windows to be
|
|
650
|
-
faithful, because what it reads is a number inside a JSON value.
|
|
879
|
+
faithful, because what it reads is a number inside a JSON value. The
|
|
880
|
+
workspace-denial family is exercised the same way and **does** need the
|
|
881
|
+
platform fact, which the suite supplies by stubbing `process.platform` for the
|
|
882
|
+
arms that need `win32` and restoring it — the plugin reads the real fact rather
|
|
883
|
+
than a config knob, because a knob would be a backdoor into a shipped decision.
|
|
884
|
+
What that proves is the decision layer against the producers' stamped values;
|
|
885
|
+
the ACE-inheritance path itself is still unwitnessed here, and the advisory
|
|
886
|
+
says as much by shipping no repair command.
|
|
887
|
+
- **The standing-grant section is source-level, and the out-of-tree reach is the
|
|
888
|
+
reporter's measurement.** What this section states about the backend's own
|
|
889
|
+
behaviour — that the workspace grant is standing, that nothing in the dispose or
|
|
890
|
+
fail-closed path revokes it, that the Low label is inheritable and lives in the
|
|
891
|
+
SACL — is read off the shipped source and the backend's own suite, and is quoted
|
|
892
|
+
as such. What it states about a hard link carrying the label onto a
|
|
893
|
+
content-addressed store is [#8314]'s measurement on their machine, which is why
|
|
894
|
+
the advisory attributes it instead of asserting it as a property of every
|
|
895
|
+
workspace, and why **no removal command is shipped**: this project has no
|
|
896
|
+
Windows host on which to verify one, and an unverified removal command is the
|
|
897
|
+
same defect this plugin exists to answer.
|
|
651
898
|
- **It repairs nothing and elevates nothing.** If the directory really is
|
|
652
899
|
Full-control for the caller, the remaining ACL hypothesis is
|
|
653
900
|
`SeSecurityPrivilege` — i.e. the backend's documented prerequisite would be
|
|
@@ -667,7 +914,7 @@ than one that stays silent.
|
|
|
667
914
|
outside the seam this plugin subscribes to. The report and its proposed fix
|
|
668
915
|
stay with the maintainers; all this plugin can do is explain the provisioning
|
|
669
916
|
failure that shares its root.
|
|
670
|
-
- **The real fix is upstream, in all
|
|
917
|
+
- **The real fix is upstream, in all four families.** For the ACL failure,
|
|
671
918
|
`grantWrite` already computes `hasExactGrant` / `hasExactDeny` /
|
|
672
919
|
`hasExactLabel` and discards which one was false, so the diagnostic that turns
|
|
673
920
|
a 52-minute detour into one line belongs at that site. For the PTY failure,
|
|
@@ -676,7 +923,12 @@ than one that stays silent.
|
|
|
676
923
|
the runner should be launched with the environment its own execution needs
|
|
677
924
|
(`ELECTRON_RUN_AS_NODE=1` when `argv[0]` is an Electron binary — [`#7876`]'s
|
|
678
925
|
three candidate fixes) or refuse, in a checkable way, an MSYS2 program under a
|
|
679
|
-
restricted token.
|
|
926
|
+
restricted token. For the workspace-internal denial the site is the same
|
|
927
|
+
`grantWrite`: its early return asks only whether the **root** already carries
|
|
928
|
+
the ACE, so the descendants that missed the propagation are never repaired —
|
|
929
|
+
the check would have to look past the root, or the denial surface would have to
|
|
930
|
+
say *which* path was refused instead of only that one was. This plugin is the
|
|
931
|
+
stopgap for all four.
|
|
680
932
|
|
|
681
933
|
## Compatibility
|
|
682
934
|
|
|
@@ -767,6 +1019,7 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
767
1019
|
[discussion #7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
|
|
768
1020
|
[discussion #7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
|
|
769
1021
|
[discussion #8208]: https://github.com/deepseek-ai/deepseek-harness/discussions/8208
|
|
1022
|
+
[#423]: https://github.com/deepseek-ai/deepseek-harness/discussions/423
|
|
770
1023
|
[#7750]: https://github.com/deepseek-ai/deepseek-harness/discussions/7750
|
|
771
1024
|
[#7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
|
|
772
1025
|
[#7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
|
|
@@ -778,3 +1031,13 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
778
1031
|
[#8193]: https://github.com/deepseek-ai/deepseek-harness/discussions/8193
|
|
779
1032
|
[#8174]: https://github.com/deepseek-ai/deepseek-harness/discussions/8174
|
|
780
1033
|
[#8208]: https://github.com/deepseek-ai/deepseek-harness/discussions/8208
|
|
1034
|
+
[discussion #8312]: https://github.com/deepseek-ai/deepseek-harness/discussions/8312
|
|
1035
|
+
[discussion #8314]: https://github.com/deepseek-ai/deepseek-harness/discussions/8314
|
|
1036
|
+
[discussion #8322]: https://github.com/deepseek-ai/deepseek-harness/discussions/8322
|
|
1037
|
+
[#7709]: https://github.com/deepseek-ai/deepseek-harness/discussions/7709
|
|
1038
|
+
[#7735]: https://github.com/deepseek-ai/deepseek-harness/discussions/7735
|
|
1039
|
+
[#8175]: https://github.com/deepseek-ai/deepseek-harness/discussions/8175
|
|
1040
|
+
[#8312]: https://github.com/deepseek-ai/deepseek-harness/discussions/8312
|
|
1041
|
+
[#8313]: https://github.com/deepseek-ai/deepseek-harness/discussions/8313
|
|
1042
|
+
[#8314]: https://github.com/deepseek-ai/deepseek-harness/discussions/8314
|
|
1043
|
+
[#8322]: https://github.com/deepseek-ai/deepseek-harness/discussions/8322
|
package/cordis.patch.yml
CHANGED
|
@@ -45,6 +45,20 @@
|
|
|
45
45
|
# the confined token is itself lowered to Low, so the directory's Low label is
|
|
46
46
|
# what lets that child write there at all.
|
|
47
47
|
#
|
|
48
|
+
# And it says WHAT THE GRANT LEAVES BEHIND once it applies (#8312, #8314): the
|
|
49
|
+
# three entries are STANDING and nothing revokes them (the backend's own dispose
|
|
50
|
+
# and fail-closed paths leave them — "the intended end state (the reuse cache)"),
|
|
51
|
+
# the Low integrity label is INHERITABLE and lives in the SACL (so `icacls /reset`
|
|
52
|
+
# does not remove it — that rebuilds the DACL), a process starts at
|
|
53
|
+
# min(user, image) integrity, and an NTFS hard link shares one security descriptor
|
|
54
|
+
# with the file object it names — so in a pnpm workspace, whose node_modules are
|
|
55
|
+
# hard links into a content-addressed store, the label can reach OUTSIDE the tree
|
|
56
|
+
# and stay on the store's objects, after which unrelated builds fail in ways that
|
|
57
|
+
# name the build tool and not DSH. The section is stated before the reader runs
|
|
58
|
+
# the command, and it ships NO removal command: the maintainers' own diagnosis
|
|
59
|
+
# skill reports the label and does not remove it, and removing an integrity label
|
|
60
|
+
# needs WRITE_OWNER.
|
|
61
|
+
#
|
|
48
62
|
# Optional config:
|
|
49
63
|
#
|
|
50
64
|
# - set:
|
|
@@ -64,15 +78,23 @@
|
|
|
64
78
|
# this environment, and it is bounded by `maxDenials`, because a plugin that can
|
|
65
79
|
# stop command execution must never be the reason a session cannot finish.
|
|
66
80
|
#
|
|
67
|
-
# A SECOND family is recognized on the same seam (#7638): with the
|
|
68
|
-
# preset and a confining sandbox mode, every shell call dies instantly
|
|
81
|
+
# A SECOND family is recognized on the same seam (#7638, #8322): with the
|
|
82
|
+
# `minimal` preset and a confining sandbox mode, every shell call dies instantly
|
|
83
|
+
# with
|
|
69
84
|
#
|
|
70
85
|
# PTY shell exited during startup
|
|
71
86
|
#
|
|
72
87
|
# The terminal backend cannot create the pseudo-console inside the sandbox, so
|
|
73
88
|
# the child exits before its first prompt; retrying never helps and `minimal`
|
|
74
|
-
# mounts no fallback shell tool.
|
|
75
|
-
#
|
|
89
|
+
# mounts no fallback shell tool. Inside that combination the HOST BINARY decides
|
|
90
|
+
# (#8322, one runner and one ConPTY in every arm): a console-subsystem `node.exe`
|
|
91
|
+
# host starts the confined shell, while the packaged desktop's GUI-subsystem
|
|
92
|
+
# Electron host kills it silently — the same console rule the third family
|
|
93
|
+
# states, where a restricted token can inherit a console but not create one. The
|
|
94
|
+
# outcome is deterministic per (session mode x runner host), and the mode that
|
|
95
|
+
# counts is the one the session records. That advisory states the resolved mode,
|
|
96
|
+
# tells the model to STOP rather than retry, and hands the user the choices — a
|
|
97
|
+
# preset swap, a profile-patch row, or a host that owns a console (#8313); it
|
|
76
98
|
# never names a command to run (there is no shell to run it in) and never names
|
|
77
99
|
# a shell tool the failing composition does not mount. It is sent only when the
|
|
78
100
|
# mode the call actually ran under confines; otherwise the failure is left
|
|
@@ -81,6 +103,62 @@
|
|
|
81
103
|
# above deliberately does NOT cover this family: its remedy is a patch-layer
|
|
82
104
|
# change the user makes between sessions, not a command that repairs the
|
|
83
105
|
# running one.
|
|
106
|
+
#
|
|
107
|
+
# A THIRD family is recognized from a value, not from text (#7876, #7877,
|
|
108
|
+
# #8193, #8208, #8313):
|
|
109
|
+
#
|
|
110
|
+
# [exit code: -1073741502] (0xC0000142 STATUS_DLL_INIT_FAILED)
|
|
111
|
+
#
|
|
112
|
+
# A confined Windows child died while its native images were loading, i.e.
|
|
113
|
+
# before its entry point. It reaches the tool result as an ordinary nonzero
|
|
114
|
+
# exit code — upstream's runner-failure rules admit only exit 127 with the
|
|
115
|
+
# `windows-acl-run: ` signature, so this code is never reclassified and never
|
|
116
|
+
# arrives as an error — which is why a plugin reading only error results is
|
|
117
|
+
# structurally blind to it, and why the model retries a command that cannot
|
|
118
|
+
# start. The read is the shell tool's own canonical value
|
|
119
|
+
# (`ToolExecutionSuccess.value`, `kind: 'foreground'`), so no line of rendered
|
|
120
|
+
# text can be mistaken for it. The advisory states the resolved mode, says the
|
|
121
|
+
# process never reached its entry point, enumerates the measured producers
|
|
122
|
+
# (an MSYS2 / Git-Bash program that cannot create its signal pipe under the
|
|
123
|
+
# restricted token; the sandbox runner hosted by a GUI-subsystem Electron binary
|
|
124
|
+
# or spawned without a console), carries the one conversion the model can make
|
|
125
|
+
# itself (rewrite the work as PowerShell or `cmd`), names the remedy #8193
|
|
126
|
+
# measured — a real node.exe host, which the desktop ships — and says plainly
|
|
127
|
+
# which producers it does not know. Like the second family, it is mode-gated.
|
|
128
|
+
#
|
|
129
|
+
# A FOURTH family is the other half of the FIRST one's backend (#423). There the
|
|
130
|
+
# grant could not be applied at all; here it WAS applied — on the workspace root,
|
|
131
|
+
# once — and Windows ACE inheritance silently skipped the objects whose DACL the
|
|
132
|
+
# caller could not write, so commands writing into a subdirectory created or moved
|
|
133
|
+
# in from outside (an installer, an editor, another harness under its own account)
|
|
134
|
+
# are denied forever while the same command against a directory the harness itself
|
|
135
|
+
# created succeeds. The backend's provisioning check short-circuits on the root
|
|
136
|
+
# (`hasExactGrant` returns early once the root carries the ACE), so those
|
|
137
|
+
# descendants are never revisited. It is read from a value rather than from text,
|
|
138
|
+
# like the third family and for the same reason (a denial exits nonzero and the
|
|
139
|
+
# shipped shell tools report that as a finished run), but it needs no bespoke code:
|
|
140
|
+
# the executors stamp the denial onto the value as
|
|
141
|
+
#
|
|
142
|
+
# sandbox: { mode: "workspace-write", denied: true }
|
|
143
|
+
#
|
|
144
|
+
# produced by matching the backend's own refusal dialect in the captured stderr —
|
|
145
|
+
# so this plugin reads the harness's own reading of its own sandbox. It speaks
|
|
146
|
+
# only when the host is Windows, the stamped mode is `workspace-write`, and EVERY
|
|
147
|
+
# absolute path the command names lies inside the session's workspace: a denial
|
|
148
|
+
# anywhere else (outside the workspace, under `read-only`, a runner failure) is
|
|
149
|
+
# the sandbox working as designed and is left alone. The advisory prints the path
|
|
150
|
+
# it keyed on beside the root it tested it against, says that retrying is provably
|
|
151
|
+
# useless, gives the DISCRIMINATOR — reading/listing use the normal token while
|
|
152
|
+
# writing/deleting use the restricted one, and both sides must pass, so an object
|
|
153
|
+
# whose DACL names only Administrators/SYSTEM plus the capability SID looks granted
|
|
154
|
+
# to a check that just greps for the SID — names the second measured variant of
|
|
155
|
+
# the same shape (the coverage missing on the mandatory-integrity LABEL rather
|
|
156
|
+
# than on the DACL, separated from outside a session by the repository's own
|
|
157
|
+
# `diagnose-windows-sandbox-acl`), and states the Windows ceiling that closes the
|
|
158
|
+
# obvious repair (a recursive `icacls /grant` for a capability SID is refused with
|
|
159
|
+
# ERROR_NONE_MAPPED (1332), because that SID has no name to map). It ships NO
|
|
160
|
+
# repair command, for the reason the standing-grant section ships none: this
|
|
161
|
+
# project has no Windows host to verify a line on.
|
|
84
162
|
|
|
85
163
|
- insert:
|
|
86
164
|
- id: sandbox-grant-advisor
|