@argszero/cordis-plugin-sandbox-grant-advisor 0.11.0 → 0.13.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 +73 -16
- package/cordis.patch.yml +52 -25
- package/lib/advice.js +137 -52
- package/lib/types/advice.d.ts +24 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -439,10 +439,13 @@ the advisory never names the dead directory.
|
|
|
439
439
|
|
|
440
440
|
### 3. A confined child that never started (`native-init`)
|
|
441
441
|
|
|
442
|
-
|
|
442
|
+
Seven reports of one exit code: [`#7876`] and [`#8193`] (the packaged desktop
|
|
443
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)
|
|
445
|
-
|
|
444
|
+
the Web UI launched from a terminal, which works), [`#8334`] (the same code with
|
|
445
|
+
the authorization side attached: the grant **succeeded** and every confined child
|
|
446
|
+
still died) and [`#7877`] (MSYS2 / Git
|
|
447
|
+
Bash) — and [`#8208`], which found the mechanism the console cases share, and
|
|
448
|
+
[`#8336`], which measured the creation flags that mechanism turns on. All are `0xC0000142`
|
|
446
449
|
`STATUS_DLL_INIT_FAILED` — the Windows
|
|
447
450
|
loader terminated the process while it was initializing its native images, i.e.
|
|
448
451
|
**before the program's entry point**. A command that ran and then failed exits
|
|
@@ -548,10 +551,37 @@ Two producers have been measured under a confining mode:
|
|
|
548
551
|
ordinary path (`process.ts:567`, i.e. `0x404`); `CREATE_NO_WINDOW`
|
|
549
552
|
(`0x08000000`) is **not a constant anywhere in that source**. Those flags are
|
|
550
553
|
fatal under a console-less runner and harmless under one that owns a console —
|
|
551
|
-
so
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
554
|
+
so **there** the console decides. `0.7.x` wrote the two-set version of that
|
|
555
|
+
list and called the flags "a necessary ingredient ... the host process image is
|
|
556
|
+
what turns it fatal"; both halves were wrong, and `0.8.0` replaces them.
|
|
557
|
+
|
|
558
|
+
**`0.11.0` and earlier concluded too much from those three sets** — "it is the
|
|
559
|
+
console and not the flag list that decides" — and [`#8336`] measured the case
|
|
560
|
+
that falsifies it: with a console-owning host, a restricted token and the Low
|
|
561
|
+
integrity level, varying only the creation flags, `CREATE_NO_WINDOW` and
|
|
562
|
+
`CREATE_NEW_CONSOLE` both died with `0xC0000142`, while `0`, `DETACHED_PROCESS`
|
|
563
|
+
and `CREATE_NEW_PROCESS_GROUP` reached the program. So the console decides
|
|
564
|
+
**whether a flag that merely shares the inherited console is fatal**, and a
|
|
565
|
+
flag that forces the child to create one of its own is fatal regardless. The
|
|
566
|
+
pair inside that matrix is what makes it a rule rather than a list of forbidden
|
|
567
|
+
flags: the same `CREATE_NO_WINDOW` on an *unrestricted* token reached the
|
|
568
|
+
program, and `STARTF_USESHOWWINDOW` with `SW_HIDE` — how the harness hides a
|
|
569
|
+
window without isolating a console — was harmless on the restricted one.
|
|
570
|
+
`0.12.0` replaces the withdrawn sentence with the rule and that matrix.
|
|
571
|
+
|
|
572
|
+
**The `windowsHide` suspicion, answered.** It is the first thing a search turns
|
|
573
|
+
up for this failure, and it points at the wrong component: the flag appears
|
|
574
|
+
once on the *ordinary* subprocess path
|
|
575
|
+
(`packages/subprocess/subprocess-local/src/spawn.ts:472`,
|
|
576
|
+
`windowsHide: platform === 'win32'`), which starts the runner rather than the
|
|
577
|
+
confined child, and the restricted spawn defines no `CREATE_NO_WINDOW` constant
|
|
578
|
+
at all. It is **not** set in `dsh-jobs` — a search there finds nothing, and
|
|
579
|
+
every `windowsHide` in the tree sits on a host-side or ordinary spawn. And
|
|
580
|
+
where it *is* set, [`#8208`] measured it both ways on a host that works: with
|
|
581
|
+
and without `windowsHide`, the confined `pwsh` reached exit `0` with its own
|
|
582
|
+
stdout intact. The reason is the rule again — a windowless console is still a
|
|
583
|
+
console, and that is what the child inherits; what decides is whether the host
|
|
584
|
+
owns a console **object**, not whether it owns a window.
|
|
555
585
|
|
|
556
586
|
**One arm was applied, measured, and rejected.** Putting `DETACHED_PROCESS` on
|
|
557
587
|
the *restricted child* removes its console request and does stop the crash —
|
|
@@ -623,7 +653,7 @@ the single shape that happened to be measured first. It never
|
|
|
623
653
|
offers `danger-full-access` as a fix and never suggests a sandbox setting be
|
|
624
654
|
relaxed.
|
|
625
655
|
|
|
626
|
-
### 4. Denied inside the workspace (`workspace-denial`, added in 0.11.0)
|
|
656
|
+
### 4. Denied inside the workspace (`workspace-denial`, added in 0.11.0; the two branches it forks into in 0.13.0)
|
|
627
657
|
|
|
628
658
|
[#423] is one report and its own follow-up, and it is the **other end of the
|
|
629
659
|
backend §1 is about**. There the workspace grant could not be applied at all and
|
|
@@ -677,14 +707,38 @@ reading of its own sandbox, not a guess about a line of output.
|
|
|
677
707
|
`Administrators`/`SYSTEM` plus the capability SID is refused on the read side
|
|
678
708
|
too, and looks fine to a check that merely greps for the capability SID.
|
|
679
709
|
[#423] measured exactly that on a `.cache` directory.
|
|
680
|
-
- **The
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
label
|
|
710
|
+
- **The fork, and the second branch** (added in 0.13.0, from [#8383]). The
|
|
711
|
+
grant and the mandatory-integrity **label** go out in the same single
|
|
712
|
+
security-descriptor write, and that write lands on the workspace root (plus
|
|
713
|
+
the session's private temp directory) with **no descendant walk** — so the
|
|
714
|
+
label half can fail to reach the tree while the DACL half arrives at every
|
|
715
|
+
level, and it produces the identical denial. The backend's root-only
|
|
716
|
+
idempotency check then requires the grant, the world delete-child deny **and**
|
|
717
|
+
the exact label (`hasExactGrant()` + `hasExactDeny()` + `hasExactLabel()` all
|
|
718
|
+
matching, `acl.ts:386-388`, label read at `:198-204`) before it returns early,
|
|
719
|
+
so once the root is labelled the propagation is never attempted again and an
|
|
720
|
+
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.
|
|
688
742
|
- **No repair command.** The obvious one, a recursive `icacls /grant` for the
|
|
689
743
|
capability SID, is refused by Windows itself with `ERROR_NONE_MAPPED` (1332) —
|
|
690
744
|
the tool cannot map that SID to a name, so a grant that must name it never
|
|
@@ -1041,3 +1095,6 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
1041
1095
|
[#8313]: https://github.com/deepseek-ai/deepseek-harness/discussions/8313
|
|
1042
1096
|
[#8314]: https://github.com/deepseek-ai/deepseek-harness/discussions/8314
|
|
1043
1097
|
[#8322]: https://github.com/deepseek-ai/deepseek-harness/discussions/8322
|
|
1098
|
+
[#8334]: https://github.com/deepseek-ai/deepseek-harness/discussions/8334
|
|
1099
|
+
[#8336]: https://github.com/deepseek-ai/deepseek-harness/discussions/8336
|
|
1100
|
+
[#8383]: https://github.com/deepseek-ai/deepseek-harness/discussions/8383
|
package/cordis.patch.yml
CHANGED
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
# running one.
|
|
106
106
|
#
|
|
107
107
|
# A THIRD family is recognized from a value, not from text (#7876, #7877,
|
|
108
|
-
# #8193, #8208, #8313):
|
|
108
|
+
# #8193, #8208, #8313, #8336, #8334):
|
|
109
109
|
#
|
|
110
110
|
# [exit code: -1073741502] (0xC0000142 STATUS_DLL_INIT_FAILED)
|
|
111
111
|
#
|
|
@@ -124,20 +124,31 @@
|
|
|
124
124
|
# or spawned without a console), carries the one conversion the model can make
|
|
125
125
|
# itself (rewrite the work as PowerShell or `cmd`), names the remedy #8193
|
|
126
126
|
# measured — a real node.exe host, which the desktop ships — and says plainly
|
|
127
|
-
# which producers it does not know.
|
|
128
|
-
#
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
#
|
|
134
|
-
#
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
# the
|
|
127
|
+
# which producers it does not know. It also states the one rule the whole family
|
|
128
|
+
# follows from, because a reader without it reaches for the wrong flag: in a
|
|
129
|
+
# restricted token a console can be INHERITED but not CREATED, so a creation
|
|
130
|
+
# flag that forces the child to make one of its own — `CREATE_NO_WINDOW`,
|
|
131
|
+
# `CREATE_NEW_CONSOLE` — is fatal on its own, while `DETACHED_PROCESS` (which
|
|
132
|
+
# declines a console rather than creating one) is not; #8336 measured that
|
|
133
|
+
# matrix on one machine, and #8208 measured `windowsHide` both ways on a host
|
|
134
|
+
# that works, so the advisory answers that suspicion with where the flag
|
|
135
|
+
# actually appears — the ordinary subprocess path, not the confined one — and
|
|
136
|
+
# with why a windowless console is still an inheritable console. Like the second
|
|
137
|
+
# family, it is mode-gated.
|
|
138
|
+
#
|
|
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
|
|
146
|
+
# short-circuits on the root (the grant, the world delete-child deny and the
|
|
147
|
+
# exact label must all match before it returns early), so those descendants are
|
|
148
|
+
# never revisited. It is read from a value rather than from text, like the third
|
|
149
|
+
# family and for the same reason (a denial exits nonzero and the shipped shell
|
|
150
|
+
# tools report that as a finished run), but it needs no bespoke code: the
|
|
151
|
+
# executors stamp the denial onto the value as
|
|
141
152
|
#
|
|
142
153
|
# sandbox: { mode: "workspace-write", denied: true }
|
|
143
154
|
#
|
|
@@ -147,16 +158,32 @@
|
|
|
147
158
|
# absolute path the command names lies inside the session's workspace: a denial
|
|
148
159
|
# anywhere else (outside the workspace, under `read-only`, a runner failure) is
|
|
149
160
|
# 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
|
|
151
|
-
# useless
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
#
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
161
|
+
# it keyed on beside the root it tested it against and says that retrying is
|
|
162
|
+
# provably useless — and then, before any check, forks the diagnosis on BREADTH,
|
|
163
|
+
# the one fact the reader already owns:
|
|
164
|
+
#
|
|
165
|
+
# - a HANDFUL of stubborn objects while the rest of the tree writes normally →
|
|
166
|
+
# the DACL branch (#423: the capability ACE did not arrive at those objects),
|
|
167
|
+
# with the two-sided discriminator that a single check gets wrong — reading
|
|
168
|
+
# and listing use the normal token while writing and deleting use the
|
|
169
|
+
# restricted one, so an object whose DACL names only Administrators/SYSTEM
|
|
170
|
+
# plus the capability SID looks granted to a check that merely greps for the
|
|
171
|
+
# SID;
|
|
172
|
+
# - 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.
|
|
182
|
+
#
|
|
183
|
+
# It also states the Windows ceiling that closes the obvious repair (a recursive
|
|
184
|
+
# `icacls /grant` for a capability SID is refused with ERROR_NONE_MAPPED (1332),
|
|
185
|
+
# because that SID has no name to map; and a manually widened DACL does not clear
|
|
186
|
+
# the label half either, because the refusal precedes the ACL). It ships NO
|
|
160
187
|
# repair command, for the reason the standing-grant section ships none: this
|
|
161
188
|
# project has no Windows host to verify a line on.
|
|
162
189
|
|
package/lib/advice.js
CHANGED
|
@@ -132,11 +132,16 @@
|
|
|
132
132
|
* normal token while writing and deleting use the restricted one, so an object
|
|
133
133
|
* whose DACL names only `Administrators`/`SYSTEM` plus the capability SID is
|
|
134
134
|
* refused on the read side too and looks granted to a grep for the SID
|
|
135
|
-
* (#423 measured it).
|
|
136
|
-
* sentence
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
135
|
+
* (#423 measured it). And the second branch is now **first-class rather than a
|
|
136
|
+
* sentence**: the same root-only short-circuit on the *label* half, where the
|
|
137
|
+
* DACL reaches every object and the mandatory-integrity label reaches none of
|
|
138
|
+
* the subdirectories, so nothing below the root is writable at all (`#8383`,
|
|
139
|
+
* measured against `0.2.0-rc.2` with the decisive control of a directory
|
|
140
|
+
* created *after* the grant). The two branches are indistinguishable from
|
|
141
|
+
* inside a session and permanent for the same reason, so the text forks them
|
|
142
|
+
* on **breadth** — the one fact the reader already owns — before it hands over
|
|
143
|
+
* any check, and separates them from outside by the repository's own
|
|
144
|
+
* `diagnose-windows-sandbox-acl` and its `LOW_LABEL` report.
|
|
140
145
|
*
|
|
141
146
|
* Both give a **discriminator, not just a remedy**: applying a fix without
|
|
142
147
|
* confirming the cause teaches nothing when the fix does not work. For the ACL
|
|
@@ -156,7 +161,10 @@
|
|
|
156
161
|
* against: the reader can audit the plugin's own reasoning instead of taking a
|
|
157
162
|
* claim about two strings on faith. That family's check is also the one place
|
|
158
163
|
* where a single fact is not enough — reachability has two sides, and the text
|
|
159
|
-
* says which one a check on the ACE alone would miss
|
|
164
|
+
* says which one a check on the ACE alone would miss — and it is the one family
|
|
165
|
+
* with a **second cut before the check**: which half is missing is settled by how
|
|
166
|
+
* MUCH of the tree is affected, because a reader who repairs the DACL of a
|
|
167
|
+
* label-starved workspace has spent a privilege for nothing.
|
|
160
168
|
*
|
|
161
169
|
* @module
|
|
162
170
|
*/
|
|
@@ -189,17 +197,19 @@ export const PTY_DISCUSSIONS = '#7638 / #8322';
|
|
|
189
197
|
* launched from a terminal does not, which is the same variable the PTY family
|
|
190
198
|
* now names.
|
|
191
199
|
*/
|
|
192
|
-
export const NATIVE_INIT_DISCUSSIONS = '#7876 / #7877 / #8193 / #8208 / #8313';
|
|
200
|
+
export const NATIVE_INIT_DISCUSSIONS = '#7876 / #7877 / #8193 / #8208 / #8313 / #8336 / #8334';
|
|
193
201
|
/**
|
|
194
|
-
* The upstream
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
202
|
+
* The upstream threads the workspace-denial advisory is a stopgap for.
|
|
203
|
+
*
|
|
204
|
+
* `#423` is the failure and its first measurements: 170 of 729 objects missing
|
|
205
|
+
* the grant, root-level files among them, and the two-sided reachability rule.
|
|
206
|
+
* Its second comment named the mandatory-label variant of the same shape, which
|
|
207
|
+
* `#8383` then measured on its own — the exact complement, with the DACL
|
|
208
|
+
* present and inheriting correctly at every level while the label reaches none
|
|
209
|
+
* of the subdirectories, so the variant stopped being a footnote and became the
|
|
210
|
+
* second branch the discriminator forks into.
|
|
201
211
|
*/
|
|
202
|
-
export const WORKSPACE_DENIAL_DISCUSSIONS = '#423';
|
|
212
|
+
export const WORKSPACE_DENIAL_DISCUSSIONS = '#423 / #8383';
|
|
203
213
|
/**
|
|
204
214
|
* The thread list each family's withholding note cites.
|
|
205
215
|
*
|
|
@@ -682,12 +692,31 @@ function nativeInitAdvisory(failure, mode, onElectron, href) {
|
|
|
682
692
|
'Honest boundary — 0xC0000142 has producers this list does not have: a program that cannot load one of',
|
|
683
693
|
'its own DLLs dies this way too, and the backend\'s own source records the console case as an inherent',
|
|
684
694
|
'limit of the backend (`CREATE_NO_WINDOW` / `CREATE_NEW_CONSOLE` children die with `STATUS_DLL_INIT_FAILED`',
|
|
685
|
-
'under the restriction). #8208 explains that limit instead of repeating it, and the explanation is
|
|
686
|
-
'
|
|
687
|
-
'
|
|
688
|
-
'
|
|
689
|
-
'
|
|
690
|
-
'
|
|
695
|
+
'under the restriction). #8208 explains that limit instead of repeating it, and the explanation is a single',
|
|
696
|
+
'rule: in a restricted token a console can be INHERITED but not CREATED. Both shapes above follow from it —',
|
|
697
|
+
'the child needs a console it did not create, and anything that forces it to CREATE one is fatal on its own.',
|
|
698
|
+
'#8336 measured that side directly, on one machine, with a console-owning host, a restricted token and the',
|
|
699
|
+
'Low integrity level, varying only the creation flags: `0`, `DETACHED_PROCESS` and `CREATE_NEW_PROCESS_GROUP`',
|
|
700
|
+
'all reached the program, while `CREATE_NO_WINDOW` and `CREATE_NEW_CONSOLE` both died with the code above.',
|
|
701
|
+
'`STARTF_USESHOWWINDOW` with `SW_HIDE` — how a window is hidden without isolating a console — was harmless,',
|
|
702
|
+
'and so was `CREATE_NO_WINDOW` on an UNRESTRICTED token, which is what makes the two a pair rather than a',
|
|
703
|
+
'list of forbidden flags.',
|
|
704
|
+
'The creation flags this harness actually passes are three sets and none of them is `CREATE_NO_WINDOW` —',
|
|
705
|
+
'`0` on the piped path, `CREATE_SUSPENDED` on the inherited-job path, and',
|
|
706
|
+
'`CREATE_SUSPENDED | CREATE_UNICODE_ENVIRONMENT` on the ordinary path (that constant is not defined anywhere',
|
|
707
|
+
'in the process source). Those three are fatal under a console-less host and harmless under a host that owns',
|
|
708
|
+
'a console, so there the console decides. A flag that forces creation is a different animal: it decides even',
|
|
709
|
+
'when a console is there to be inherited.',
|
|
710
|
+
'',
|
|
711
|
+
'One thing that gets suspected and is not the cause: `windowsHide`. It is named here because it is the',
|
|
712
|
+
'first thing a search turns up, and it points at the wrong component — the flag is not set anywhere on the',
|
|
713
|
+
'confined path above. It appears on the ORDINARY subprocess path (`dsh-subprocess-local`: `windowsHide:`',
|
|
714
|
+
'`platform === \'win32\'`), which starts the runner rather than the confined child, and the restricted spawn',
|
|
715
|
+
'in `dsh-win32-process` passes the three flag sets just listed and defines no `CREATE_NO_WINDOW` constant at',
|
|
716
|
+
'all. Where it IS set — on the host — #8208 measured it both ways on a host that works: with and without',
|
|
717
|
+
'`windowsHide`, the confined `pwsh` reached exit 0 with its own stdout intact. The reason is the rule again:',
|
|
718
|
+
'a windowless console is still a console, and that is what the child inherits. What decides is whether the',
|
|
719
|
+
'host owns a console OBJECT, not whether it owns a window.',
|
|
691
720
|
'',
|
|
692
721
|
'One arm of this family was applied, measured, and rejected — named so a reader does not reach for it:',
|
|
693
722
|
'putting `DETACHED_PROCESS` on the RESTRICTED CHILD removes its console request and does stop the crash,',
|
|
@@ -785,19 +814,25 @@ function ptyAdvisory(failure, mode, tool, href) {
|
|
|
785
814
|
* against**, because the family's claim is a statement about two strings and the
|
|
786
815
|
* reader is entitled to audit it. It must give the **two-sided** reachability
|
|
787
816
|
* rule, since a check that only looks for the capability ACE reports an object
|
|
788
|
-
* as granted when the read side is refused as well. And it must
|
|
789
|
-
* **
|
|
790
|
-
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
794
|
-
*
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
*
|
|
817
|
+
* as granted when the read side is refused as well. And it must split the
|
|
818
|
+
* diagnosis into its **two branches** — the DACL entry that did not arrive, and
|
|
819
|
+
* the mandatory-integrity LABEL that did not (#8383) — because from inside a
|
|
820
|
+
* session they are indistinguishable, both are permanent for the same
|
|
821
|
+
* root-only-short-circuit reason, and a reader who repairs the wrong half has
|
|
822
|
+
* learned nothing. The fork is placed on **breadth**, which is the one fact the
|
|
823
|
+
* reader already has: a handful of stubborn objects is the DACL branch, and
|
|
824
|
+
* nothing below the root writable at all is the label branch.
|
|
825
|
+
*
|
|
826
|
+
* What it must not do is print a repair command. For the DACL branch the obvious
|
|
827
|
+
* one is refused by Windows with `ERROR_NONE_MAPPED` (1332) — a capability SID
|
|
828
|
+
* has no name for `icacls` to map — and the one that would work needs
|
|
829
|
+
* `WRITE_DAC` on the object, which is the right in question. For the label
|
|
830
|
+
* branch the shape of the move is a SACL write on the whole subtree, which needs
|
|
831
|
+
* the same class of object right and is not verified here either. This project
|
|
832
|
+
* has no Windows host to verify a line on, and the standing-grant section of the
|
|
833
|
+
* ACL advisory is held to the same standard for the same reason: an unverified
|
|
834
|
+
* remedy delivered confidently is the defect this plugin exists to answer. The
|
|
835
|
+
* mechanism is stated instead, and the reader is told why the command is absent.
|
|
801
836
|
* @param failure - the recognized failure.
|
|
802
837
|
* @param tool - the tool whose call was denied, when the caller knows it.
|
|
803
838
|
* @param href - optional URL shown for the upstream thread.
|
|
@@ -810,7 +845,8 @@ function workspaceDenialAdvisory(failure, tool, href) {
|
|
|
810
845
|
const call = tool === undefined ? 'This command' : `The \`${tool}\` command`;
|
|
811
846
|
const subject = failure.paths[0] ?? '<the path from the error line above>';
|
|
812
847
|
return [
|
|
813
|
-
'Denied inside your own workspace — the workspace
|
|
848
|
+
'Denied inside your own workspace — the access the workspace is supposed to carry does not cover that part of',
|
|
849
|
+
'the tree, and retrying cannot repair it.',
|
|
814
850
|
'',
|
|
815
851
|
'What was reported:',
|
|
816
852
|
` ${failureLine(failure)}`,
|
|
@@ -818,8 +854,8 @@ function workspaceDenialAdvisory(failure, tool, href) {
|
|
|
818
854
|
'path it named is inside this session\'s workspace:',
|
|
819
855
|
...failure.paths.map(path => ` ${path}`),
|
|
820
856
|
` (workspace root: ${failure.workspaceRoot})`,
|
|
821
|
-
'So this is not the sandbox declining work that belongs outside the workspace. It is the workspace\'s own
|
|
822
|
-
'failing to cover an object inside it, which is why every command touching that object fails the same way.',
|
|
857
|
+
'So this is not the sandbox declining work that belongs outside the workspace. It is the workspace\'s own access',
|
|
858
|
+
'setup failing to cover an object inside it, which is why every command touching that object fails the same way.',
|
|
823
859
|
'',
|
|
824
860
|
'Why — and why retrying cannot fix it:',
|
|
825
861
|
' The host-side grant is written ONCE, on the workspace ROOT, and relies on Windows ACE inheritance to reach',
|
|
@@ -835,36 +871,85 @@ function workspaceDenialAdvisory(failure, tool, href) {
|
|
|
835
871
|
' agent harness running under its own account) are the ones the propagation skipped. #423 measured 170 of 729',
|
|
836
872
|
' objects missing it, INCLUDING root-level files — so it is not only subdirectories, and "write it at the',
|
|
837
873
|
' workspace root instead" is not a safe move either.',
|
|
874
|
+
' The same call also writes the mandatory-integrity LABEL, and that half can fail to reach the tree while the',
|
|
875
|
+
' ACE half arrives — that is the second branch below, and it produces this identical denial. Both halves are',
|
|
876
|
+
' permanent for the same reason, so settling WHICH one you are looking at comes before anything else.',
|
|
838
877
|
'',
|
|
839
|
-
'Confirm it —
|
|
878
|
+
'Confirm it — and settle FIRST which of the two halves is missing, because both produce this same denial and a',
|
|
879
|
+
'reader who repairs the wrong one has learned nothing:',
|
|
840
880
|
` icacls "${subject}"`,
|
|
841
|
-
|
|
842
|
-
'
|
|
843
|
-
'
|
|
881
|
+
` icacls "${failure.workspaceRoot}"`,
|
|
882
|
+
'BREADTH IS THE FIRST CUT, and it needs no tool — it is what your own attempts have already shown:',
|
|
883
|
+
' - A HANDFUL of stubborn objects while the rest of the tree writes normally → the DACL half. The capability ACE',
|
|
884
|
+
' did not arrive at those objects; the workspace is otherwise usable. This is the #423 measurement.',
|
|
885
|
+
' - NOTHING below the root is writable at all, with the root itself the only writable place → the LABEL half',
|
|
886
|
+
' (#8383). A Low-integrity child may write to a directory only when that directory\'s own mandatory-integrity',
|
|
887
|
+
' label is Low as well; the kernel runs its no-write-up check IN ADDITION to the access check, so a DACL that',
|
|
888
|
+
' is perfect everywhere changes nothing.',
|
|
889
|
+
'',
|
|
890
|
+
'Half one — the DACL ACE did not reach the object:',
|
|
891
|
+
' The root carries an inheritable ACE for the workspace capability SID — a `S-1-4-…` that `icacls` prints as an',
|
|
892
|
+
' unresolved SID rather than a name — with Modify or Full control; the failing object does not have it. Check each',
|
|
893
|
+
' path listed above the same way; the first is shown here.',
|
|
844
894
|
' THE TRAP: reading and listing go through the NORMAL token, writing and deleting through the RESTRICTED',
|
|
845
895
|
' (low-integrity) one, and both sides must pass. An object whose DACL names only Administrators/SYSTEM plus the',
|
|
846
896
|
' capability SID is refused on the read side as well, so it looks granted to a check that only searches for the',
|
|
847
897
|
' capability SID — #423 measured exactly that on a `.cache` directory. A check that asks only "is the SID there?"',
|
|
848
898
|
' reports those objects as fine while LIST and WRITE are both denied.',
|
|
849
|
-
'
|
|
850
|
-
'
|
|
851
|
-
'
|
|
852
|
-
'
|
|
853
|
-
'
|
|
899
|
+
' TEST: does the object show the capability ACE at all, inherited or not? Absent → this half, and the object is',
|
|
900
|
+
' the thing to repair.',
|
|
901
|
+
'',
|
|
902
|
+
'Half two — the DACL arrived and the LABEL did not (#8383, measured 2026-09-30):',
|
|
903
|
+
' Measured on a `0.2.0-rc.2` install (Windows 11, 26200 family, NTFS): the workspace root writable, EVERY',
|
|
904
|
+
' subdirectory denied, and — this is what makes it the label half — the capability ACE present and correctly',
|
|
905
|
+
' inherited on every one of them. The label, meanwhile, exists on the root alone:',
|
|
906
|
+
' icacls "<root>" → Mandatory Label\\Low Mandatory Level:(OI)(CI)(NW)',
|
|
907
|
+
' icacls "<subdir>" → no Mandatory Label line at all',
|
|
908
|
+
' The decisive form of that measurement is a directory created AFTER the grant was materialized: it inherits the',
|
|
909
|
+
' capability ACE (marked `(I)`) and still receives no label. That is what rules out "these objects simply predate',
|
|
910
|
+
' the grant" and isolates the failure to the label layer. One caveat the report itself is careful about and worth',
|
|
911
|
+
' keeping: a GRANDCHILD directory having no label proves nothing on its own, because inheritance is per-parent and',
|
|
912
|
+
' the intermediate directory carries none — only a DIRECT child of the labelled root is evidence.',
|
|
913
|
+
' Why it is permanent: the label goes out in the same single security-descriptor write that carries the grant,',
|
|
914
|
+
' with the inheritance flag set, but only on the paths the backend enumerates — the workspace root (plus the',
|
|
915
|
+
' session\'s private temp directory), with no descendant walk. The root-only idempotency check then requires the',
|
|
916
|
+
' grant, the world delete-child deny AND the exact label (`hasExactGrant()` + `hasExactDeny()` + `hasExactLabel()`',
|
|
917
|
+
' all matching) before it returns early, so once the root is labelled the propagation is never attempted again and',
|
|
918
|
+
' the children that missed it are never revisited — the same root-only short-circuit as the DACL half, on the',
|
|
919
|
+
' 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).',
|
|
921
|
+
' The package README describes that label as inheritable and covering the tree, and its grant materialization as',
|
|
922
|
+
' an eager full-tree propagation. Read it as a statement about the ACE that is written, not about what the',
|
|
923
|
+
' children ended up carrying: the inheritance flag is declared on the root, the propagation to the children is',
|
|
924
|
+
' what is missing. From outside a session the repository\'s own diagnosis skill separates the two halves —',
|
|
925
|
+
' `diagnose-windows-sandbox-acl` (0.2.0 and later) prints the directory\'s owner, the caller\'s rights and',
|
|
926
|
+
' `WRITE_DAC`/`WRITE_OWNER` — the facts the DACL half turns on — and `LOW_LABEL` (`S-1-16-4096`) for this one.',
|
|
927
|
+
' TEST: does a subdirectory show the capability ACE WITHOUT a Mandatory Label line? Yes → this half.',
|
|
854
928
|
'',
|
|
855
929
|
'Why the natural repair is closed — this is a Windows ceiling, not a mistake in the command:',
|
|
856
930
|
' The grant the root carries is written for a capability SID, and a recursive `icacls /grant "*S-1-4-…:…" /T /C`',
|
|
857
931
|
' is refused with ERROR_NONE_MAPPED (1332): the tool cannot map that SID to a name, so a grant that needs to name',
|
|
858
932
|
' it never reaches the child. Widening the grant to a nameable account is a different grant with different',
|
|
859
|
-
' consequences, not this one restored.',
|
|
933
|
+
' consequences, not this one restored. For the label half the same ceiling applies one layer over: re-labelling a',
|
|
934
|
+
' subtree is a SACL write, which needs the object right the merged call already needed, and a manually widened',
|
|
935
|
+
' DACL does NOT clear the failure — #8383 confirmed that adding an explicit FullControl entry for the user on a',
|
|
936
|
+
' subdirectory still left the write denied, because the refusal happens before the ACL is ever consulted.',
|
|
860
937
|
'',
|
|
861
938
|
'What helps:',
|
|
862
939
|
' - The move that is yours, and the only one available inside the session: write new files under a directory the',
|
|
863
940
|
' harness itself created in this workspace. Those carry the grant, because their inheritance did apply — the',
|
|
864
941
|
' reporter\'s own measurement, that objects DSH created are complete here and externally created ones are not.',
|
|
865
|
-
'
|
|
866
|
-
' the
|
|
867
|
-
'
|
|
942
|
+
' Treat this as a workaround and not a repair, and on the LABEL half expect even this to be unreliable: the',
|
|
943
|
+
' directories the harness creates in a fresh workspace are children of a root whose label did not propagate, so',
|
|
944
|
+
' whether any given one works is a fact about the host you have in front of you rather than something this text',
|
|
945
|
+
' can promise.',
|
|
946
|
+
' - The user\'s move, per half: for the DACL half, repair the specific object from an account that already holds',
|
|
947
|
+
' WRITE_DAC on it, by writing the ACE the root carries into the child\'s own DACL. For the label half, the',
|
|
948
|
+
' corresponding move is on the integrity label rather than the DACL, and it has to cover the tree, not one',
|
|
949
|
+
' object; it is the same class of object write, so it is not something an unelevated prompt can do either.',
|
|
950
|
+
' - The route that costs no rights at all, and the honest recommendation while the halves remain unrepairable from',
|
|
951
|
+
' inside: build and run where the confinement is not the thing under test — which is a decision about what you',
|
|
952
|
+
' are doing, not a sandbox tier to reach for silently.',
|
|
868
953
|
' No repair command is printed here on purpose. This project has no Windows host to verify one on, and shipping',
|
|
869
954
|
' an unverified line would be the same defect this plugin exists to answer. The mechanism above is what is known;',
|
|
870
955
|
' the line is yours to choose.',
|
|
@@ -873,8 +958,8 @@ function workspaceDenialAdvisory(failure, tool, href) {
|
|
|
873
958
|
'mode. That retry can only succeed by removing the confinement itself — it repairs nothing in the workspace, and',
|
|
874
959
|
'the next session meets the same gap. Take it only if that is what you mean to buy.',
|
|
875
960
|
'',
|
|
876
|
-
'Do NOT retry this call unchanged: the provisioning path short-circuits on the root
|
|
877
|
-
'
|
|
961
|
+
'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.',
|
|
878
963
|
'',
|
|
879
964
|
'How to read this: the denial names no cause and points at no repair, so the diagnosis is delivered here instead.',
|
|
880
965
|
'This is a stopgap, ' + where + '. This plugin speaks only when the mode is `workspace-write`, the host is',
|
package/lib/types/advice.d.ts
CHANGED
|
@@ -132,11 +132,16 @@
|
|
|
132
132
|
* normal token while writing and deleting use the restricted one, so an object
|
|
133
133
|
* whose DACL names only `Administrators`/`SYSTEM` plus the capability SID is
|
|
134
134
|
* refused on the read side too and looks granted to a grep for the SID
|
|
135
|
-
* (#423 measured it).
|
|
136
|
-
* sentence
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
135
|
+
* (#423 measured it). And the second branch is now **first-class rather than a
|
|
136
|
+
* sentence**: the same root-only short-circuit on the *label* half, where the
|
|
137
|
+
* DACL reaches every object and the mandatory-integrity label reaches none of
|
|
138
|
+
* the subdirectories, so nothing below the root is writable at all (`#8383`,
|
|
139
|
+
* measured against `0.2.0-rc.2` with the decisive control of a directory
|
|
140
|
+
* created *after* the grant). The two branches are indistinguishable from
|
|
141
|
+
* inside a session and permanent for the same reason, so the text forks them
|
|
142
|
+
* on **breadth** — the one fact the reader already owns — before it hands over
|
|
143
|
+
* any check, and separates them from outside by the repository's own
|
|
144
|
+
* `diagnose-windows-sandbox-acl` and its `LOW_LABEL` report.
|
|
140
145
|
*
|
|
141
146
|
* Both give a **discriminator, not just a remedy**: applying a fix without
|
|
142
147
|
* confirming the cause teaches nothing when the fix does not work. For the ACL
|
|
@@ -156,7 +161,10 @@
|
|
|
156
161
|
* against: the reader can audit the plugin's own reasoning instead of taking a
|
|
157
162
|
* claim about two strings on faith. That family's check is also the one place
|
|
158
163
|
* where a single fact is not enough — reachability has two sides, and the text
|
|
159
|
-
* says which one a check on the ACE alone would miss
|
|
164
|
+
* says which one a check on the ACE alone would miss — and it is the one family
|
|
165
|
+
* with a **second cut before the check**: which half is missing is settled by how
|
|
166
|
+
* MUCH of the tree is affected, because a reader who repairs the DACL of a
|
|
167
|
+
* label-starved workspace has spent a privilege for nothing.
|
|
160
168
|
*
|
|
161
169
|
* @module
|
|
162
170
|
*/
|
|
@@ -190,17 +198,19 @@ export declare const PTY_DISCUSSIONS = "#7638 / #8322";
|
|
|
190
198
|
* launched from a terminal does not, which is the same variable the PTY family
|
|
191
199
|
* now names.
|
|
192
200
|
*/
|
|
193
|
-
export declare const NATIVE_INIT_DISCUSSIONS = "#7876 / #7877 / #8193 / #8208 / #8313";
|
|
201
|
+
export declare const NATIVE_INIT_DISCUSSIONS = "#7876 / #7877 / #8193 / #8208 / #8313 / #8336 / #8334";
|
|
194
202
|
/**
|
|
195
|
-
* The upstream
|
|
203
|
+
* The upstream threads the workspace-denial advisory is a stopgap for.
|
|
196
204
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
205
|
+
* `#423` is the failure and its first measurements: 170 of 729 objects missing
|
|
206
|
+
* the grant, root-level files among them, and the two-sided reachability rule.
|
|
207
|
+
* Its second comment named the mandatory-label variant of the same shape, which
|
|
208
|
+
* `#8383` then measured on its own — the exact complement, with the DACL
|
|
209
|
+
* present and inheriting correctly at every level while the label reaches none
|
|
210
|
+
* of the subdirectories, so the variant stopped being a footnote and became the
|
|
211
|
+
* second branch the discriminator forks into.
|
|
202
212
|
*/
|
|
203
|
-
export declare const WORKSPACE_DENIAL_DISCUSSIONS = "#423";
|
|
213
|
+
export declare const WORKSPACE_DENIAL_DISCUSSIONS = "#423 / #8383";
|
|
204
214
|
/**
|
|
205
215
|
* The thread list each family's withholding note cites.
|
|
206
216
|
*
|
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. 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 measured variant of the same shape (coverage missing on the mandatory-integrity LABEL rather than on the DACL, separable outside a session only by the repository's own `diagnose-windows-sandbox-acl`), 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) — 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.
|
|
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",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|