@argszero/cordis-plugin-sandbox-grant-advisor 0.10.0 → 0.12.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Recognize the three environment failures this plugin explains, and refuse
2
+ * Recognize the four environment failures this plugin explains, and refuse
3
3
  * everything else.
4
4
  *
5
5
  * ## The ACL provisioning failure (`acl-provisioning`)
@@ -147,6 +147,82 @@
147
147
  * observe and would make this family untestable on the host this plugin is
148
148
  * built on — which is how a family ships without ever having been run.
149
149
  *
150
+ * ## Denied inside the workspace (`workspace-denial`)
151
+ *
152
+ * The fourth family is the other half of the same backend the first one
153
+ * explains. There the grant could not be applied at all and every command died
154
+ * before it ran; here the grant *was* applied and the workspace looks
155
+ * provisioned — and part of the tree still refuses writes. `#423` is the
156
+ * report: under `workspace-write` on Windows, a command writing into a
157
+ * subdirectory that was created or moved in from outside the session is denied,
158
+ * forever, while the same command against a directory the harness itself
159
+ * created succeeds.
160
+ *
161
+ * Like the native-init family, and for the same structural reason, this one is
162
+ * **invisible from the error path**: a denied command exits nonzero, and the
163
+ * shipped shell tools report a nonzero exit as a finished run rather than as
164
+ * `isError`, so the fact arrives in `ToolExecutionSuccess.value`. Unlike that
165
+ * family it does not need a bespoke code to be read — the shipped executors
166
+ * *stamp the denial* onto the value, as a structured triple no line of rendered
167
+ * text can fabricate:
168
+ *
169
+ * `sandbox: { mode, denied: true, enforcement? }`
170
+ *
171
+ * (`packages/shell/bash-sandbox/src/index.ts` and `pwsh-sandbox`, both on
172
+ * `classifyDenial`; projected into the tool result value by `tool-bash` /
173
+ * `tool-pwsh`). `denied` is produced by matching the backend's own refusal
174
+ * dialect in the *captured stderr* — `'access is denied'`, `'access to the
175
+ * path'`, `'permission denied'`, `'operation not permitted'` for the
176
+ * `windows-acl` backend — so the value is the harness's reading of its own
177
+ * sandbox, not this plugin's reading of a message.
178
+ *
179
+ * **`denied` alone is not this family, and the narrowings are the whole
180
+ * design.** A denial is the *sanctioned* outcome in three other situations, and
181
+ * advising about a missing inherited grant in any of them would be the
182
+ * confidently wrong cause this module exists to avoid:
183
+ *
184
+ * - **Outside the workspace.** `fs-sandbox` throws its marker only when a path
185
+ * falls outside the writable roots, and a confined shell denying a path
186
+ * outside the workspace is the designed escalation path — the denial surface
187
+ * offers one retry under a wider mode, and that offer is correct there.
188
+ * - **Under `read-only`.** That mode denies *every* write by construction
189
+ * (`fs-sandbox`'s `checkedTarget`: `read-only` throws before any containment
190
+ * question is asked), so an in-workspace denial under it is the mode working.
191
+ * - **A runner failure.** The executor refuses to call a run denied when the
192
+ * runner itself failed (`denied: !runnerFailed && …`), and a value that
193
+ * carries `runnerFailed: true` is left alone for the same reason.
194
+ *
195
+ * What is left is exactly the anomaly: **a denial under `workspace-write` of a
196
+ * path inside the session's own workspace**, which is the one combination the
197
+ * harness is supposed to make impossible. The mode comes off the value (the
198
+ * executor stamped the mode it actually ran under, so no policy lookup can
199
+ * disagree with it), and the containment test needs the workspace root, which
200
+ * comes from the policy resolver at the call site —
201
+ * {@link classifyWorkspaceDenial} takes it as a fact rather than reading it, so
202
+ * the recognition stays a pure function of its inputs.
203
+ *
204
+ * **The in-workspace half of that test is keyed on the command's own text**, and
205
+ * deliberately not on the stderr the denial was inferred from: the regex looks
206
+ * for drive-qualified or UNC path literals in the call's `command` argument, and
207
+ * the family is recognized only when **every** such literal it finds lies under
208
+ * the workspace root. A command that names an outside path as well — an
209
+ * interpreter shipped under `C:\Program Files`, an output directory on another
210
+ * volume — is refused rather than guessed at, and so is a command that names
211
+ * only relative paths: in both cases the plugin cannot say *which* path was
212
+ * denied, and silence is the fail-closed direction. The paths it did key on are
213
+ * carried into the advisory, so the reader can see the reasoning rather than
214
+ * take it on faith.
215
+ *
216
+ * **The platform gate is real here, and unlike the native-init family it cannot
217
+ * be dropped.** The mechanism is Windows ACE inheritance: the backend writes the
218
+ * grant once, on the workspace root, and relies on the operating system to
219
+ * propagate it to descendants, which needs `WRITE_DAC` on each descendant at
220
+ * that moment. Every other backend applies its policy per call to the process
221
+ * (landlock rules, a bwrap profile, a seatbelt profile) and has no descendant to
222
+ * miss, so the same value on those hosts is a different story. `win32` is
223
+ * therefore part of recognition, passed in rather than read, and the suite runs
224
+ * this family with the platform fact supplied.
225
+ *
150
226
  * @module
