@argszero/cordis-plugin-sandbox-grant-advisor 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  * `sandbox-grant-advisor`: turn an environment failure that has no path forward
3
3
  * into a diagnosis the model — and the user reading the transcript — can act on.
4
4
  *
5
- * ## The three failures it recognizes
5
+ * ## The four failures it recognizes
6
6
  *
7
7
  * **Workspace provisioning (Windows ACL).** Four reports of one signature
8
8
  * (`#7538`, `#7622`, `#7646`, `#7720`) describe the same shape: the host-side write grant
@@ -68,6 +68,26 @@
68
68
  * tool's own canonical output, never a line of rendered text — and which three
69
69
  * narrowings keep the recognition from firing on something else.
70
70
  *
71
+ * **Denied inside the workspace (Windows ACL, `#423`).** The fourth family is
72
+ * the other half of the backend the first one explains, and it is the first
73
+ * whose remedy is withheld on purpose. There the grant could not be applied at
74
+ * all; here it *was* applied — on the workspace root, once — and Windows ACE
75
+ * inheritance silently skipped objects whose DACL the caller could not write.
76
+ * The backend's provisioning check short-circuits on the root (`hasExactGrant`
77
+ * returns early once the root carries the ACE, `sandbox-windows-acl/src/acl.ts`),
78
+ * so those descendants are never revisited and the same command fails forever,
79
+ * while a directory the harness itself created works. It arrives at the same
80
+ * seam as the native-init family and for the same structural reason: a denied
81
+ * command exits nonzero and the shipped shell tools report that as a finished
82
+ * run, so the fact is in `ToolExecutionSuccess.value` rather than in an error.
83
+ * Unlike that family it needs no bespoke code — the executors stamp the denial
84
+ * onto the value as a structured triple (`sandbox: { mode, denied, enforcement? }`,
85
+ * produced by matching the backend's own refusal dialect in the captured
86
+ * stderr), so this plugin reads the harness's own reading of its own sandbox.
87
+ * See `src/signature.ts` for the facts that narrow it, and in particular for
88
+ * why a denial is the *designed* outcome in three other situations (outside the
89
+ * workspace, under `read-only`, a runner failure) and must never be advised.
90
+ *
71
91
  * ## Where it acts, and why there
72
92
  *
73
93
  * One listener on the public `tools/post-execute` waterfall
@@ -107,9 +127,15 @@
107
127
  * packaged desktop binary, which the plugin **measures and reports** rather
108
128
  * than assumes), and carries the one conversion a model can actually make —
109
129
  * rewrite the work as PowerShell or `cmd` when the program that could not
110
- * start was an MSYS2 one. All three ride `additionalContexts`, so the model
111
- * sees the diagnosis beside the failure rather than only in a log it never
112
- * reads.
130
+ * start was an MSYS2 one. For the workspace-denial family it prints the path
131
+ * it keyed on beside the root it tested it against, says that retrying is
132
+ * provably useless (the root-only provisioning check never revisits the
133
+ * object), gives the two-sided reachability rule, names the label variant of
134
+ * the same shape, and prints **no** repair command — the obvious one is
135
+ * refused by Windows with `ERROR_NONE_MAPPED (1332)`, and this project has no
136
+ * Windows host to verify a line on. All four ride `additionalContexts`, so the
137
+ * model sees the diagnosis beside the failure rather than only in a log it
138
+ * never reads.
113
139
  * 2. **A bounded fail-fast, ACL family only.** With `enforceAfter` set, a call
114
140
  * this plugin has *watched fail* this way is refused at `tools/pre-execute`
115
141
  * once the environment has failed at least that many times. It is off by
@@ -121,8 +147,13 @@
121
147
  * only sent when the resolved mode actually confines; if the mode is not
122
148
  * confining, or cannot be
123
149
  * resolved at all, the failure is left exactly as it was **and the host log
124
- * says so once**. Silence alone would make "the sandbox is not the cause" and
125
- * "this plugin could not tell" indistinguishable from the outside.
150
+ * says so once**. The workspace-denial advisory is withheld the same way when
151
+ * the workspace root — the fact the containment claim is tested against —
152
+ * cannot be resolved. Silence alone would make "the sandbox is not the cause" and
153
+ * "this plugin could not tell" indistinguishable from the outside. A denial
154
+ * the classifier *can* place outside the workspace is a different case and is
155
+ * left silent on purpose: that is the sanctioned escalation path, not a
156
+ * puzzle, and a note about it would be noise.
126
157
  *
127
158
  * ## Honest boundaries
128
159
  *
@@ -139,7 +170,10 @@
139
170
  * tools' own foreground projection with the reported codes, including the
140
171
  * signed form the reporter saw (`-1073741502`) and the real MSYS2 stderr, so the
