@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 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. Three
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 three failures it recognizes
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 is recognized at the
28
- **same seam** from the canonical value of a result the pipeline calls a
29
- *success*, for a reason §3 gives in full.
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 with `danger-full-access`, which drops the very confinement the sandbox exists to give.
313
- Prefer 1 or 2.
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
- Four reports of one exit code: [`#7876`] and [`#8193`] (the packaged desktop
335
- app) and [`#7877`] (MSYS2 / Git Bash) — and [`#8208`], which found the mechanism
336
- the console cases share. All are `0xC0000142`
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. If the mode is `danger-full-access`, or
528
- cannot be resolved at all (no `sandboxPolicy` service mounted, no agent
529
- session, a resolver that throws), the failure is left exactly as it was
530
- **and the host log says so once**. Silence alone would make "the sandbox is
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 does not extend to the two mode-gated families** (it
546
- is ACL-only by construction, in the parameter type): the ACL remedy is a
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. Refusing calls in those families could only pad a session that is
556
- already unable to do the thing being refused.
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 three families (the third from the producer's own canonical value,
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), the once-per-agent-per-family rule, the sandbox-mode gate
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 three families.** For the ACL failure,
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. This plugin is the stopgap for all three.
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 `minimal`
68
- # preset and a confining sandbox mode, every shell call dies instantly with
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. That advisory states the resolved mode, tells
75
- # the model to STOP rather than retry, and hands the user a preset choice — it
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