151
227
  */
152
228
  /** Which provisioning operation failed, and which diagnosis follows from it. */
@@ -174,7 +250,9 @@ export type FailureFamily =
174
250
  /** The persistent PTY shell could not start under a confining sandbox mode. */
175
251
  | 'pty-startup'
176
252
  /** A confined Windows child died while its native images were initializing. */
177
- | 'native-init';
253
+ | 'native-init'
254
+ /** A confined command was denied a path inside its own workspace (Windows ACL). */
255
+ | 'workspace-denial';
178
256
  /** One recognized provisioning failure, with the producer's own fields kept. */
179
257
  export interface ProvisioningFailure {
180
258
  /** Which family this failure belongs to. */
@@ -233,8 +311,53 @@ export interface NativeInitFailure {
233
311
  /** The same code normalized to its unsigned 32-bit form, for comparison and printing. */
234
312
  readonly exitCode: number;
235
313
  }
314
+ /**
315
+ * The mode in which a write **inside** the workspace is supposed to succeed.
316
+ *
317
+ * It is the only mode this family is recognized under, and that is a fact about
318
+ * the other two rather than about this one: `read-only` denies every write by
319
+ * construction, and `danger-full-access` spawns nothing through the sandbox at
320
+ * all — so a denial under either is the mode doing its job, and the one place a
321
+ * denial is an anomaly is here.
322
+ */
323
+ export declare const DENIAL_MODE = "workspace-write";
324
+ /**
325
+ * The facts the workspace-denial recognition needs beyond the result value.
326
+ *
327
+ * Both are passed in rather than read here, for the reason the other families
328
+ * state in their own words: a gate that trusts the shape of its input is the
329
+ * gate that reports a cause from the wrong world. The type is also what keeps
330
+ * the platform fact honest — the Android/Cygwin-style "`process.platform` is
331
+ * win32" belief cannot be smuggled in as a default.
332
+ */
333
+ export interface WorkspaceDenialFacts {
334
+ /** The host's `process.platform`, as the caller read it. */
335
+ readonly platform: string;
336
+ /** The session's workspace root, as the policy resolver reported it. */
337
+ readonly workspaceRoot: string;
338
+ }
339
+ /**
340
+ * A confined command that was denied a path inside its own workspace.
341
+ *
342
+ * The producer fields are kept verbatim rather than paraphrased, so the advisory
343
+ * can show the reader the exact facts the recognition keyed on — which matters
344
+ * more here than anywhere else in this module, because the family's claim
345
+ * ("this path is inside your workspace") is a statement about two strings.
346
+ */
347
+ export interface WorkspaceDenialFailure {
348
+ /** Which family this failure belongs to. */
349
+ readonly family: 'workspace-denial';
350
+ /** The mode the denied call ran under, as the executor stamped it on the value. */
351
+ readonly mode: string;
352
+ /** The command's exit status, or `null` when the tool reported none. */
353
+ readonly exitCode: number | null;
354
+ /** The in-workspace absolute paths the call named, in the order they appear. */
355
+ readonly paths: readonly string[];
356
+ /** The workspace root those paths were tested against, as it was given. */
357
+ readonly workspaceRoot: string;
358
+ }
236
359
  /** Any failure this plugin recognizes, tagged by family. */
237
- export type RecognizedFailure = ProvisioningFailure | PtyStartupFailure | NativeInitFailure;
360
+ export type RecognizedFailure = ProvisioningFailure | PtyStartupFailure | NativeInitFailure | WorkspaceDenialFailure;
238
361
  /**
239
362
  * Classify one failure message against the ACL family.
240
363
  * @param message - the failure text, from the result's `error.message` or its rendered content.
@@ -283,3 +406,66 @@ export declare function classifyNativeInitDeath(value: unknown): NativeInitFailu
283
406
  * @returns the message text the producing layer would have produced.
284
407
  */
285
408
  export declare function failureLine(failure: RecognizedFailure): string;
409
+ /**
410
+ * The drive-qualified and UNC path literals in one command line.
411
+ *
412
+ * Deliberately crude, in the direction that keeps the diagnosis honest. A
413
+ * literal is taken to end at the first character a shell token cannot carry
414
+ * unquoted, so a path containing a space is recovered as its first segment — a
415
+ * **prefix** of the real path, which preserves the containment answer for every
416
+ * path that lies under the root (a prefix of a descendant is still a descendant)
417
+ * and refuses the test for paths whose root itself contains a space. A path that
418
+ * appears twice is returned once.
419
+ * @param command - the `command` argument of the failing shell call.
420
+ * @returns the literals found, in order, without trailing separators.
421
+ */
422
+ export declare function windowsPathsIn(command: string): string[];
423
+ /**
424
+ * Whether one path lies at or under one root, the way Windows compares them.
425
+ * @param root - the workspace root.
426
+ * @param path - the candidate path.
427
+ * @returns true when `path` is `root` itself or a descendant of it.
428
+ */
429
+ export declare function isInsideWorkspace(root: string, path: string): boolean;
430
+ /**
431
+ * Whether one settled value carries the executors' workspace-denial stamp.
432
+ *
433
+ * The **structural half** of the fourth family's recognition, split out so that
434
+ * "this is not a denial at all" (silent, and the overwhelming majority of
435
+ * successful calls) stays distinguishable from "this is a denial and the plugin
436
+ * could not finish placing it" — the case the disclosure rule requires the host
437
+ * log to account for. It reads the value the executors wrote, never a line of
438
+ * rendered text, and it is deliberately platform-blind: the platform gate is a
439
+ * fact the classifier takes as an argument, and a stamp test that baked it in
440
+ * could not be used to decide whether a missing platform fact is worth saying
441
+ * out loud.
442
+ *
443
+ * What it does **not** decide is which path was denied — that needs the call's
444
+ * arguments and the workspace root, and belongs to
445
+ * {@link classifyWorkspaceDenial}. A stamp alone is the sanctioned outcome in
446
+ * three other situations (outside the workspace, under `read-only`, a runner
447
+ * failure); this function excludes the third by itself and leaves the first two
448
+ * to the classifier, which is the only place they can be separated.
449
+ * @param value - a settled execution's canonical value.
450
+ * @returns true when the value is a foreground shell projection stamped
451
+ * `denied: true` under `workspace-write` with no runner failure.
452
+ */
453
+ export declare function hasWorkspaceDenialStamp(value: unknown): boolean;
454
+ /**
455
+ * Classify one **successful** execution's canonical value as a workspace-internal
456
+ * denial, and recover the paths the call named.
457
+ *
458
+ * Four facts, in the order they narrow: the call ran on Windows (the mechanism
459
+ * is ACE inheritance, which no other backend has); the value carries the
460
+ * executor's denial stamp ({@link hasWorkspaceDenialStamp}); the call's
461
+ * `command` argument carries at least one absolute Windows path literal; and
462
+ * **all** of them lie under the workspace root. The last is the one that
463
+ * separates this family from the sanctioned escalation path — a command naming
464
+ * any path outside its own workspace is refused rather than guessed at, because
465
+ * the plugin cannot say which path the sandbox refused.
466
+ * @param value - the settled execution's canonical value (`ToolExecutionSuccess.value`).
467
+ * @param args - the settled call's parsed arguments (`ToolExecution.arguments`).
468
+ * @param facts - the host platform and the session's workspace root.
469
+ * @returns the recognized failure, or undefined when this is not one.
470
+ */
471
+ export declare function classifyWorkspaceDenial(value: unknown, args: unknown, facts: WorkspaceDenialFacts): WorkspaceDenialFailure | undefined;
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: 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. 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 \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 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) \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.10.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 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.12.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",