141
172
  * recognition runs against the producer's data rather than against a message
142
- * this plugin invented.
173
+ * this plugin invented. The workspace-denial family is exercised the same way:
174
+ * the test builds the executors' own `sandbox` stamp and the shipped foreground
175
+ * projection, supplies `win32` as the platform fact, and covers both the
176
+ * recognized case and each of the narrowings that must stay silent.
143
177
  * - **It does not repair anything.** No ACL is written, no privilege is
144
178
  * requested, nothing is elevated, no environment variable is set for another
145
179
  * process, no preset is installed and no mode is changed: the remedies are the
@@ -148,7 +182,7 @@
148
182
  * guard keys on *call identity* (identical arguments retried); this one keys
149
183
  * on the *environment signature*, which is how several different commands can
150
184
  * share one cause. They can be mounted together.
151
- * - **The real fix is upstream**, in all three families: the ACL failure should
185
+ * - **The real fix is upstream**, in all four families: the ACL failure should
152
186
  * name
153
187
  * the outstanding condition at the site that knows it (`grantWrite` computes
154
188
  * `hasExactGrant`/`hasExactDeny`/`hasExactLabel` and discards which was
@@ -156,7 +190,12 @@
156
190
  * incompatible with the PTY backend" or fall back to a one-shot shell, and the
157
191
  * sandbox runner should be launched with the environment its own execution
158
192
  * needs (`ELECTRON_RUN_AS_NODE` when argv[0] is an Electron binary) or with a
159
- * documented, checkable refusal for MSYS2 programs. This plugin is the stopgap.
193
+ * documented, checkable refusal for MSYS2 programs. The workspace-denial family
194
+ * is the same shape once more: `grantWrite` returns early when the *root*
195
+ * already carries the ACE, so the descendants that missed the propagation are
196
+ * never repaired — the check would have to look past the root, or the denial
197
+ * surface would have to say *which* path was refused instead of only that one
198
+ * was. This plugin is the stopgap.
160
199
  *
161
200
  * @module @argszero/cordis-plugin-sandbox-grant-advisor
162
201
  */
@@ -19,6 +19,13 @@
19
19
  * session — so what is quoted in the advisory is the policy that actually
20
20
  * governed the failing call, not a guess reconstructed from configuration.
21
21
  *
22
+ * The same lookup answers the **workspace root** for the fourth family
23
+ * (`workspace-denial`, #423), whose claim is that the path a denied command
24
+ * named lies inside the session's own workspace. Same request, same fail-closed
25
+ * posture, separate function ({@link resolveWorkspaceRoot}), so that neither
26
+ * family can fail on a field it never reads and each can name the failure in its
27
+ * own words.
28
+ *
22
29
  * ## Why this is a guarded lookup instead of an import
23
30
  *
24
31
  * `@deepseek-ai/dsh-sandbox-policy` is **optional** in this plugin's world: a
@@ -73,6 +80,18 @@ export type ModeResolution =
73
80
  readonly ok: false;
74
81
  readonly withheld: string;
75
82
  };
83
+ /** The outcome of resolving one agent's workspace root. */
84
+ export type RootResolution =
85
+ /** The workspace root the enforcing providers were handed. */
86
+ {
87
+ readonly ok: true;
88
+ readonly workspaceRoot: string;
89
+ }
90
+ /** No answer was available, and why — the host side says so out loud. */
91
+ | {
92
+ readonly ok: false;
93
+ readonly withheld: string;
94
+ };
76
95
  /**
77
96
  * Resolve the effective sandbox mode for one agent's call.
78
97
  *
@@ -85,3 +104,18 @@ export type ModeResolution =
85
104
  * @returns the mode, or the reason it could not be resolved.
86
105
  */
87
106
  export declare function resolveSandboxMode(ctx: Context, agent: Agent): ModeResolution;
107
+ /**
108
+ * The workspace root the policy resolver reports for one agent's call.
109
+ *
110
+ * The same lookup, the same request and the same fail-closed posture as
111
+ * {@link resolveSandboxMode}, and for the same reason: the root is what the
112
+ * enforcing providers were handed, so it is the value the workspace-denial
113
+ * family must test containment against rather than one reconstructed from
114
+ * configuration. The two are separate functions rather than one that returns
115
+ * both because each family needs one of them, and a family that needs only the
116
+ * mode must not be able to fail on a missing root (nor the other way round).
117
+ * @param ctx - the plugin's context.
118
+ * @param agent - the agent whose call failed.
119
+ * @returns the root, or the reason it could not be resolved.
120
+ */
121
+ export declare function resolveWorkspaceRoot(ctx: Context, agent: Agent): RootResolution;
@@ -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. 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.11.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",