@argszero/cordis-plugin-sandbox-grant-advisor 0.8.1 → 0.9.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
@@ -35,12 +35,14 @@ stuck.
35
35
 
36
36
  ### 1. Workspace provisioning — the Windows ACL failure (`acl-provisioning`)
37
37
 
38
- Seven reports describe this exact line: [discussion #7538], [discussion #7622],
39
- [discussion #7646], [discussion #7720], [discussion #7750], [discussion #7735] and
40
- [discussion #8232] (the last three on data-volume workspaces, where *no* ACE names
41
- the caller at all — the inherited `Authenticated Users: Modify` is the whole of
42
- their access). In each one every sandboxed command fails the same way, before it
43
- runs, and the error names neither the missing right nor a remedy.
38
+ Twelve reports describe this exact line: [discussion #7538], [discussion #7622],
39
+ [discussion #7646], [discussion #7720], [discussion #7750], [discussion #7735],
40
+ [discussion #7771], [discussion #7804], [discussion #7816], [discussion #8232],
41
+ [discussion #8272] and [discussion #8275] (three of them — [#7750], [#7735],
42
+ [#8232] — on data-volume workspaces, where *no* ACE names the caller at all: the
43
+ inherited `Authenticated Users: Modify` is the whole of their access). In each one
44
+ every sandboxed command fails the same way, before it runs, and the error names
45
+ neither the missing right nor a remedy.
44
46
 
45
47
  `#8232` contributes two facts about the *shape* of the failure rather than its
46
48
  cause, and both are in the advisory now. One is that the failure belongs to the
@@ -54,6 +56,39 @@ repaired. The report also reaches the same root cause on its own (the merged wri
54
56
  wanting `WRITE_OWNER`, which owner-implicit rights do not carry), matching the
55
57
  backend's documented prerequisite.
56
58
 
59
+ `#8272` and `#8275` add neither a cause nor a remedy — both land on the same
60
+ missing right — but each one closes a reading the diagnosis leaves open, and both
61
+ are in the advisory since 0.9.0.
62
+
63
+ `#8272` reached the same conclusion by a **better probe than the one above**: it
64
+ set a Low integrity level on a directory it owned, unelevated
65
+ (`icacls <dir> /setintegritylevel "(OI)(CI)Low"`), and was refused. That isolates
66
+ the label half of the merged call, which is the half that needs `WRITE_OWNER`, and
67
+ it does so without asking anyone to interpret a merged failure. It is quoted here
68
+ as evidence and deliberately **not** offered as a check: where the caller holds Full
69
+ control the same command *succeeds*, and then the label — and its inheritance — is
70
+ already written. A diagnostic that writes when it succeeds is not a diagnostic this
71
+ plugin hands out; the two read-only checks above are.
72
+
73
+ The report then went looking for the backend's own repair
74
+ (`diagnose-windows-sandbox-acl`) on a `0.1.7-rc.2` install and found it missing,
75
+ which it read as the package having dropped the skill from its `files` glob. It has
76
+ not: measured on the published tarballs (2026-09-29), `0.1.7-rc.2` names the skill
77
+ **zero times** in either README, ships no `assets/` directory at all, and carries no
78
+ file containing the registration symbol — while `0.2.0-rc.2` ships
79
+ `assets/diagnose-windows-sandbox-acl/{SKILL.md,scripts/diagnose-windows-sandbox-acl.ps1}`
80
+ and names it three times per README. The skill is **new in `0.2.0`**; a `0.1.7`
81
+ install that found the name was reading a `0.2.0`-era document. So the reach that
82
+ works is the upgrade, not a packaging fix, and the advisory says so — item 4 below.
83
+
84
+ `#8275` is the other direction: it had the skill, and the skill's own remedy
85
+ (Full control) *worked*, after which it noticed that the Low label the successful
86
+ apply writes is what regresses the two side effects it documents. Its proposal is to
87
+ **degrade provisioning to DACL-only** when the label is the half being refused,
88
+ which reads as though the label were one of two independent layers. It is not, and
89
+ the advisory answers that with the mechanism rather than with a refusal — item 6
90
+ below.
91
+
57
92
  `#7720` is worth reading for where the failure lands: the grant is materialized
58
93
  at sandbox **initialization**, so this is not one refused operation but *every*
59
94
  shell tool at once — the reporter could not run `netstat` or even `icacls` to
@@ -105,6 +140,16 @@ Two consequences follow from that one line:
105
140
  what confines deletes to the workspace, and reverting it reintroduces the
106
141
  escape it closed. `#7750` asks for exactly this and explains why it is a
107
142
  usability regression traded for a security fix.
143
+
144
+ There is a **second boundary on that same line**, and `#8272` is what made it
145
+ worth stating: the backend's own repair, `diagnose-windows-sandbox-acl`, is not
146
+ part of `0.1.7-*` at all — it arrives with the **`0.2.0`** line, where the
147
+ package starts shipping it under `assets/`. A reader who found the skill named
148
+ in a README while running `0.1.7-rc.2` was reading a `0.2.0`-era document, not a
149
+ package that dropped something: that release's own README names it zero times and
150
+ no file in its tree carries the registration symbol. So the useful reach is the
151
+ upgrade, and the unhelpful one — repairing a `files` glob in a release that has
152
+ no such directory — is not offered.
108
153
  5. **The repair is per-directory, and the report is what established that.** `#8232`
109
154
  applied the `(WO)` line, watched the workspace start working, and then hit the
110
155
  same error on a *second* workspace root on the same volume. `(OI)(CI)` carries
@@ -112,6 +157,25 @@ Two consequences follow from that one line:
112
157
  sibling root is untouched by it. One line per workspace root is therefore the
113
158
  correct shape of the remedy, not one line per machine — and the advisory says so,
114
159
  because the natural reading of "it worked" is "it is fixed".
160
+ 6. **A "weaker grant" is a mechanism this backend cannot express, not a policy it
161
+ declines** (added in 0.9.0, from `#8275`). The label is what makes the apply a
162
+ SACL write, so declining it looks like dropping the expensive half — but there is
163
+ no DACL-only path to fall back to: the label rides the **same**
164
+ `SetNamedSecurityInfoW` as the grant (`acl.ts`: the flags are
165
+ `DACL_SECURITY_INFORMATION` alone only when the label edit is `keep`, and
166
+ `grantWrite`'s apply branch always passes `apply`), and the idempotence fast path
167
+ requires the exact label among its three conditions. And dropping the label alone
168
+ would not leave a working workspace: the confined token is itself lowered to Low
169
+ before any child starts (`token.ts`'s `restrictTokenIntegrity`, whose own comment
170
+ calls Low "the level the mandatory labels `grantWrite` applies are matched
171
+ against"), so the directory's Low label is what lets that Low child write there at
172
+ all under no-write-up. A DACL-only mode that keeps the token lowering yields a
173
+ workspace the sandbox can start a command in and the command then cannot write to;
174
+ one that drops the token lowering too — which is what `#8275`'s own appendix
175
+ records a community patch having to do — gives up half the confinement rather than
176
+ one of two independent layers. The advisory says this instead of a bare "no",
177
+ because a reader told only "no" reaches for the workaround without knowing what
178
+ else it has to change.
115
179
 
116
180
  The grant is materialized lazily, on the first confined call, and **nothing is
117
181
  cached when it throws** — so the same failure repeats per command (850 calls
@@ -134,6 +198,10 @@ Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4) ... `0.1.7-alph
134
198
  label — and with it the SACL, flag 20 — arrives. ... Rolling back is not the fix either: the label is what
135
199
  confines deletes to the workspace, and reverting it reintroduces the escape it closed.
136
200
 
201
+ The other boundary on that last line: the `diagnose-windows-sandbox-acl` skill is not part of `0.1.7-*` at all
202
+ — it arrives with `0.2.0`, where the backend starts shipping it under `assets/`. The reach that works is the
203
+ upgrade, not a packaging fix.
204
+
137
205
  Confirm the cause (unelevated) — `icacls` is a normal user command:
138
206
  icacls "D:\ws"
139
207
 
@@ -162,6 +230,14 @@ What will NOT fix it on its own — both look like the right move, and both were
162
230
  supplies the DACL half and still not WRITE_OWNER, the right this call needs.
163
231
  icacls "D:\ws" /reset /T /C
164
232
  restores inheritance, and inheritance is what supplied the Modify-only ACE above.
233
+
234
+ One thing to know before asking for a weaker grant, because that is the next idea after this diagnosis —
235
+ and it is not a smaller version of the same grant:
236
+ The label rides the SAME `SetNamedSecurityInfoW` as the DACL, so there is no DACL-only path to fall back
237
+ to — it would have to be built. And dropping the label alone would not leave a working workspace: the
238
+ backend lowers the confined token to Low before any child starts, and the directory's Low label is what
239
+ lets that Low child write here at all. Declining the label usefully means declining the token's Low level
240
+ with it, which gives up half the confinement rather than one of two independent layers.
165
241
  ```
166
242
 
167
243
  **Why the remedy forks** (added in 0.5.0). The same error covers two different
@@ -633,10 +709,19 @@ the newest of that line.
633
709
 
634
710
  The whole set is re-probed whenever this package's source changes rather than
635
711
  carried over from an earlier version: the range is a claim about *this* build of
636
- the plugin, so `0.7.1` re-ran all five lines above. A line whose probe fails is
637
- removed from the range rather than left claimed. The scratch tree's resolved
638
- versions are the ones to read back when a probe is quoted as evidence — the probe
639
- script pins them by exact version, and `--keep` leaves the tree in place to check.
712
+ the plugin, so `0.7.1` re-ran all five lines above and `0.9.0` re-ran them again. A
713
+ line whose probe fails is removed from the range rather than left claimed. The
714
+ scratch tree's resolved versions are the ones to read back when a probe is quoted
715
+ as evidence — the probe script pins them by exact version, and `--keep` leaves the
716
+ tree in place to check.
717
+
718
+ **The next line's pre-releases are outside that range on purpose**, and the guard
719
+ in `test/packaging.spec.mjs` enforces it (a range that admits `0.2.0` fails the
720
+ suite). `0.2.0-rc.2` — the build the newest report in this family runs, including
721
+ its Desktop variant — was nevertheless probed by hand (`npm run test:probe-lines --
722
+ 0.2.0-rc.2`) and the suite goes green there, which is the honest reason to expect
723
+ the plugin to work on it. It is a measurement, not a claim: the range widens when
724
+ the `0.2.0` line is the released one rather than its pre-release.
640
725
 
641
726
  The mode lookup stays guarded through this version too: the native-init family
642
727
  needs the same resolved mode as the PTY family, and it takes it from the same
@@ -676,6 +761,8 @@ the current runtime cannot distinguish rather than counting it as a pass.
676
761
  [discussion #7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
677
762
  [discussion #7816]: https://github.com/deepseek-ai/deepseek-harness/discussions/7816
678
763
  [discussion #8232]: https://github.com/deepseek-ai/deepseek-harness/discussions/8232
764
+ [discussion #8272]: https://github.com/deepseek-ai/deepseek-harness/discussions/8272
765
+ [discussion #8275]: https://github.com/deepseek-ai/deepseek-harness/discussions/8275
679
766
  [discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
680
767
  [discussion #7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
681
768
  [discussion #7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
@@ -686,6 +773,8 @@ the current runtime cannot distinguish rather than counting it as a pass.
686
773
  [#7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
687
774
  [#7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
688
775
  [#8232]: https://github.com/deepseek-ai/deepseek-harness/discussions/8232
776
+ [#8272]: https://github.com/deepseek-ai/deepseek-harness/discussions/8272
777
+ [#8275]: https://github.com/deepseek-ai/deepseek-harness/discussions/8275
689
778
  [#8193]: https://github.com/deepseek-ai/deepseek-harness/discussions/8193
690
779
  [#8174]: https://github.com/deepseek-ai/deepseek-harness/discussions/8174
691
780
  [#8208]: https://github.com/deepseek-ai/deepseek-harness/discussions/8208
package/lib/advice.js CHANGED
@@ -39,6 +39,23 @@
39
39
  * one-liner would send the second environment to a command that is refused
40
40
  * before it runs — the same defect this module exists to answer, a remedy that
41
41
  * does not work delivered confidently.
42
+ *
43
+ * **Two later reports add shape and a boundary, not a new cause.** `#8272`
44
+ * arrived at the missing right with an independent probe (it set a Low
45
+ * integrity level on a directory it owned, unelevated, and was refused — the
46
+ * label half isolated from the merged call), and then went looking for the
47
+ * backend's own `diagnose-windows-sandbox-acl` skill on a `0.1.7-rc.2` install,
48
+ * where it does not exist: the skill is new in **`0.2.0`**, and that release's
49
+ * README is where the promise was read. So the boundary section states where
50
+ * the skill actually arrives, and calls the "the package dropped it" reading
51
+ * what it is — version skew, not a `files` glob. `#8275` adds the question the
52
+ * diagnosis invites: whether the label half can simply be declined, since the
53
+ * label is what makes the apply a SACL write. It can't, and the reason is a
54
+ * mechanism rather than a policy — the label rides the *same* call as the grant
55
+ * and the confined token is itself lowered to Low, so a DACL-only mode that
56
+ * keeps the token lowering yields a workspace the confined child cannot write
57
+ * to. That is stated instead of a bare "no", because a reader told only "no"
58
+ * reaches for the workaround without knowing what else it would have to change.
42
59
  * - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
43
60
  * caller — least of all by the model, which has no shell to run anything in.
44
61
  * So its advice says so and stops: the remedy is a user-side preset choice,
@@ -92,7 +109,7 @@
92
109
  */
93
110
  import { failureLine, STATUS_DLL_INIT_FAILED } from './signature.js';
94
111
  /** The upstream threads the ACL advisory is a stopgap for. */
95
- export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232';
112
+ export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275';
96
113
  /** The upstream thread the persistent-shell advisory is a stopgap for. */
97
114
  export const PTY_DISCUSSIONS = '#7638';
98
115
  /** The upstream threads the native-init-death advisory is a stopgap for. */
@@ -239,8 +256,57 @@ function versionBoundary() {
239
256
  '`0.1.6-alpha.x`-or-older line this exact failure therefore belongs to a different cause space, while on any',
240
257
  '`0.1.7-*` line it is this one. Rolling back is not the fix either: the label is what confines deletes to the',
241
258
  'workspace, and reverting it reintroduces the escape it closed.',
259
+ '',
260
+ 'The other boundary on that last line, for a reader who goes looking for the built-in repair: the',
261
+ '`diagnose-windows-sandbox-acl` skill is not part of `0.1.7-*` at all — it arrives with `0.2.0`, which is where',
262
+ 'the backend starts shipping it under `assets/`. A `0.1.7-rc.2` install that found the skill named in a README',
263
+ 'was reading a `0.2.0`-era document: that release\'s own README names it zero times, and no file in its tree',
264
+ 'carries the registration symbol. The reach that works is the upgrade, not a packaging fix — nothing was',
265
+ 'dropped from the `0.1.7` file list, because there was nothing in `0.1.7` to drop.',
242
266
  ].join('\n');
243
267
  }
268
+ /**
269
+ * Why a "weaker grant" is not a smaller version of the same thing.
270
+ *
271
+ * The natural next question after "this needs `WRITE_OWNER`" is whether the
272
+ * label can simply be declined — the label is what makes the apply a SACL write,
273
+ * so dropping it looks like dropping the expensive half. The answer is that the
274
+ * two halves are one mechanism, and the shape of the answer matters more than
275
+ * the verdict: a reader who is told only "no" reaches for the community workaround
276
+ * without knowing what else it has to change.
277
+ *
278
+ * Everything here is read off the shipped source rather than inferred: the label
279
+ * rides the **same** `SetNamedSecurityInfoW` as the grant (`acl.ts`: the
280
+ * security-information flags are `DACL_SECURITY_INFORMATION` alone only when the
281
+ * label edit is `keep`, and `grantWrite`'s apply branch always passes `apply`),
282
+ * and the confined token is lowered to Low before any child starts
283
+ * (`token.ts`'s `restrictTokenIntegrity`, whose own comment calls Low "the level
284
+ * the mandatory labels `grantWrite` applies are matched against"). The directory's
285
+ * Low label is therefore what lets the Low child write there at all under
286
+ * no-write-up — so "sandbox works, workspace unlabelled" is not a configuration
287
+ * this backend can express.
288
+ *
289
+ * Emitted only for `apply-denied`, the class whose diagnosis is the missing
290
+ * `WRITE_OWNER`: that is the one where the label is what gets refused, and so the
291
+ * only one where declining it is an idea a reader could have.
292
+ * @returns the section's lines.
293
+ */
294
+ function degradedGrant() {
295
+ return [
296
+ 'One thing to know before asking for a weaker grant, because that is the next idea after this diagnosis —',
297
+ 'and it is not a smaller version of the same grant:',
298
+ ' The label rides the SAME `SetNamedSecurityInfoW` as the DACL (one call, two security-information flags), so',
299
+ ' there is no DACL-only path to fall back to — it would have to be built. And dropping the label alone would',
300
+ ' not leave a working workspace: the backend lowers the confined token to Low before any child starts, and its',
301
+ ' own comment calls Low "the level the mandatory labels `grantWrite` applies are matched against". The',
302
+ ' directory\'s Low label is what lets that Low child write here at all; a workspace that keeps the token',
303
+ ' lowering but not the label is one the sandbox can start a command in and the command then cannot write to.',
304
+ ' Declining the label usefully means declining the token\'s Low level with it — which gives up half the',
305
+ ' confinement rather than one of two independent layers, and is a different proposal from dropping a layer',
306
+ ' that was never load-bearing.',
307
+ 'This is not offered here as a fix, and neither is `danger-full-access`.',
308
+ ];
309
+ }
244
310
  /**
245
311
  * Build the advisory attached to the failing tool result.
246
312
  *
@@ -329,8 +395,11 @@ function aclAdvisory(failure, href) {
329
395
  'inherits Full control for you — and open the session on that one.',
330
396
  '',
331
397
  ...nonFixes(failure, path),
398
+ ...(failure.klass === 'apply-denied' ? [...degradedGrant(), ''] : []),
332
399
  'How to read this: the harness documents the prerequisite (' + PREREQUISITE + ') and this',
333
400
  'error does not name it yet, so the advice is delivered here instead. This is a stopgap, ' + where + '.',
401
+ 'It arrives through the session rather than through a shell, which is the thing this failure has just taken',
402
+ 'away — the reason a repair script cannot be the answer at the moment it is needed.',
334
403
  'What it is NOT: this plugin neither edits ACLs nor elevates — the command above is yours to run.',
335
404
  'Your file read/write tools still work; only sandboxed command execution is blocked.',
336
405
  ].join('\n');
@@ -39,6 +39,23 @@
39
39
  * one-liner would send the second environment to a command that is refused
40
40
  * before it runs — the same defect this module exists to answer, a remedy that
41
41
  * does not work delivered confidently.
42
+ *
43
+ * **Two later reports add shape and a boundary, not a new cause.** `#8272`
44
+ * arrived at the missing right with an independent probe (it set a Low
45
+ * integrity level on a directory it owned, unelevated, and was refused — the
46
+ * label half isolated from the merged call), and then went looking for the
47
+ * backend's own `diagnose-windows-sandbox-acl` skill on a `0.1.7-rc.2` install,
48
+ * where it does not exist: the skill is new in **`0.2.0`**, and that release's
49
+ * README is where the promise was read. So the boundary section states where
50
+ * the skill actually arrives, and calls the "the package dropped it" reading
51
+ * what it is — version skew, not a `files` glob. `#8275` adds the question the
52
+ * diagnosis invites: whether the label half can simply be declined, since the
53
+ * label is what makes the apply a SACL write. It can't, and the reason is a
54
+ * mechanism rather than a policy — the label rides the *same* call as the grant
55
+ * and the confined token is itself lowered to Low, so a DACL-only mode that
56
+ * keeps the token lowering yields a workspace the confined child cannot write
57
+ * to. That is stated instead of a bare "no", because a reader told only "no"
58
+ * reaches for the workaround without knowing what else it would have to change.
42
59
  * - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
43
60
  * caller — least of all by the model, which has no shell to run anything in.
44
61
  * So its advice says so and stops: the remedy is a user-side preset choice,
@@ -93,7 +110,7 @@
93
110
  import type { ProvisioningFailure, RecognizedFailure } from './signature.js';
94
111
  import type { SandboxModeName } from './mode.js';
95
112
  /** The upstream threads the ACL advisory is a stopgap for. */
96
- export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232";
113
+ export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275";
97
114
  /** The upstream thread the persistent-shell advisory is a stopgap for. */
98
115
  export declare const PTY_DISCUSSIONS = "#7638";
99
116
  /** The upstream threads the native-init-death advisory is a stopgap for. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@argszero/cordis-plugin-sandbox-grant-advisor",
3
- "description": "Turns three sandbox environment failures that name neither their cause nor a remedy into a diagnosis with a path forward. Family 1, the Windows workspace ACL: ten reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816, #8232) of one signature \u2014 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. Family 2, the persistent shell (#7638): 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. 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 \u2014 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 \u2014 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) \u2014 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 three 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; both mode-gated families advised only under a mode the policy resolver reports as confining), 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 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 \u2014 the owner's implicit WRITE_DAC already covers the DACL half \u2014 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% \u2014 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, states the resolved mode, tells the model to stop rather than retry, and hands the user-side preset choice over \u2014 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 \u2014 naming both measured shapes of the desktop host rather than asserting the one that was measured first \u2014 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 \u2014 for the Electron host \u2014 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 \u2014 it never claims the sandbox caused the failure and never offers a widened mode as a fix. 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.8.1",
3
+ "description": "Turns three sandbox environment failures that name neither their cause nor a remedy into a diagnosis with a path forward. Family 1, the Windows workspace ACL: twelve reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816, #8232, #8272, #8275) of one signature \u2014 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. Family 2, the persistent shell (#7638): 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. 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 \u2014 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 \u2014 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) \u2014 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 three 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; both mode-gated families advised only under a mode the policy resolver reports as confining), 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 \u2014 the owner's implicit WRITE_DAC already covers the DACL half \u2014 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% \u2014 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, states the resolved mode, tells the model to stop rather than retry, and hands the user-side preset choice over \u2014 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 \u2014 naming both measured shapes of the desktop host rather than asserting the one that was measured first \u2014 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 \u2014 for the Electron host \u2014 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 \u2014 it never claims the sandbox caused the failure and never offers a widened mode as a fix. 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.9.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",