@argszero/cordis-plugin-sandbox-grant-advisor 0.13.0 → 0.14.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
@@ -46,8 +46,9 @@ inherited `Authenticated Users: Modify` is the whole of their access). In each o
46
46
  every sandboxed command fails the same way, before it runs, and the error names
47
47
  neither the missing right nor a remedy.
48
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.
49
+ Two further reports — [discussion #8312] and [discussion #8314] — and a third,
50
+ [discussion #8412], describe the other end of the same backend: what its grant
51
+ leaves behind *after* it applies.
51
52
  The advisory states that too (see [What the grant leaves behind](#what-the-grant-leaves-behind-added-in-0100)),
52
53
  because the advisory is the thing handing over the command that applies the grant.
53
54
 
@@ -183,6 +184,25 @@ Two consequences follow from that one line:
183
184
  one of two independent layers. The advisory says this instead of a bare "no",
184
185
  because a reader told only "no" reaches for the workaround without knowing what
185
186
  else it has to change.
187
+ 7. **The missing right was confirmed from outside, unelevated** (added in 0.14.0,
188
+ from `#8426`). Everything above is argued from the backend's source; `#8426`
189
+ measured it with none of that. On a directory whose ACL named the account only
190
+ through the inherited `Authenticated Users:(M)` entry, asking that directory
191
+ for a Low mandatory-integrity label — the label half alone, no DACL write, so
192
+ it isolates the right the merged call wants — was **refused**, and the
193
+ identical operation **succeeded** the moment the same account granted itself
194
+ `(OI)(CI)F`, whose mask carries `WRITE_OWNER`, and was reversible from there
195
+ (back to Medium works just as well). No elevation anywhere in it. That makes the
196
+ claim in item 2 a measurement rather than a reading: the missing right is an
197
+ object right on the **directory**, not `SeRelabelPrivilege` and not a token
198
+ privilege. The same report then repaired two real trees the same way — object
199
+ right first, integrity label second, 9521 and 5592 objects, no failures — an
200
+ order that cannot be swapped, since the second step is the one that needs what
201
+ the first supplies, and it is the order the advisory's own remedy already
202
+ follows. The probe itself is quoted here and **not** in the advisory, for the
203
+ reason item 6 of the `#8272` paragraph above gives: a label write is not a
204
+ check, because where the caller already holds Full control it succeeds and
205
+ writes the label. The advisory hands over only the two read-only checks.
186
206
 
187
207
  The grant is materialized lazily, on the first confined call, and **nothing is
188
208
  cached when it throws** — so the same failure repeats per command (850 calls
@@ -653,7 +673,7 @@ the single shape that happened to be measured first. It never
653
673
  offers `danger-full-access` as a fix and never suggests a sandbox setting be
654
674
  relaxed.
655
675
 
656
- ### 4. Denied inside the workspace (`workspace-denial`, added in 0.11.0; the two branches it forks into in 0.13.0)
676
+ ### 4. Denied inside the workspace (`workspace-denial`, added in 0.11.0; the branches it forks into in 0.13.0 and 0.14.0)
657
677
 
658
678
  [#423] is one report and its own follow-up, and it is the **other end of the
659
679
  backend §1 is about**. There the workspace grant could not be applied at all and
@@ -707,7 +727,8 @@ reading of its own sandbox, not a guess about a line of output.
707
727
  `Administrators`/`SYSTEM` plus the capability SID is refused on the read side
708
728
  too, and looks fine to a check that merely greps for the capability SID.
709
729
  [#423] measured exactly that on a `.cache` directory.
710
- - **The fork, and the second branch** (added in 0.13.0, from [#8383]). The
730
+ - **The fork, and the other two branches** (the second added in 0.13.0 from
731
+ [#8383] and its second instance [#8421]; the third in 0.14.0 from [#8409]). The
711
732
  grant and the mandatory-integrity **label** go out in the same single
712
733
  security-descriptor write, and that write lands on the workspace root (plus
713
734
  the session's private temp directory) with **no descendant walk** — so the
@@ -718,27 +739,50 @@ reading of its own sandbox, not a guess about a line of output.
718
739
  matching, `acl.ts:386-388`, label read at `:198-204`) before it returns early,
719
740
  so once the root is labelled the propagation is never attempted again and an
720
741
  unlabelled child is never revisited — the same root-only short-circuit as the
721
- DACL half, on the other half of the same call. From inside a session the two
722
- are indistinguishable, so the advisory forks them on **breadth**, the one fact
723
- the reader already owns: a **handful** of stubborn objects while the rest of
724
- the tree writes normally is the DACL branch, and **nothing below the root
725
- writable at all**, with the root itself the only writable place, is the label
726
- branch. A Low-integrity child may write to a directory only if that
727
- directory's own label is Low — the kernel's no-write-up check runs *in
728
- addition to* the access check — so a DACL that is perfect everywhere changes
729
- nothing. [#8383] measured it on `0.2.0-rc.2` with the decisive control of a
730
- directory created **after** the grant: it inherits the capability ACE marked
731
- `(I)` and still gets no label. (The report's own care is worth keeping: a
732
- *grandchild* directory having no label proves nothing, because inheritance is
733
- per-parent and the intermediate directory carries none — only a **direct**
734
- child of the labelled root is evidence.) From outside a session the
735
- repository's own diagnosis skill separates the halves
736
- (`diagnose-windows-sandbox-acl`, 0.2.0 and later, prints the owner, the
737
- caller's rights and `WRITE_DAC`/`WRITE_OWNER` for the DACL half and `LOW_LABEL`
738
- — `S-1-16-4096` — for this one). And a widened DACL does not clear it either,
739
- because the refusal precedes the ACL: [#8383] confirmed that adding an
740
- explicit `FullControl` entry for the user on a subdirectory still left the
741
- write denied.
742
+ DACL half, on the other half of the same call. From inside a session the
743
+ branches are indistinguishable, so the advisory forks them on **breadth**, the
744
+ one fact the reader already owns: a **handful** of stubborn objects while the
745
+ rest of the tree writes normally is the DACL branch, **nothing below the root
746
+ writable at all** with the root itself the only writable place is the label
747
+ branch, and **the root itself refused** — nothing writable, not even the root —
748
+ is a third branch with a different cause and a different remedy (below). A
749
+ Low-integrity child may write to a directory only if that directory's own label
750
+ is Low — the kernel's no-write-up check runs *in addition to* the access check
751
+ — so a DACL that is perfect everywhere changes nothing. [#8383] measured it on
752
+ `0.2.0-rc.2` with the decisive control of a directory created **after** the
753
+ grant: it inherits the capability ACE marked `(I)` and still gets no label.
754
+ (The report's own care is worth keeping: a *grandchild* directory having no
755
+ label proves nothing, because inheritance is per-parent and the intermediate
756
+ directory carries none — only a **direct** child of the labelled root is
757
+ evidence.) From outside a session the repository's own diagnosis skill
758
+ separates the DACL half from the label half (`diagnose-windows-sandbox-acl`,
759
+ 0.2.0 and later, prints the owner, the caller's rights and
760
+ `WRITE_DAC`/`WRITE_OWNER` for the one and `LOW_LABEL` — `S-1-16-4096` — for the
761
+ other). And a widened DACL does not clear the label half either, because the
762
+ refusal precedes the ACL: [#8383] confirmed that adding an explicit
763
+ `FullControl` entry for the user on a subdirectory still left the write denied.
764
+ - **The third branch, and the one recovery this family has** (added in 0.14.0,
765
+ from [#8409]). The root's own standing grant can be **gone**: the workspace
766
+ root's security descriptor rewritten from outside the harness (an ACL reset, a
767
+ restored descriptor, a tool that re-applies one) leaves nothing writable, the
768
+ root included. It is not a descendant missing an inherited ACE — it is the ACE
769
+ the harness itself wrote that is no longer there — and the reason no retry
770
+ reaches it is one layer **above** the backend's own check: the session-scoped
771
+ provider materializes the root ACE once per **provider lifetime** and keeps the
772
+ root in its own in-memory map, consulting the map instead of the DACL
773
+ thereafter (`sandbox-local/src/index.ts`: the guard at `:403-421`,
774
+ "materializes once per workspace per server lifetime"), so the `hasExactGrant`
775
+ check the other two branches turn on is not even reached again. The
776
+ discriminator is measured on both paths and is a fact about where the cache
777
+ lives, not about one path being more careful: the agentless/runner invocation
778
+ passes no session id (`windowsAclRunnerArgv()`), so the runner owns the DACLs
779
+ and re-applies the grant on **every** spawn — which really does read the DACL —
780
+ and heals by itself, while the desktop session does not. The recovery is the
781
+ only one this family has and it is the **user's**, not a command: restart the
782
+ provider (quit and reopen the desktop app, or open the workspace in a fresh
783
+ one), which empties the map and lets the next provision read the DACL, find the
784
+ ACE absent and write it — the reporter measured exactly that ("restarting the
785
+ desktop brings it back").
742
786
  - **No repair command.** The obvious one, a recursive `icacls /grant` for the
743
787
  capability SID, is refused by Windows itself with `ERROR_NONE_MAPPED` (1332) —
744
788
  the tool cannot map that SID to a name, so a grant that must name it never
@@ -1069,6 +1113,7 @@ the current runtime cannot distinguish rather than counting it as a pass.
1069
1113
  [discussion #8232]: https://github.com/deepseek-ai/deepseek-harness/discussions/8232
1070
1114
  [discussion #8272]: https://github.com/deepseek-ai/deepseek-harness/discussions/8272
1071
1115
  [discussion #8275]: https://github.com/deepseek-ai/deepseek-harness/discussions/8275
1116
+ [discussion #8412]: https://github.com/deepseek-ai/deepseek-harness/discussions/8412
1072
1117
  [discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
1073
1118
  [discussion #7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
1074
1119
  [discussion #7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
@@ -1098,3 +1143,7 @@ the current runtime cannot distinguish rather than counting it as a pass.
1098
1143
  [#8334]: https://github.com/deepseek-ai/deepseek-harness/discussions/8334
1099
1144
  [#8336]: https://github.com/deepseek-ai/deepseek-harness/discussions/8336
1100
1145
  [#8383]: https://github.com/deepseek-ai/deepseek-harness/discussions/8383
1146
+ [#8409]: https://github.com/deepseek-ai/deepseek-harness/discussions/8409
1147
+ [#8412]: https://github.com/deepseek-ai/deepseek-harness/discussions/8412
1148
+ [#8421]: https://github.com/deepseek-ai/deepseek-harness/discussions/8421
1149
+ [#8426]: https://github.com/deepseek-ai/deepseek-harness/discussions/8426
package/cordis.patch.yml CHANGED
@@ -38,6 +38,16 @@
38
38
  # `diagnose-windows-sandbox-acl` repair arrives with the `0.2.0` line. Neither
39
39
  # makes downgrading a fix, and repairing the `0.1.7` file list is not a thing
40
40
  # that can be done — there is nothing in that release to list.
41
+ # - the missing right confirmed from outside (#8426): asking a directory that
42
+ # names the account only through the inherited `Authenticated Users:(M)`
43
+ # entry for a Low mandatory-integrity label is refused unelevated — the label
44
+ # half alone isolates the right the merged call wants — and the identical
45
+ # operation succeeds the moment that account grants itself `(OI)(CI)F`, so the
46
+ # missing right is a right on the directory, not `SeRelabelPrivilege` and not
47
+ # elevation. The same report repaired two trees object-right-first and
48
+ # label-second (9521 and 5592 objects, no failures), the order the advisory's
49
+ # own remedy already follows. The probe is not offered as a check: a label
50
+ # write is not a read.
41
51
  #
42
52
  # The advisory also says why a "weaker grant" is not a smaller version of the
43
53
  # same one, because that is the next idea the diagnosis invites: the label rides
@@ -45,8 +55,8 @@
45
55
  # the confined token is itself lowered to Low, so the directory's Low label is
46
56
  # what lets that child write there at all.
47
57
  #
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
58
+ # And it says WHAT THE GRANT LEAVES BEHIND once it applies (#8312, #8314, #8412):
59
+ # the three entries are STANDING and nothing revokes them (the backend's own dispose
50
60
  # and fail-closed paths leave them — "the intended end state (the reuse cache)"),
51
61
  # the Low integrity label is INHERITABLE and lives in the SACL (so `icacls /reset`
52
62
  # does not remove it — that rebuilds the DACL), a process starts at
@@ -136,13 +146,13 @@
136
146
  # with why a windowless console is still an inheritable console. Like the second
137
147
  # family, it is mode-gated.
138
148
  #
139
- # A FOURTH family is the other half of the FIRST one's backend (#423, #8383).
140
- # There the grant could not be applied at all; here it WAS applied — on the
141
- # workspace root, once — and Windows ACE inheritance silently skipped the objects
142
- # whose DACL the caller could not write, so commands writing into a subdirectory
143
- # created or moved in from outside (an installer, an editor, another harness
144
- # under its own account) are denied forever while the same command against a
145
- # directory the harness itself created succeeds. The backend's provisioning check
149
+ # A FOURTH family is the other half of the FIRST one's backend (#423, #8383,
150
+ # #8421, #8409). There the grant could not be applied at all; here it WAS applied
151
+ # — on the workspace root, once — and Windows ACE inheritance silently skipped the
152
+ # objects whose DACL the caller could not write, so commands writing into a
153
+ # subdirectory created or moved in from outside (an installer, an editor, another
154
+ # harness under its own account) are denied forever while the same command against
155
+ # a directory the harness itself created succeeds. The backend's provisioning check
146
156
  # short-circuits on the root (the grant, the world delete-child deny and the
147
157
  # exact label must all match before it returns early), so those descendants are
148
158
  # never revisited. It is read from a value rather than from text, like the third
@@ -170,15 +180,28 @@
170
180
  # plus the capability SID looks granted to a check that merely greps for the
171
181
  # SID;
172
182
  # - NOTHING below the root writable at all, the root itself the only writable
173
- # place → the LABEL branch (#8383, measured on 0.2.0-rc.2 with the decisive
174
- # control of a directory created after the grant): the DACL is present and
175
- # inherits correctly at every level while the Low mandatory-integrity label
176
- # never reaches the children, so the kernel's no-write-up check refuses the
177
- # Low-integrity child before the ACL is ever consulted — same permanent shape
178
- # (the label goes out in the same single security-descriptor write as the
179
- # grant, only on the paths the backend enumerates, with no descendant walk),
180
- # separable from outside a session by the repository's own
181
- # `diagnose-windows-sandbox-acl` and its `LOW_LABEL` (`S-1-16-4096`) report.
183
+ # place → the LABEL branch (#8383, second instance #8421; measured on
184
+ # 0.2.0-rc.2 with the decisive control of a directory created after the grant):
185
+ # the DACL is present and inherits correctly at every level while the Low
186
+ # mandatory-integrity label never reaches the children, so the kernel's
187
+ # no-write-up check refuses the Low-integrity child before the ACL is ever
188
+ # consulted — same permanent shape (the label goes out in the same single
189
+ # security-descriptor write as the grant, only on the paths the backend
190
+ # enumerates, with no descendant walk), separable from outside a session by
191
+ # the repository's own `diagnose-windows-sandbox-acl` and its `LOW_LABEL`
192
+ # (`S-1-16-4096`) report;
193
+ # - THE ROOT ITSELF REFUSED, nothing writable, not even the root → a third
194
+ # shape, and the only branch of this family with a recovery inside the product
195
+ # (#8409). The standing grant is gone from the root's own DACL — rewritten
196
+ # from outside the harness — and the layer that would put it back has stopped
197
+ # looking: the session-scoped provider materializes the root ACE once per
198
+ # provider lifetime and consults its own in-memory map afterwards without
199
+ # ever re-reading the DACL, while the agentless/runner path passes no session
200
+ # id, owns the DACLs itself and re-applies the grant on every spawn — which
201
+ # is why that path heals and this one does not. No command reaches it; the
202
+ # move is the user's and it is the provider, not the ACL: restart the desktop
203
+ # app or open the workspace in a fresh one, which empties the map and lets the
204
+ # next provision re-read the DACL and rewrite the missing ACE.
182
205
  #
183
206
  # It also states the Windows ceiling that closes the obvious repair (a recursive
184
207
  # `icacls /grant` for a capability SID is refused with ERROR_NONE_MAPPED (1332),
package/lib/advice.js CHANGED
@@ -173,12 +173,19 @@ import { failureLine, STATUS_DLL_INIT_FAILED } from './signature.js';
173
173
  * The upstream threads the ACL advisory is a stopgap for.
174
174
  *
175
175
  * The first twelve report the provisioning failure itself (the merged DACL +
176
- * label write being refused). The last two, `#8312` and `#8314`, report the
177
- * other end of the same backend — what its grant leaves behind once it
178
- * *succeeds* — which the advisory states because it is the fact a reader needs
179
- * at the moment it hands them the command that applies that grant.
176
+ * label write being refused). `#8312` and `#8314` report the other end of the
177
+ * same backend — what its grant leaves behind once it *succeeds* — which the
178
+ * advisory states because it is the fact a reader needs at the moment it hands
179
+ * them the command that applies that grant. The last three arrived later and
180
+ * are all inside the same advisory: `#8426` is the independent, unelevated
181
+ * measurement of the missing `WRITE_OWNER` the diagnosis above had only ever
182
+ * argued for, and `#8412` is a second instance of what the grant leaves behind
183
+ * (the standing Low label reaching programs the user launches themselves).
184
+ * `#8409` is the same backend once more and belongs to both lists: one of its
185
+ * three defects is the provisioning failure this family explains, and another
186
+ * is the fourth family's third branch.
180
187
  */
181
- export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275 / #8312 / #8314';
188
+ export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275 / #8312 / #8314 / #8426 / #8412 / #8409';
182
189
  /**
183
190
  * The upstream threads the persistent-shell advisory is a stopgap for.
184
191
  *
@@ -207,9 +214,13 @@ export const NATIVE_INIT_DISCUSSIONS = '#7876 / #7877 / #8193 / #8208 / #8313 /
207
214
  * `#8383` then measured on its own — the exact complement, with the DACL
208
215
  * present and inheriting correctly at every level while the label reaches none
209
216
  * of the subdirectories, so the variant stopped being a footnote and became the
210
- * second branch the discriminator forks into.
217
+ * second branch the discriminator forks into; `#8421` met it again, which is
218
+ * why the branch reads as a shape rather than as one install. `#8409` is the
219
+ * third branch's source and a report of the DACL half as well: the workspace
220
+ * root's own standing grant can be gone, and the layer that would re-apply it
221
+ * consults a per-lifetime map instead of the DACL, so nothing notices.
211
222
  */
212
- export const WORKSPACE_DENIAL_DISCUSSIONS = '#423 / #8383';
223
+ export const WORKSPACE_DENIAL_DISCUSSIONS = '#423 / #8383 / #8421 / #8409';
213
224
  /**
214
225
  * The thread list each family's withholding note cites.
215
226
  *
@@ -422,6 +433,61 @@ function degradedGrant() {
422
433
  'This is not offered here as a fix, and neither is `danger-full-access`.',
423
434
  ];
424
435
  }
436
+ /**
437
+ * The independent measurement of the missing right, from `#8426`.
438
+ *
439
+ * Everything above is argued from the backend's own source — the merged
440
+ * security-information flags, the owner's implicit rights, the mask an inherited
441
+ * `Authenticated Users` entry carries. A reader is entitled to a measurement
442
+ * taken by someone who had none of that, and `#8426` is one: on a directory
443
+ * whose ACL named the account only through that inherited Modify entry, asking
444
+ * the directory for a Low mandatory-integrity label — the label half alone, no
445
+ * DACL write, so it isolates the right the merged call wants — was refused, and
446
+ * the same operation succeeded the moment that account granted itself
447
+ * `(OI)(CI)F`, whose mask carries `WRITE_OWNER`. Unelevated throughout, and
448
+ * reversible afterwards (the level moved back to Medium just as easily). That is
449
+ * the missing right demonstrated from outside this project's reading, and it is
450
+ * a right on the **directory**: no `SeRelabelPrivilege`, no token privilege, no
451
+ * elevation.
452
+ *
453
+ * The command itself is deliberately **not** printed here, and that is a
454
+ * decision rather than an omission — the same one the `#8272` paragraph of the
455
+ * README records. A label write is not a check: where the caller holds Full
456
+ * control it SUCCEEDS, and then the label and its inheritance are already
457
+ * written. This module hands over only operations that are safe to run when they
458
+ * succeed, which is why the two checks above are reads. The evidence is what the
459
+ * report measured; the probe stays in the report.
460
+ *
461
+ * The same report then repaired two real trees the same way — object right
462
+ * first, integrity label second, 9521 and 5592 objects, no failures. The order
463
+ * is not interchangeable and is worth stating: the second step is the one that
464
+ * needs what the first supplies. Both halves are already in the advisory's own
465
+ * remedy, in the same order.
466
+ *
467
+ * The contrast with the workspace-denial family is deliberate. There the
468
+ * recursive grant cannot be hand-written at all, because the SID it must name
469
+ * has no name to map (`ERROR_NONE_MAPPED`, 1332), which is why that advisory
470
+ * forks on breadth instead of handing over a line.
471
+ * @returns the section's lines.
472
+ */
473
+ function writeOwnerEvidence() {
474
+ return [
475
+ 'A second confirmation, measured by a reporter who had none of the reasoning above (#8426) — unelevated, on a',
476
+ 'directory whose ACL named the account only through the inherited `Authenticated Users:(M)` entry: asking that',
477
+ 'directory for a Low mandatory-integrity label — the LABEL half on its own, with no DACL write, so it isolates',
478
+ 'the one right this call wants — was refused, and the identical operation stopped being refused the moment the',
479
+ 'same account granted itself `(OI)(CI)F`, whose mask carries WRITE_OWNER: same account, same elevation,',
480
+ 'succeeding at once, and reversible from there (the level moves back to Medium just as easily).',
481
+ 'So the missing right is a right on this DIRECTORY, exactly as the diagnosis above reads it: no',
482
+ 'SeRelabelPrivilege, no token privilege, no elevation anywhere in that measurement. (That probe is not printed',
483
+ 'here and is not offered as a check: a label write is not a read — where the caller already holds Full control',
484
+ 'it succeeds, and then the label and its inheritance are written. This module hands over only the two reads',
485
+ 'above.) The same report repaired two real trees that way — object right first, integrity label second, 9521',
486
+ 'and 5592 objects, no failures — an order that cannot be swapped, since the second step is the one that needs',
487
+ 'what the first supplies; which is why the remedy below grants first and lets the backend write the label.',
488
+ '',
489
+ ];
490
+ }
425
491
  /**
426
492
  * What the grant leaves behind once it applies, and how far its label travels.
427
493
  *
@@ -547,6 +613,7 @@ function aclAdvisory(failure, href) {
547
613
  '(WO) / Write owner. If the strongest entry naming you is (M) / Modify — or no entry names you at all and',
548
614
  'your access comes from an inherited `Authenticated Users:(M)` — that is this failure.',
549
615
  '',
616
+ ...(failure.klass === 'apply-denied' ? writeOwnerEvidence() : []),
550
617
  'Ownership decides which of the two commands below can work, so read it first — PowerShell 5.1 or later:',
551
618
  ` (Get-Acl "${path}").Owner # compare with: whoami`,
552
619
  'If that is not your own account, take the second branch: the first one is refused before it runs.',
@@ -866,6 +933,11 @@ function workspaceDenialAdvisory(failure, tool, href) {
866
933
  ' therefore never revisited — not later in this session, not in any later one — so the identical command keeps',
867
934
  ' failing and there is no number of attempts that changes that',
868
935
  ' (`packages/sandbox/sandbox-windows-acl/src/acl.ts`).',
936
+ ' There is a second, higher skip, and it is the one the third branch below turns on: on a session-scoped',
937
+ ' workspace the layer that ASKS for the grant keeps its own per-lifetime map of the roots it has already',
938
+ ' provisioned and consults that map instead of the DACL, so for those roots the check above is not even reached',
939
+ ' again (`packages/sandbox/sandbox-local/src/index.ts`: the map guard at :403-421, "materializes once per',
940
+ ' workspace per server lifetime").',
869
941
  ' WHICH objects miss it is a fact about who created them: objects the harness itself creates inherit the ACE,',
870
942
  ' while objects that already existed or that another account or tool created (an installer, an editor, another',
871
943
  ' agent harness running under its own account) are the ones the propagation skipped. #423 measured 170 of 729',
@@ -875,7 +947,7 @@ function workspaceDenialAdvisory(failure, tool, href) {
875
947
  ' ACE half arrives — that is the second branch below, and it produces this identical denial. Both halves are',
876
948
  ' permanent for the same reason, so settling WHICH one you are looking at comes before anything else.',
877
949
  '',
878
- 'Confirm it — and settle FIRST which of the two halves is missing, because both produce this same denial and a',
950
+ 'Confirm it — and settle FIRST which of the three is missing, because all three produce this same denial and a',
879
951
  'reader who repairs the wrong one has learned nothing:',
880
952
  ` icacls "${subject}"`,
881
953
  ` icacls "${failure.workspaceRoot}"`,
@@ -886,6 +958,11 @@ function workspaceDenialAdvisory(failure, tool, href) {
886
958
  ' (#8383). A Low-integrity child may write to a directory only when that directory\'s own mandatory-integrity',
887
959
  ' label is Low as well; the kernel runs its no-write-up check IN ADDITION to the access check, so a DACL that',
888
960
  ' is perfect everywhere changes nothing.',
961
+ ' - THE ROOT ITSELF IS REFUSED — nothing writes, the root included, and work that used to succeed here now',
962
+ ' does not → NEITHER half above (#8409). The standing grant is gone from the root\'s own DACL, and the layer',
963
+ ' that would put it back has stopped looking. Two things about this branch are different in kind from the',
964
+ ' other two: no command reaches it, and it is the one branch of this family with a recovery inside the',
965
+ ' product — both are in half three below.',
889
966
  '',
890
967
  'Half one — the DACL ACE did not reach the object:',
891
968
  ' The root carries an inheritable ACE for the workspace capability SID — a `S-1-4-…` that `icacls` prints as an',
@@ -917,7 +994,9 @@ function workspaceDenialAdvisory(failure, tool, href) {
917
994
  ' all matching) before it returns early, so once the root is labelled the propagation is never attempted again and',
918
995
  ' the children that missed it are never revisited — the same root-only short-circuit as the DACL half, on the',
919
996
  ' other half of the same call (`packages/sandbox/sandbox-windows-acl/src/acl.ts`: the guard at :386-388, the',
920
- ' label read at :198-204).',
997
+ ' label read at :198-204). A second report met this same half afterwards (#8421): the label written on the root',
998
+ ' alone, the pre-existing subdirectories carrying none of it. That is why this branch is stated as a shape this',
999
+ ' backend produces rather than as one machine\'s result.',
921
1000
  ' The package README describes that label as inheritable and covering the tree, and its grant materialization as',
922
1001
  ' an eager full-tree propagation. Read it as a statement about the ACE that is written, not about what the',
923
1002
  ' children ended up carrying: the inheritance flag is declared on the root, the propagation to the children is',
@@ -926,6 +1005,33 @@ function workspaceDenialAdvisory(failure, tool, href) {
926
1005
  ' `WRITE_DAC`/`WRITE_OWNER` — the facts the DACL half turns on — and `LOW_LABEL` (`S-1-16-4096`) for this one.',
927
1006
  ' TEST: does a subdirectory show the capability ACE WITHOUT a Mandatory Label line? Yes → this half.',
928
1007
  '',
1008
+ 'Half three — the standing grant is gone from the ROOT itself (#8409, reported 2026-09-30):',
1009
+ ' Reported as a control beside two other defects on the same install (Windows 11, NTFS) rather than as a',
1010
+ ' theory: after the workspace root\'s own security descriptor is rewritten from OUTSIDE the harness — an ACL',
1011
+ ' reset, a restored descriptor, a tool that re-applies one — every write inside the workspace is refused, the',
1012
+ ' root\'s own directory included. What is missing is the ACE the harness itself wrote, not a descendant\'s',
1013
+ ' inheritance of it:',
1014
+ ' icacls "<root>" → no ACE for the workspace capability SID in its own DACL',
1015
+ ' The discriminator is what the SAME machine does on the other path, and the reporter measured both: the',
1016
+ ' agentless/runner invocation recovered by itself with no restart, while the desktop session did not. That',
1017
+ ' asymmetry is not one path being more careful — it is where the cache lives. The session-scoped provider',
1018
+ ' materializes the root ACE ONCE PER PROVIDER LIFETIME and keeps that root in its own in-memory map; every later',
1019
+ ' call consults the map and never reads the DACL again, so the exact-ACE check the two halves above turn on is',
1020
+ ' not even reached (`packages/sandbox/sandbox-local/src/index.ts`: the guard at :403-421, "materializes once per',
1021
+ ' workspace per server lifetime" at :359, and the standing edits it skips are called the "cross-session reuse',
1022
+ ' cache" in `packages/sandbox/sandbox-windows-acl/src/grant.ts:22-31`). The runner path used by agentless',
1023
+ ' sessions passes no session id (`windowsAclRunnerArgv()`, :365-380), so the runner owns the DACLs there and',
1024
+ ' re-applies the grant on every spawn — which really does read the DACL, and that is why that path heals and',
1025
+ ' this one does not.',
1026
+ ' RECOVERY: this is the only branch of this family a user can leave behind, and the move is the PROVIDER rather',
1027
+ ' than a command. RESTART IT — quit and reopen the desktop app, or open the workspace in a fresh one. That',
1028
+ ' empties the map, so the next provision reads the DACL, finds the ACE absent and writes it again; the reporter',
1029
+ ' measured exactly that ("restarting the desktop brings it back"). Nothing reachable from inside this session',
1030
+ ' gets there: the map is what is being consulted, and no command you can run here changes what it holds. Do not',
1031
+ ' reach for either repair above either — both are about propagating a grant to DESCENDANTS, and this branch has',
1032
+ ' lost the grant the propagation starts from.',
1033
+ ' TEST: is the workspace ROOT itself denied — no capability ACE in its own DACL? Yes → this branch.',
1034
+ '',
929
1035
  'Why the natural repair is closed — this is a Windows ceiling, not a mistake in the command:',
930
1036
  ' The grant the root carries is written for a capability SID, and a recursive `icacls /grant "*S-1-4-…:…" /T /C`',
931
1037
  ' is refused with ERROR_NONE_MAPPED (1332): the tool cannot map that SID to a name, so a grant that needs to name',
@@ -934,6 +1040,9 @@ function workspaceDenialAdvisory(failure, tool, href) {
934
1040
  ' subtree is a SACL write, which needs the object right the merged call already needed, and a manually widened',
935
1041
  ' DACL does NOT clear the failure — #8383 confirmed that adding an explicit FullControl entry for the user on a',
936
1042
  ' subdirectory still left the write denied, because the refusal happens before the ACL is ever consulted.',
1043
+ ' The third branch is not closed by this ceiling and does not need to be: there is nothing to write by hand',
1044
+ ' there — the grant that went missing is the harness\'s own, and all that has to happen is for the layer holding',
1045
+ ' the map to look at the DACL again, which the restart in half three is what makes it do.',
937
1046
  '',
938
1047
  'What helps:',
939
1048
  ' - The move that is yours, and the only one available inside the session: write new files under a directory the',
@@ -947,6 +1056,7 @@ function workspaceDenialAdvisory(failure, tool, href) {
947
1056
  ' WRITE_DAC on it, by writing the ACE the root carries into the child\'s own DACL. For the label half, the',
948
1057
  ' corresponding move is on the integrity label rather than the DACL, and it has to cover the tree, not one',
949
1058
  ' object; it is the same class of object write, so it is not something an unelevated prompt can do either.',
1059
+ ' For the third branch there is no object to repair at all: the move is the restart named in half three.',
950
1060
  ' - The route that costs no rights at all, and the honest recommendation while the halves remain unrepairable from',
951
1061
  ' inside: build and run where the confinement is not the thing under test — which is a decision about what you',
952
1062
  ' are doing, not a sandbox tier to reach for silently.',
@@ -959,7 +1069,8 @@ function workspaceDenialAdvisory(failure, tool, href) {
959
1069
  'the next session meets the same gap. Take it only if that is what you mean to buy.',
960
1070
  '',
961
1071
  'Do NOT retry this call unchanged: the provisioning path short-circuits on the root — on the grant AND on the',
962
- 'label — so the object that missed the propagation is never revisited.',
1072
+ 'label — so the object that missed the propagation is never revisited; and in the third branch the layer still',
1073
+ 'being consulted is the provider\'s own map, which nothing inside this session can change.',
963
1074
  '',
964
1075
  'How to read this: the denial names no cause and points at no repair, so the diagnosis is delivered here instead.',
965
1076
  'This is a stopgap, ' + where + '. This plugin speaks only when the mode is `workspace-write`, the host is',
@@ -174,12 +174,19 @@ import type { SandboxModeName } from './mode.js';
174
174
  * The upstream threads the ACL advisory is a stopgap for.
175
175
  *
176
176
  * The first twelve report the provisioning failure itself (the merged DACL +
177
- * label write being refused). The last two, `#8312` and `#8314`, report the
178
- * other end of the same backend — what its grant leaves behind once it
179
- * *succeeds* — which the advisory states because it is the fact a reader needs
180
- * at the moment it hands them the command that applies that grant.
177
+ * label write being refused). `#8312` and `#8314` report the other end of the
178
+ * same backend — what its grant leaves behind once it *succeeds* — which the
179
+ * advisory states because it is the fact a reader needs at the moment it hands
180
+ * them the command that applies that grant. The last three arrived later and
181
+ * are all inside the same advisory: `#8426` is the independent, unelevated
182
+ * measurement of the missing `WRITE_OWNER` the diagnosis above had only ever
183
+ * argued for, and `#8412` is a second instance of what the grant leaves behind
184
+ * (the standing Low label reaching programs the user launches themselves).
185
+ * `#8409` is the same backend once more and belongs to both lists: one of its
186
+ * three defects is the provisioning failure this family explains, and another
187
+ * is the fourth family's third branch.
181
188
  */
182
- export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275 / #8312 / #8314";
189
+ export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275 / #8312 / #8314 / #8426 / #8412 / #8409";
183
190
  /**
184
191
  * The upstream threads the persistent-shell advisory is a stopgap for.
185
192
  *
@@ -208,9 +215,13 @@ export declare const NATIVE_INIT_DISCUSSIONS = "#7876 / #7877 / #8193 / #8208 /
208
215
  * `#8383` then measured on its own — the exact complement, with the DACL
209
216
  * present and inheriting correctly at every level while the label reaches none
210
217
  * of the subdirectories, so the variant stopped being a footnote and became the
211
- * second branch the discriminator forks into.
218
+ * second branch the discriminator forks into; `#8421` met it again, which is
219
+ * why the branch reads as a shape rather than as one install. `#8409` is the
220
+ * third branch's source and a report of the DACL half as well: the workspace
221
+ * root's own standing grant can be gone, and the layer that would re-apply it
222
+ * consults a per-lifetime map instead of the DACL, so nothing notices.
212
223
  */
213
- export declare const WORKSPACE_DENIAL_DISCUSSIONS = "#423 / #8383";
224
+ export declare const WORKSPACE_DENIAL_DISCUSSIONS = "#423 / #8383 / #8421 / #8409";
214
225
  /**
215
226
  * The thread list each family's withholding note cites.
216
227
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@argszero/cordis-plugin-sandbox-grant-advisor",
3
- "description": "Turns four sandbox environment failures that name neither their cause nor a remedy into a diagnosis with a path forward. Family 1, the Windows workspace ACL: twelve reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816, #8232, #8272, #8275) of one signature — every sandboxed command fails before it runs with `SetNamedSecurityInfoW failed (Win32 5): grantWrite(<workspace>)`, because the merged DACL + mandatory-label write needs WRITE_OWNER on the directory (an object right the caller can self-grant), not SeSecurityPrivilege and not elevation; the host grant is materialized lazily and caches nothing on the failure path, so the same failure repeats per command. Two further reports (#8312, #8314) describe the other end of that same backend — what its grant leaves behind once it succeeds: three STANDING entries nothing revokes (the workspace capability SID, the world delete-child deny, and an INHERITABLE Low integrity label that lives in the SACL, so resetting the DACL does not take it off), which makes every program launched from a folder DSH has written to run at Low integrity, and — through the NTFS hard links a pnpm workspace is made of, where two names share one file object and therefore one security descriptor — can reach out of the tree onto a content-addressed store's shared objects, leaving every project that builds from that store with Low-integrity executables and failures that name the build tool rather than DSH; the advisory states all of it before handing over the command that applies that grant, and offers no removal command, because the maintainers' own diagnosis skill does not remove the label either and removing one needs WRITE_OWNER. Family 2, the persistent shell (#7638, #8322): with the `minimal` preset under a confining sandbox mode every shell call dies instantly with `PTY shell exited during startup` because the terminal backend cannot create the pseudo-console inside the sandbox, retrying never helps, and `minimal` mounts no fallback shell tool; #8322 separated the arms inside that mode with one runner and one ConPTY — a console-subsystem node.exe host starts the confined shell while the packaged desktop's GUI-subsystem Electron host kills it silently — so the advisory names the host alongside the mode, says the outcome is deterministic per (session mode × host) rather than intermittent, and adds the console-owning host the Web UI provides (#8313) to the user-side options. Family 3, a confined Windows child that died during native initialization (#7876, #7877): every command spawned through the sandbox runner can report exit 0xC0000142 STATUS_DLL_INIT_FAILED with the process never reaching its entry point — the packaged desktop starts that runner as [process.execPath, entry], and that host has been measured twice with one indistinguishable appearance from inside a session — either nothing on the runner path ran because the Electron binary launches as an application unless the child's environment carries ELECTRON_RUN_AS_NODE=1 (#7876), or the desktop launcher does set that variable and the child still dies because the restricted token is derived from the Electron process image (#8193), and an MSYS2 / Git-Bash program cannot create its signal pipe under the restricted token while cmd.exe and pwsh run fine in the same workspace under the same mode (#7877) — and because upstream's runner-failure rules admit only exit 127 with the `windows-acl-run: ` signature, the code is never an error: it arrives as the canonical value of a result the pipeline calls a success. The plugin observes the public `tools/post-execute` waterfall, classifies all four signatures narrowly (only the two `...NamedSecurityInfoW` operations; the PTY message matched on a whole line, never as a substring; the loader status read as a 32-bit integer out of the shell tool's own canonical success value, never from rendered text; the two value-read families advised only when the facts they need hold \u2014 a confining resolved mode for the native-init death, and a Windows host, a `workspace-write` stamped mode and an in-workspace path for the denial), and attaches ONE durable user-role advisory per agent per family through `additionalContexts`. The ACL advisory names the missing right, both environments the identical text can describe (an inherited Modify-only entry, a data volume where no ACE names the caller at all, and a directory owned by another account), the version boundary that arrived with the mandatory label (0.1.7-alpha.1, flag 20, versus the DACL-only flag 4 up to 0.1.6-alpha.x) together with why downgrading is not the remedy, the second boundary the later reports needed (the backend's own `diagnose-windows-sandbox-acl` skill is not part of the 0.1.7 line at all — it arrives with 0.2.0, measured on both published tarballs: 0.1.7-rc.2 names it zero times in either README, ships no assets, and carries no registration symbol anywhere in its tree, while 0.2.0-rc.2 ships assets/diagnose-windows-sandbox-acl/ and names it three times per README — so a reader who found the promise in a README was reading a 0.2.0-era document, and sending them to repair a files glob would be the wrong repair), and why a \"weaker grant\" is a mechanism the backend cannot express rather than a policy it declines (the label rides the SAME SetNamedSecurityInfoW as the grant — there is no DACL-only apply path to fall back to — and the confined token is itself lowered to Low, so the object's Low label is what lets the confined child write there at all; declining the label usefully means declining the token level with it), the discriminator, the remedy forked on an ownership check the user runs (`(Get-Acl \"<dir>\").Owner`), because one command cannot serve both rights situations: where the caller owns the directory, the unelevated `icacls ... :(OI)(CI)(WO)` is the whole of what is missing — the owner's implicit WRITE_DAC already covers the DACL half — and where the caller does not own it that same command is refused for want of WRITE_DAC, so the grant has to come from an elevated account, or by taking ownership first, or by moving the workspace under %USERPROFILE% — and the two remedies that look right and are not (`takeown`, `icacls /reset`), each with the reason it fails — and it states the two things about the failure's shape that the reports had to measure for themselves (#8232): that it belongs to the workspace rather than to the command (a command that only reads fails identically, so there is no harmless retry), and that the grant is scoped to the directory it names and its children, so a sibling workspace root on the same volume needs the line once more; the PTY advisory names the failing combination and the host binary that decides it, states the resolved mode and that the session's recorded mode is the one that counts, tells the model to stop rather than retry, and hands over the user-side options (a preset swap, a patch row, or the console-owning host of #8313) — it never names a shell tool the failing composition does not mount; the native-init advisory states the resolved mode, says the process died before its entry point, enumerates the two producers measured under a confining mode — naming both measured shapes of the desktop host rather than asserting the one that was measured first — with the check that separates them (what program the reader ran; whether this host is the packaged desktop binary, which the plugin measures and reports rather than assumes), carries the one conversion a model can make itself (rewrite the work as PowerShell or `cmd` when an MSYS2 program is what could not start), and — for the Electron host — names the fix #8193 measured rather than a wider mode: host the runner on a real node.exe (the desktop ships one under `resources/runtime/primary-runtime/dependencies/node/bin/node.exe`), where the same confined `pwsh`/`cmd` spawns succeed, while `danger-full-access` is described as a way to confirm the diagnosis and not a fix, together with the warning that unsetting `ELECTRON_RUN_AS_NODE` instead would leave the desktop's Electron-hosted runner unable to execute `runner.js` at all (the interaction #8193 records with #8174), and says plainly which producers it does not know — it never claims the sandbox caused the failure and never offers a widened mode as a fix. It also states the single rule the whole family follows from, because a reader without it reaches for the wrong flag: in a restricted token a console can be INHERITED but not CREATED, so a creation flag that forces the child to make one of its own (`CREATE_NO_WINDOW`, `CREATE_NEW_CONSOLE`) is fatal on its own, while `DETACHED_PROCESS` — which declines a console rather than creating one — is not; #8336 measured that matrix on one machine, including the pair that makes it a rule rather than a list (the same flag on an UNRESTRICTED token is harmless, as is `STARTF_USESHOWWINDOW` with `SW_HIDE`), and the advisory answers the `windowsHide` suspicion instead of ignoring it: that flag is not set anywhere on the confined path — it appears on the ORDINARY subprocess path (`dsh-subprocess-local`'s spawn, which starts the runner rather than the confined child), and the restricted spawn defines no `CREATE_NO_WINDOW` constant at all — and where it IS set, #8208 measured it both ways on a host that works, because a windowless console is still a console: what decides is whether the host owns a console OBJECT, not whether it owns a window (#8336, #8334). Family 4, a confined command DENIED a path inside its own workspace (#423): the workspace grant is applied once, on the root, and relies on Windows ACE inheritance to reach the tree beneath it, so an object that already existed (created by an installer, an editor, another harness under its own account) whose DACL the caller could not write kept its older DACL — silently, because a skipped inherited ACE produces no error, no return value and no log line — after which the backend's root-only check (`hasExactGrant(workspaceRoot)`) short-circuits and those descendants are NEVER revisited, so the identical command keeps failing while a directory the harness itself created works; the family is read from the executors' own structured stamp on a successful result (`sandbox: { mode, denied, enforcement? }`, produced by matching the backend's refusal dialect in the captured stderr, so what it reads is the harness's own reading of its own sandbox), and it speaks only when the host is Windows, the stamped mode is `workspace-write`, and EVERY absolute path the command's own arguments name lies under the session's workspace — a denial anywhere else is the designed escalation path, and a command naming any outside path as well, or only relative ones, is refused rather than guessed at because the plugin cannot say which path was refused; the advisory prints the path it keyed on beside the root it tested it against, states that retrying is provably useless, gives the two-sided discriminator (reading and listing use the normal token while writing and deleting use the restricted one, so an object whose DACL names only Administrators/SYSTEM plus the capability SID looks granted to a check that merely greps for the SID), names the second branch the same shape forks into — the mandatory-integrity LABEL reaching none of the subdirectories while the DACL arrives at every level (#8383, measured on 0.2.0-rc.2 with the decisive control of a directory created after the grant) — and forks the two on BREADTH before any check, because from inside a session they are indistinguishable and a reader who repairs the wrong half has learned nothing; it gives each half its own TEST, states that both are permanent for the same root-only short-circuit (grant + world delete-child deny + exact label must all match before the backend returns early, on the same single security-descriptor write), separates them from outside a session by the repository's own `diagnose-windows-sandbox-acl` (owner rights and `WRITE_OWNER` for one half, `LOW_LABEL` for the other), and states the Windows ceiling that closes the obvious repair (a recursive `icacls /grant` for a capability SID is refused with ERROR_NONE_MAPPED (1332) because that SID has no name to map; a widened DACL does not clear the label half either, because the refusal precedes the ACL) — while shipping NO repair command at all, for the reason the standing-grant section ships none: this project has no Windows host to verify a line on. An optional, off-by-default `enforceAfter` refuses an identical ACL call this plugin has watched fail, bounded by `maxDenials`; the blocking half is ACL-only by design. It never edits an ACL, never elevates, never sets another process's environment, and never changes a preset or a mode, and it complements repeat-guard-escalation, which keys on call identity rather than on the environment signature.",
4
- "version": "0.13.0",
3
+ "description": "Turns four sandbox environment failures that name neither their cause nor a remedy into a diagnosis with a path forward. Family 1, the Windows workspace ACL: twelve reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816, #8232, #8272, #8275) of one signature — every sandboxed command fails before it runs with `SetNamedSecurityInfoW failed (Win32 5): grantWrite(<workspace>)`, because the merged DACL + mandatory-label write needs WRITE_OWNER on the directory (an object right the caller can self-grant), not SeSecurityPrivilege and not elevation; the host grant is materialized lazily and caches nothing on the failure path, so the same failure repeats per command. Two further reports (#8312, #8314) and a third (#8412) describe the other end of that same backend — what its grant leaves behind once it succeeds: three STANDING entries nothing revokes (the workspace capability SID, the world delete-child deny, and an INHERITABLE Low integrity label that lives in the SACL, so resetting the DACL does not take it off), which makes every program launched from a folder DSH has written to run at Low integrity, and — through the NTFS hard links a pnpm workspace is made of, where two names share one file object and therefore one security descriptor — can reach out of the tree onto a content-addressed store's shared objects, leaving every project that builds from that store with Low-integrity executables and failures that name the build tool rather than DSH; the advisory states all of it before handing over the command that applies that grant, and offers no removal command, because the maintainers' own diagnosis skill does not remove the label either and removing one needs WRITE_OWNER. Family 2, the persistent shell (#7638, #8322): with the `minimal` preset under a confining sandbox mode every shell call dies instantly with `PTY shell exited during startup` because the terminal backend cannot create the pseudo-console inside the sandbox, retrying never helps, and `minimal` mounts no fallback shell tool; #8322 separated the arms inside that mode with one runner and one ConPTY — a console-subsystem node.exe host starts the confined shell while the packaged desktop's GUI-subsystem Electron host kills it silently — so the advisory names the host alongside the mode, says the outcome is deterministic per (session mode × host) rather than intermittent, and adds the console-owning host the Web UI provides (#8313) to the user-side options. Family 3, a confined Windows child that died during native initialization (#7876, #7877): every command spawned through the sandbox runner can report exit 0xC0000142 STATUS_DLL_INIT_FAILED with the process never reaching its entry point — the packaged desktop starts that runner as [process.execPath, entry], and that host has been measured twice with one indistinguishable appearance from inside a session — either nothing on the runner path ran because the Electron binary launches as an application unless the child's environment carries ELECTRON_RUN_AS_NODE=1 (#7876), or the desktop launcher does set that variable and the child still dies because the restricted token is derived from the Electron process image (#8193), and an MSYS2 / Git-Bash program cannot create its signal pipe under the restricted token while cmd.exe and pwsh run fine in the same workspace under the same mode (#7877) — and because upstream's runner-failure rules admit only exit 127 with the `windows-acl-run: ` signature, the code is never an error: it arrives as the canonical value of a result the pipeline calls a success. The plugin observes the public `tools/post-execute` waterfall, classifies all four signatures narrowly (only the two `...NamedSecurityInfoW` operations; the PTY message matched on a whole line, never as a substring; the loader status read as a 32-bit integer out of the shell tool's own canonical success value, never from rendered text; the two value-read families advised only when the facts they need hold \u2014 a confining resolved mode for the native-init death, and a Windows host, a `workspace-write` stamped mode and an in-workspace path for the denial), and attaches ONE durable user-role advisory per agent per family through `additionalContexts`. The ACL advisory names the missing right, both environments the identical text can describe (an inherited Modify-only entry, a data volume where no ACE names the caller at all, and a directory owned by another account), the version boundary that arrived with the mandatory label (0.1.7-alpha.1, flag 20, versus the DACL-only flag 4 up to 0.1.6-alpha.x) together with why downgrading is not the remedy, the second boundary the later reports needed (the backend's own `diagnose-windows-sandbox-acl` skill is not part of the 0.1.7 line at all — it arrives with 0.2.0, measured on both published tarballs: 0.1.7-rc.2 names it zero times in either README, ships no assets, and carries no registration symbol anywhere in its tree, while 0.2.0-rc.2 ships assets/diagnose-windows-sandbox-acl/ and names it three times per README — so a reader who found the promise in a README was reading a 0.2.0-era document, and sending them to repair a files glob would be the wrong repair), and why a \"weaker grant\" is a mechanism the backend cannot express rather than a policy it declines (the label rides the SAME SetNamedSecurityInfoW as the grant — there is no DACL-only apply path to fall back to — and the confined token is itself lowered to Low, so the object's Low label is what lets the confined child write there at all; declining the label usefully means declining the token level with it), the discriminator, the remedy forked on an ownership check the user runs (`(Get-Acl \"<dir>\").Owner`), because one command cannot serve both rights situations: where the caller owns the directory, the unelevated `icacls ... :(OI)(CI)(WO)` is the whole of what is missing — the owner's implicit WRITE_DAC already covers the DACL half — and where the caller does not own it that same command is refused for want of WRITE_DAC, so the grant has to come from an elevated account, or by taking ownership first, or by moving the workspace under %USERPROFILE% — and the two remedies that look right and are not (`takeown`, `icacls /reset`), each with the reason it fails, and it states that independent measurement of the missing right (#8426): `icacls <dir> /setintegritylevel \"(OI)(CI)Low\"` is refused unelevated, and the identical command succeeds the moment that account grants itself `(OI)(CI)F` (whose mask carries WRITE_OWNER), which is the right demonstrated from outside this project's reading and shown to be a right on the directory rather than SeRelabelPrivilege or elevation — the same report repairing two trees object-right-first and label-second (9521 and 5592 objects, no failures), the order the advisory's own remedy already follows — and it states the two things about the failure's shape that the reports had to measure for themselves (#8232): that it belongs to the workspace rather than to the command (a command that only reads fails identically, so there is no harmless retry), and that the grant is scoped to the directory it names and its children, so a sibling workspace root on the same volume needs the line once more; the PTY advisory names the failing combination and the host binary that decides it, states the resolved mode and that the session's recorded mode is the one that counts, tells the model to stop rather than retry, and hands over the user-side options (a preset swap, a patch row, or the console-owning host of #8313) — it never names a shell tool the failing composition does not mount; the native-init advisory states the resolved mode, says the process died before its entry point, enumerates the two producers measured under a confining mode — naming both measured shapes of the desktop host rather than asserting the one that was measured first — with the check that separates them (what program the reader ran; whether this host is the packaged desktop binary, which the plugin measures and reports rather than assumes), carries the one conversion a model can make itself (rewrite the work as PowerShell or `cmd` when an MSYS2 program is what could not start), and — for the Electron host — names the fix #8193 measured rather than a wider mode: host the runner on a real node.exe (the desktop ships one under `resources/runtime/primary-runtime/dependencies/node/bin/node.exe`), where the same confined `pwsh`/`cmd` spawns succeed, while `danger-full-access` is described as a way to confirm the diagnosis and not a fix, together with the warning that unsetting `ELECTRON_RUN_AS_NODE` instead would leave the desktop's Electron-hosted runner unable to execute `runner.js` at all (the interaction #8193 records with #8174), and says plainly which producers it does not know — it never claims the sandbox caused the failure and never offers a widened mode as a fix. It also states the single rule the whole family follows from, because a reader without it reaches for the wrong flag: in a restricted token a console can be INHERITED but not CREATED, so a creation flag that forces the child to make one of its own (`CREATE_NO_WINDOW`, `CREATE_NEW_CONSOLE`) is fatal on its own, while `DETACHED_PROCESS` — which declines a console rather than creating one — is not; #8336 measured that matrix on one machine, including the pair that makes it a rule rather than a list (the same flag on an UNRESTRICTED token is harmless, as is `STARTF_USESHOWWINDOW` with `SW_HIDE`), and the advisory answers the `windowsHide` suspicion instead of ignoring it: that flag is not set anywhere on the confined path — it appears on the ORDINARY subprocess path (`dsh-subprocess-local`'s spawn, which starts the runner rather than the confined child), and the restricted spawn defines no `CREATE_NO_WINDOW` constant at all — and where it IS set, #8208 measured it both ways on a host that works, because a windowless console is still a console: what decides is whether the host owns a console OBJECT, not whether it owns a window (#8336, #8334). Family 4, a confined command DENIED a path inside its own workspace (#423, #8383, #8421, #8409): the workspace grant is applied once, on the root, and relies on Windows ACE inheritance to reach the tree beneath it, so an object that already existed (created by an installer, an editor, another harness under its own account) whose DACL the caller could not write kept its older DACL — silently, because a skipped inherited ACE produces no error, no return value and no log line — after which the backend's root-only check (`hasExactGrant(workspaceRoot)`) short-circuits and those descendants are NEVER revisited, so the identical command keeps failing while a directory the harness itself created works; the family is read from the executors' own structured stamp on a successful result (`sandbox: { mode, denied, enforcement? }`, produced by matching the backend's refusal dialect in the captured stderr, so what it reads is the harness's own reading of its own sandbox), and it speaks only when the host is Windows, the stamped mode is `workspace-write`, and EVERY absolute path the command's own arguments name lies under the session's workspace — a denial anywhere else is the designed escalation path, and a command naming any outside path as well, or only relative ones, is refused rather than guessed at because the plugin cannot say which path was refused; the advisory prints the path it keyed on beside the root it tested it against, states that retrying is provably useless, gives the two-sided discriminator (reading and listing use the normal token while writing and deleting use the restricted one, so an object whose DACL names only Administrators/SYSTEM plus the capability SID looks granted to a check that merely greps for the SID), names the second branch the same shape forks into — the mandatory-integrity LABEL reaching none of the subdirectories while the DACL arrives at every level (#8383, second instance #8421, measured on 0.2.0-rc.2 with the decisive control of a directory created after the grant) — and names the third: the workspace ROOT itself refused, the standing grant gone from its own DACL and the provider's per-lifetime map the only layer still consulted, which never re-reads the DACL while the agentless runner path (no session id) re-applies the grant on every spawn and therefore heals (#8409) — and forks the three on BREADTH before any check, because from inside a session the first two are indistinguishable and a reader who repairs the wrong half has learned nothing; it gives each branch its own TEST, states that the propagation halves are permanent for the same root-only short-circuit (grant + world delete-child deny + exact label must all match before the backend returns early, on the same single security-descriptor write) and that the third is the one branch with a recovery inside the product — a user restart of the provider, which empties the map and lets the next provision rewrite the missing ACE — separates the first two from outside a session by the repository's own `diagnose-windows-sandbox-acl` (owner rights and `WRITE_OWNER` for one half, `LOW_LABEL` for the other), and states the Windows ceiling that closes the obvious repair (a recursive `icacls /grant` for a capability SID is refused with ERROR_NONE_MAPPED (1332) because that SID has no name to map; a widened DACL does not clear the label half either, because the refusal precedes the ACL — and the third branch needs no hand-written ACE at all) — while shipping NO repair command at all, for the reason the standing-grant section ships none: this project has no Windows host to verify a line on. An optional, off-by-default `enforceAfter` refuses an identical ACL call this plugin has watched fail, bounded by `maxDenials`; the blocking half is ACL-only by design. It never edits an ACL, never elevates, never sets another process's environment, and never changes a preset or a mode, and it complements repeat-guard-escalation, which keys on call identity rather than on the environment signature.",
4
+ "version": "0.14.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",