@argszero/cordis-plugin-sandbox-grant-advisor 0.5.0 → 0.6.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
@@ -1,27 +1,32 @@
1
1
  # @argszero/cordis-plugin-sandbox-grant-advisor
2
2
 
3
3
  Turns a sandbox environment failure that has **no path forward** into a
4
- diagnosis the model — and the user reading the transcript — can act on. Two
4
+ diagnosis the model — and the user reading the transcript — can act on. Three
5
5
  signatures, one mechanism:
6
6
 
7
7
  ```
8
8
  SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\ws) # Windows workspace ACL
9
9
  PTY shell exited during startup # persistent shell × confining mode
10
+ [exit code: -1073741502] (0xC0000142) # a confined child that never started
10
11
  ```
11
12
 
12
13
  **This plugin is the stopgap for "the error does not name the outstanding
13
14
  condition".** It repairs nothing: no ACL is written, no privilege is requested,
14
- nothing is elevated, no preset is installed and no mode is changed.
15
+ nothing is elevated, no environment variable is set for another process, no
16
+ preset is installed and no mode is changed.
15
17
 
16
- ## The two failures it recognizes
18
+ ## The three failures it recognizes
17
19
 
18
- Both are recognized on the public **`tools/post-execute`** waterfall
19
- (`@deepseek-ai/dsh-tools`) — the one seam that has all three of: the failure text
20
+ The first two are recognized on the public **`tools/post-execute`** waterfall
21
+ (`@deepseek-ai/dsh-tools`) from the failure text. That seam is the one that has
22
+ all three of what a diagnosis needs: the failure
20
23
  (providers propagate their error unchanged and the tool pipeline settles it as an
21
24
  `isError` result), an agent identity to attribute it to (`exec.agent`), and a
22
25
  channel that speaks to the model in the same step (`PostToolDecision`'s
23
26
  `additionalContexts`, which the agent loop turns into a durable user-role message
24
- — `packages/core/agent-loop/src/tool-calls.ts`).
27
+ — `packages/core/agent-loop/src/tool-calls.ts`). The third is recognized at the
28
+ **same seam** from the canonical value of a result the pipeline calls a
29
+ *success*, for a reason §3 gives in full.
25
30
 
26
31
  That seam, not `ctx.sandbox.confine`: `confine(argv, policy, signal)` sees the
27
32
  confinement failure too, but its signature carries no agent, so a wrapper could
@@ -229,15 +234,111 @@ reads it any more (`@deepseek-ai/dsh-agent-preset-registry`: the registry
229
234
  by row id (`preset-minimal`) for a change to a shipped one. A test arm asserts
230
235
  the advisory never names the dead directory.
231
236
 
237
+ ### 3. A confined child that never started (`native-init`)
238
+
239
+ Two reports of one exit code: [`#7876`] (the packaged desktop app) and [`#7877`]
240
+ (MSYS2 / Git Bash). Both are `0xC0000142` `STATUS_DLL_INIT_FAILED` — the Windows
241
+ loader terminated the process while it was initializing its native images, i.e.
242
+ **before the program's entry point**. A command that ran and then failed exits
243
+ with its own status and prints its own output; this one produced neither.
244
+
245
+ | what was run | what came out | exit code |
246
+ | --- | --- | --- |
247
+ | `cmd.exe /c "echo cmd-ok"` | `cmd-ok` | 0 |
248
+ | `pwsh -NoLogo -NoProfile -Command "Write-Output pwsh-ok"` | `pwsh-ok` | 0 |
249
+ | `D:\Git\bin\bash.exe -c "echo bash-ok"` | `couldn't create signal pipe, Win32 error 5` | `-1073741502` |
250
+
251
+ Two producers have been measured under a confining mode:
252
+
253
+ 1. **An MSYS2 / Git-Bash program** ([`#7877`]). The restricted token's runtime
254
+ cannot create the pipe it uses for signals, so bash aborts in the loader
255
+ phase, while `cmd.exe` and `pwsh` run fine in the same workspace under the
256
+ same mode. The plugin's own composition has no way around this: `tool-bash`
257
+ and `bash-sandbox` are `disabled` on win32
258
+ (`@deepseek-ai/dsh-base/cordis.patch.yml`), so the combination is likely
259
+ never covered upstream. The **one conversion a model can make itself** is to
260
+ write the same work as a PowerShell or `cmd` command.
261
+ 2. **The packaged desktop app's sandbox runner** ([`#7876`]).
262
+ `dsh-sandbox-local` launches the runner as `[process.execPath, entry]`, and
263
+ in the packaged build `process.execPath` is the Electron executable, which
264
+ starts as an *application* unless the child's environment carries
265
+ `ELECTRON_RUN_AS_NODE=1` — so the runner never runs and every confined command
266
+ reports this code with **no output at all**. The unpacked node host
267
+ (`node apps/cli/lib/bin.js web`) is unaffected. The plugin reports whether
268
+ *this* process is an Electron binary (`process.versions.electron`) as a
269
+ measured fact rather than assuming it, because that is the check the
270
+ discriminator turns on.
271
+
272
+ **Why this family is read from a successful result.** The producer never marks
273
+ it an error, and that is a fact about upstream rather than a choice here:
274
+ `RUNNER_FAILURE_RULES['windows-acl']` admits exactly one code —
275
+ `[{ allowedExitCodes: [127], fatalSignatures: ['windows-acl-run: '] }]`
276
+ (`packages/sandbox/sandbox-local/src/index.ts`) — and `classifyRunnerFailure`
277
+ skips any other code before it looks at stderr
278
+ (`packages/sandbox/sandbox/src/diagnostics.ts`), so `0xC0000142` is never a
279
+ runner failure and `SandboxUnavailableError` is never thrown. The renderer then
280
+ reports it the way it reports any finished command — *"Non-zero exits are
281
+ reported, not errored … only infrastructure failures (spawn errors, aborts)
282
+ surface as isError results"* (`packages/shell/tool-pwsh/src/render.ts`) — as
283
+ `[exit code: …]`. **Every version of this plugin before 0.6.0 read error results
284
+ only and was structurally blind to it**, which is exactly why the model retries a
285
+ command that can never start.
286
+
287
+ The read is `ToolExecutionSuccess.value` — the tool's own canonical output,
288
+ documented as *"Execution-local canonical value; deliberately omitted from
289
+ durable events"* **and not carried on failure results at all** — so the code
290
+ arrives structurally rather than as a line of text. A command that prints
291
+ `[exit code: -1073741502]` is not this failure, and neither is a value some other
292
+ tool happens to build with an `exitCode` field: the classifier requires the
293
+ shipped shell projection (`kind: 'foreground'` plus an integer `exitCode`).
294
+
295
+ The advisory that follows:
296
+
297
+ ```
298
+ Sandboxed command never started — the process died while its native libraries were loading.
299
+
300
+ What was reported:
301
+ [exit code: -1073741502] (0xC0000142 STATUS_DLL_INIT_FAILED)
302
+ The call ran under sandbox mode `workspace-write`, where the harness starts every command through its
303
+ restricted-token runner.
304
+
305
+ 0xC0000142 is STATUS_DLL_INIT_FAILED: ... this is BEFORE the program's entry point. ...
306
+ Nothing in the code says "sandbox" by itself; what makes the sandbox a candidate is the mode above ...
307
+
308
+ Two producers have been measured under a confining Windows mode. Check which one this is:
309
+ 1. An MSYS2 / Git-Bash program ... (#7877)
310
+ If that is what could not start: write the same work as a PowerShell or `cmd` command instead
311
+ 2. The packaged desktop application's sandbox runner ... (#7876)
312
+ This process is NOT an Electron binary (`process.versions.electron` is unset), so that producer does not apply here.
313
+
314
+ Do not retry this call: the environment has not changed, and the identical call produces the identical code.
315
+
316
+ Honest boundary — 0xC0000142 has producers this list does not have: a program that cannot load one of
317
+ its own DLLs dies this way too, and the sandbox backend's own source records that a child started with
318
+ a hidden console window does as well ... This is not a claim that the sandbox caused the failure.
319
+ ```
320
+
321
+ **What it does not claim.** The code is a loader status, and the loader reports
322
+ the same status for causes that have nothing to do with the sandbox (a missing
323
+ DLL, a program's own initialization failure, the hidden-console child the
324
+ backend's own source avoids `CREATE_NO_WINDOW` for). So the advisory diagnoses
325
+ the **class** ("the process never reached its entry point") and enumerates the
326
+ producers measured under a confining mode, each with the check that separates
327
+ them — one of which the reader answers (what program failed to start) and one of
328
+ which the plugin answers (is this host the packaged desktop binary). It never
329
+ offers `danger-full-access` as a fix and never suggests a sandbox setting be
330
+ relaxed.
331
+
232
332
  ## What it does with a recognized failure
233
333
 
234
- 1. **One durable advisory per agent, per family.** An agent that hits both
334
+ 1. **One durable advisory per agent, per family.** An agent that hits two
235
335
  families is told about **both**, once each. The notice carries its own
236
336
  producer-owned `source.kind` (`sandbox-grant-advisor`) — not the retired
237
337
  `plugin` wrapper, which the current session format refuses — and a bounded
238
338
  one-line `summary` for the transcript row. The host log gets one matching
239
339
  `warn` line, so the fact survives outside the transcript too.
240
- 2. **A disclosure when it withholds.** The PTY advisory is only sent when the
340
+ 2. **A disclosure when it withholds.** The PTY and native-init advisories are
341
+ only sent when the
241
342
  resolved mode actually confines. If the mode is `danger-full-access`, or
242
343
  cannot be resolved at all (no `sandboxPolicy` service mounted, no agent
243
344
  session, a resolver that throws), the failure is left exactly as it was
@@ -256,14 +357,18 @@ the advisory never names the dead directory.
256
357
  refused twice — while a session can always make progress by spending the
257
358
  budget it has.
258
359
 
259
- **Why the blocking half does not extend to the PTY family** (it is
260
- ACL-only by construction, in the parameter type): the ACL remedy is a command
360
+ **Why the blocking half does not extend to the two mode-gated families** (it
361
+ is ACL-only by construction, in the parameter type): the ACL remedy is a
362
+ command
261
363
  the user can run *while the session continues*, so refusing further identical
262
364
  calls cannot make the session unfinishable — spending the budget always lets
263
365
  the call through, and a repaired environment is discovered by exactly that.
264
- The PTY remedy is a preset swap, which happens **between** sessions;
265
- refusing calls there could only pad a session that is already unable to do
266
- the thing being refused.
366
+ The PTY remedy is a preset swap, which happens **between** sessions, and the
367
+ native-init remedy is a launch fix on the user's side — whose one in-session
368
+ part, rewriting an MSYS2 command, the model does by calling a *different*
369
+ command, which has a different call key and is therefore never the call being
370
+ refused. Refusing calls in those families could only pad a session that is
371
+ already unable to do the thing being refused.
267
372
 
268
373
  ## Install
269
374
 
@@ -328,6 +433,15 @@ than one that stays silent.
328
433
  - **A successful command whose *output* contains the line is not a failure.**
329
434
  The gate is the result's error state, not the presence of the text — reading a
330
435
  log file that quotes the error must not trigger advice.
436
+ - **Only `STATUS_DLL_INIT_FAILED` is classified, and only from the canonical
437
+ value.** `0xC0000409` is the Cygwin/MSYS2 runtime's deliberate fast-fail (a
438
+ different mechanism with a different story) and `0xC0000135` is a missing DLL
439
+ (a packaging problem, not a sandbox one); both are refused, as is exit `127`,
440
+ which upstream's runner-failure rule already owns. And because the code is
441
+ compared as a number in `ToolExecutionSuccess.value`, a command printing
442
+ `[exit code: -1073741502]`, or any value that is not the shipped shell
443
+ projection (`kind: 'foreground'`), is not this family — a line of text can
444
+ never be mistaken for a loader status.
331
445
  - **Only one advisory per agent, per family.** The environment is explained
332
446
  once; repeating it per failed command would be noise competing with the
333
447
  failure itself.
@@ -336,7 +450,9 @@ than one that stays silent.
336
450
 
337
451
  - **The Windows path itself cannot be witnessed on macOS**, where this plugin
338
452
  was built. What the test suite proves is the decision layer — classification
339
- of both families, the once-per-agent-per-family rule, the sandbox-mode gate
453
+ of all three families (the third from the producer's own canonical value,
454
+ built by the suite with the reported `-1073741502` and the report's stderr
455
+ line), the once-per-agent-per-family rule, the sandbox-mode gate
340
456
  and its fail-closed behaviour, the fail-fast budget and its self-feeding
341
457
  guard, and the wiring to a real cordis `Context` and the real `ToolRuntime` —
342
458
  driven by fixtures that throw the producers' exact error shapes
@@ -345,7 +461,8 @@ than one that stays silent.
345
461
  does **not** prove that `icacls ... :(OI)(CI)F` fixes a given machine, nor that
346
462
  a given Windows host reproduces the PTY startup failure; those are the user's
347
463
  one-line experiment and the reporter's own control, and both advisories say
348
- where they stop.
464
+ where they stop. The native-init family is the one that needs no Windows to be
465
+ faithful, because what it reads is a number inside a JSON value.
349
466
  - **It repairs nothing and elevates nothing.** If the directory really is
350
467
  Full-control for the caller, the remaining ACL hypothesis is
351
468
  `SeSecurityPrivilege` — i.e. the backend's documented prerequisite would be
@@ -365,13 +482,16 @@ than one that stays silent.
365
482
  outside the seam this plugin subscribes to. The report and its proposed fix
366
483
  stay with the maintainers; all this plugin can do is explain the provisioning
367
484
  failure that shares its root.
368
- - **The real fix is upstream, in both families.** For the ACL failure,
485
+ - **The real fix is upstream, in all three families.** For the ACL failure,
369
486
  `grantWrite` already computes `hasExactGrant` / `hasExactDeny` /
370
487
  `hasExactLabel` and discards which one was false, so the diagnostic that turns
371
488
  a 52-minute detour into one line belongs at that site. For the PTY failure,
372
489
  the startup path should either report "this sandbox mode is incompatible with
373
- the PTY backend" or fall back to a one-shot shell. This plugin is the stopgap
374
- for both.
490
+ the PTY backend" or fall back to a one-shot shell. For the native-init death,
491
+ the runner should be launched with the environment its own execution needs
492
+ (`ELECTRON_RUN_AS_NODE=1` when `argv[0]` is an Electron binary — [`#7876`]'s
493
+ three candidate fixes) or refuse, in a checkable way, an MSYS2 program under a
494
+ restricted token. This plugin is the stopgap for all three.
375
495
 
376
496
  ## Compatibility
377
497
 
@@ -404,11 +524,16 @@ the newest of that line.
404
524
 
405
525
  The whole set is re-probed whenever this package's source changes rather than
406
526
  carried over from an earlier version: the range is a claim about *this* build of
407
- the plugin, so `0.4.0` re-ran all five lines above. A line whose probe fails is
527
+ the plugin, so `0.6.0` re-ran all five lines above. A line whose probe fails is
408
528
  removed from the range rather than left claimed. The scratch tree's resolved
409
529
  versions are the ones to read back when a probe is quoted as evidence — the probe
410
530
  script pins them by exact version, and `--keep` leaves the tree in place to check.
411
531
 
532
+ The mode lookup stays guarded through this version too: the native-init family
533
+ needs the same resolved mode as the PTY family, and it takes it from the same
534
+ `ctx.get('sandboxPolicy')` capability guard, so a composition without the service
535
+ degrades to a disclosed silence rather than failing to load.
536
+
412
537
  ## Development
413
538
 
414
539
  ```sh
@@ -425,8 +550,9 @@ refuses a call that would have worked, is worse than one that stays silent.
425
550
  `test:inject` exists because an arm nobody has seen fail proves nothing. It
426
551
  mutates the decision layer one defect at a time — the mode gate removed, the
427
552
  PTY message matched as a substring, the preset remedy pointed back at the dead
428
- legacy directory, the two families collapsed into one bookkeeping slot, the
429
- advisory delivered per call instead of per agent — and requires that specific
553
+ legacy directory, two families collapsed into one bookkeeping slot, the advisory
554
+ delivered per call instead of per agent, the loader-status read moved onto the
555
+ error path, the family keyed on the rendered text — and requires that specific
430
556
  arms fail. It reports `SILENT ARMS: none` when every arm bites, restores the
431
557
  source in a `finally`, and prints `EQUIVALENT` (with the reason) for a mutation
432
558
  the current runtime cannot distinguish rather than counting it as a pass.
@@ -440,7 +566,11 @@ the current runtime cannot distinguish rather than counting it as a pass.
440
566
  [discussion #7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
441
567
  [discussion #7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
442
568
  [discussion #7816]: https://github.com/deepseek-ai/deepseek-harness/discussions/7816
569
+ [discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
570
+ [discussion #7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
571
+ [discussion #7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
443
572
  [#7750]: https://github.com/deepseek-ai/deepseek-harness/discussions/7750
444
573
  [#7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
445
574
  [#7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
446
- [discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
575
+ [#7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
576
+ [#7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
package/lib/advice.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * environment failure, and what is deliberately withheld.
4
4
  *
5
5
  * The text is assembled here as pure functions so every sentence can be pinned
6
- * by a test, one family at a time. The two families are shaped by the same two
7
- * questions, and they answer them differently:
6
+ * by a test, one family at a time. The three families are shaped by the same
7
+ * question — is this the sandbox's doing, and what can the reader do about it —
8
+ * and they answer it differently:
8
9
  *
9
10
  * - **The ACL failure** (`acl-provisioning`) *is* fixable by the caller, so its
10
11
  * advice names the right the caller is missing and gives the command.
@@ -45,6 +46,19 @@
45
46
  * Handing the model a command here would be advice to run something that
46
47
  * cannot run, and naming a one-shot shell tool would be advice to call a tool
47
48
  * the failing composition does not mount.
49
+ * - **The native-init death** (`native-init`) is the one whose remedy is **split**:
50
+ * the *class* is not the model's to fix, but one of its two measured producers
51
+ * is. A confined child that died with `STATUS_DLL_INIT_FAILED` never ran
52
+ * anything, so retrying the same call is pure waste — but if the program that
53
+ * could not start was an MSYS2/Git-Bash one, the same work expressed with
54
+ * PowerShell or `cmd` runs fine under the identical mode, and the model *can*
55
+ * make that change because the model is the one that wrote the command. So the
56
+ * advice carries a stop instruction, the one in-session conversion, and the
57
+ * user-side remedy for the other producer. What it deliberately does **not** do
58
+ * is guess which producer this is: the code alone cannot say, and the two
59
+ * checks it hands over are facts the reader holds (what program they ran;
60
+ * whether this is the packaged desktop app, which the plugin reports rather
61
+ * than assumes).
48
62
  *
49
63
  * Both give a **discriminator, not just a remedy**: applying a fix without
50
64
  * confirming the cause teaches nothing when the fix does not work. For the ACL
@@ -52,15 +66,21 @@
52
66
  * SID and grants `(F)` — which separates "Modify-only directory" from "the
53
67
  * documented prerequisite is wrong", the open question upstream. For the PTY
54
68
  * family it is the **effective sandbox mode**, which is why that advisory is
55
- * only ever built with the mode the call actually ran under.
69
+ * only ever built with the mode the call actually ran under. For the native-init
70
+ * family it is two checks the reader performs — which program could not start,
71
+ * and whether this host is the packaged desktop app — because the code alone
72
+ * cannot separate the producers and a guess would send half its readers to the
73
+ * wrong remedy.
56
74
  *
57
75
  * @module
58
76
  */
59
- import { failureLine } from './signature.js';
77
+ import { failureLine, STATUS_DLL_INIT_FAILED } from './signature.js';
60
78
  /** The upstream threads the ACL advisory is a stopgap for. */
61
79
  export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816';
62
80
  /** The upstream thread the persistent-shell advisory is a stopgap for. */
63
81
  export const PTY_DISCUSSIONS = '#7638';
82
+ /** The upstream threads the native-init-death advisory is a stopgap for. */
83
+ export const NATIVE_INIT_DISCUSSIONS = '#7876 / #7877';
64
84
  /** The documented prerequisite, quoted from the backend's README. */
65
85
  export const PREREQUISITE = 'granted directories must be caller-owned and grant `WRITE_OWNER`';
66
86
  /**
@@ -99,6 +119,19 @@ export const NOT_FIXES = [
99
119
  ];
100
120
  /** Placeholder the user replaces with the directory the error named. */
101
121
  const PLACEHOLDER = '<the directory from the error line above>';
122
+ /**
123
+ * Whether this process is running on an Electron binary.
124
+ *
125
+ * In the packaged desktop the harness host *is* Electron, started with
126
+ * `ELECTRON_RUN_AS_NODE=1` so it behaves as Node — which is why the variable is
127
+ * defined here and why its presence is the discriminator the `#7876` producer
128
+ * turns on: `sandbox-local` launches the sandbox runner as `process.execPath`,
129
+ * and in that build the exec path is the Electron executable.
130
+ * @returns true when `process.versions.electron` is set.
131
+ */
132
+ function electronHost() {
133
+ return process.versions.electron !== undefined;
134
+ }
102
135
  /**
103
136
  * The diagnosis paragraph for one class of failure.
104
137
  * @param failure - the recognized failure.
@@ -197,11 +230,11 @@ function versionBoundary() {
197
230
  *
198
231
  * The family decides everything: one function so a caller does not have to
199
232
  * remember which family needs which fact, and so the mode requirement of the
200
- * PTY family is enforced by construction rather than by convention.
233
+ * two gated families is enforced by construction rather than by convention.
201
234
  * @param failure - the recognized failure.
202
235
  * @param context - what the caller knows about the failing call.
203
236
  * @returns the user-role notice text, with any remedy ready to paste.
204
- * @throws when a PTY failure is advised without its resolved sandbox mode.
237
+ * @throws when a mode-gated failure is advised without its resolved sandbox mode.
205
238
  */
206
239
  export function advisoryText(failure, context = {}) {
207
240
  if (failure.family === 'pty-startup') {
@@ -210,6 +243,12 @@ export function advisoryText(failure, context = {}) {
210
243
  }
211
244
  return ptyAdvisory(failure, context.mode, context.tool, context.href);
212
245
  }
246
+ if (failure.family === 'native-init') {
247
+ if (context.mode === undefined) {
248
+ throw new Error('sandbox-grant-advisor: the native-init advisory requires the resolved sandbox mode');
249
+ }
250
+ return nativeInitAdvisory(failure, context.mode, context.electronHost ?? electronHost(), context.href);
251
+ }
213
252
  return aclAdvisory(failure, context.href);
214
253
  }
215
254
  /**
@@ -275,6 +314,78 @@ function aclAdvisory(failure, href) {
275
314
  'Your file read/write tools still work; only sandboxed command execution is blocked.',
276
315
  ].join('\n');
277
316
  }
317
+ /**
318
+ * Build the advisory for a confined Windows child that never reached its entry
319
+ * point.
320
+ *
321
+ * Three things this text must not do: retry silently (the identical call cannot
322
+ * start), hand the model a command to run (there may be no working shell to run
323
+ * it in — that is what died), or assert which producer this is. The code is a
324
+ * loader status and says nothing about the sandbox by itself, so the diagnosis
325
+ * is the *class* ("the process never started") plus the two producers that have
326
+ * been measured under this harness, each with the check that distinguishes it.
327
+ * One of those checks the plugin answers itself and reports as a fact — whether
328
+ * this process is an Electron binary — rather than assuming, because a reader
329
+ * told "this is the packaged desktop" without evidence would be reading a guess
330
+ * dressed as a finding.
331
+ * @param failure - the recognized failure.
332
+ * @param mode - the resolved sandbox mode the failing call ran under.
333
+ * @param onElectron - whether this process is an Electron binary.
334
+ * @param href - optional URL shown for the upstream threads.
335
+ * @returns the user-role notice text.
336
+ */
337
+ function nativeInitAdvisory(failure, mode, onElectron, href) {
338
+ const where = href === undefined ? `tracked upstream (discussions ${NATIVE_INIT_DISCUSSIONS})` : `tracked upstream: ${href}`;
339
+ return [
340
+ 'Sandboxed command never started — the process died while its native libraries were loading.',
341
+ '',
342
+ 'What was reported:',
343
+ ` ${failureLine(failure)} (0xC0000142 STATUS_DLL_INIT_FAILED)`,
344
+ `The call ran under sandbox mode \`${mode}\`, where the harness starts every command through its`,
345
+ 'restricted-token runner.',
346
+ '',
347
+ `0x${failure.exitCode.toString(16).toUpperCase()} is STATUS_DLL_INIT_FAILED: the Windows loader terminated the process while it was`,
348
+ 'initializing its DLLs and C runtime, which is BEFORE the program\'s entry point. A command that ran and',
349
+ 'then failed exits with its own status and prints its own output; this one produced neither. The number',
350
+ 'is also not a portable exit status — those are 0-255, and this is a 32-bit NTSTATUS. Nothing in the code',
351
+ 'says "sandbox" by itself; what makes the sandbox a candidate is the mode above, under which every',
352
+ 'command is spawned through the ACL runner.',
353
+ '',
354
+ 'Two producers have been measured under a confining Windows mode. Check which one this is:',
355
+ ' 1. An MSYS2 / Git-Bash program — `bash.exe`, `sh.exe`, or anything from a Git for Windows or MSYS2',
356
+ ' distribution. Under the restricted token its runtime cannot create the pipe it uses for signals,',
357
+ ' and it aborts in the loader phase (`couldn\'t create signal pipe, Win32 error 5`), while `cmd.exe`',
358
+ ' and PowerShell run fine in the same workspace under the same mode (#7877).',
359
+ ' If that is what could not start: write the same work as a PowerShell or `cmd` command instead and',
360
+ ' continue — do not retry the MSYS2 program.',
361
+ ' 2. The packaged desktop application\'s sandbox runner. `dsh-sandbox-local` starts the runner as',
362
+ ' `[process.execPath, runner.js]`, and in the packaged build `process.execPath` is the Electron',
363
+ ' executable, which starts as an *application* unless the child\'s environment carries',
364
+ ' `ELECTRON_RUN_AS_NODE=1` — so the runner never runs and every confined command reports this code',
365
+ ' with no output at all (#7876).',
366
+ onElectron
367
+ ? ' This process IS an Electron binary (`process.versions.electron` is set), so that producer applies here:'
368
+ : ' This process is NOT an Electron binary (`process.versions.electron` is unset), so the runner is a real',
369
+ onElectron
370
+ ? ' run the same command with `danger-full-access`, or from an unpacked `node apps/cli/lib/bin.js web`'
371
+ : ' Node binary here and that producer cannot be the cause. If the program was not an MSYS2 one either,',
372
+ onElectron
373
+ ? ' host where the runner is a real Node binary. If it works there, the runner never ran and this is the cause.'
374
+ : ' this failure is outside both measured producers: stop and hand it to the user.',
375
+ '',
376
+ 'Do not retry this call: the environment has not changed, and the identical call produces the identical',
377
+ 'code. Convert the work only in case 1; otherwise stop and hand it to the user.',
378
+ '',
379
+ 'Honest boundary — 0xC0000142 has producers this list does not have: a program that cannot load one of',
380
+ 'its own DLLs dies this way too, and the sandbox backend\'s own source records that a child started with',
381
+ 'a hidden console window does as well (which is why that backend avoids `CREATE_NO_WINDOW`). This is not',
382
+ 'a claim that the sandbox caused the failure — the code cannot say that. What is claimed is narrower and',
383
+ 'checkable: the process never reached its entry point, and under this mode these two producers are known.',
384
+ '',
385
+ 'This is a stopgap, ' + where + '. What it is NOT: this plugin neither changes an environment nor',
386
+ 'widens the sandbox — the checks above are yours to make, and `danger-full-access` is not offered as a fix.',
387
+ ].join('\n');
388
+ }
278
389
  /**
279
390
  * Build the advisory for a persistent-shell startup failure.
280
391
  *
@@ -328,14 +439,18 @@ function ptyAdvisory(failure, mode, tool, href) {
328
439
  * Build the pre-dispatch denial for the optional fail-fast half.
329
440
  *
330
441
  * The blocking half is deliberately **ACL-only**, and this function's parameter
331
- * type is where that is enforced. The PTY family gets an advisory and nothing
332
- * else, for a reason that is about the remedy rather than about the failure:
442
+ * type is where that is enforced. The two mode-gated families get an advisory
443
+ * and nothing else, for a reason that is about the remedy rather than about the
444
+ * failure:
333
445
  * the ACL remedy is a command the user can run *while the session continues*,
334
446
  * so refusing further identical calls cannot make the session unfinishable —
335
447
  * spending the budget always lets the call through, and a repaired environment
336
448
  * is discovered by exactly that. The PTY remedy is a preset swap, which happens
337
- * between sessions; refusing calls could only pad a session that is already
338
- * unable to do the thing being refused.
449
+ * between sessions, and the native-init remedy is a launch fix on the user's
450
+ * side (the one in-session part — rewriting an MSYS2 command — the model does by
451
+ * calling a different tool invocation, which has a different call key and is
452
+ * therefore never the call being refused); refusing calls could only pad a
453
+ * session that is already unable to do the thing being refused.
339
454
  * @param failure - the recognized failure.
340
455
  * @param observed - how many provisioning failures this agent has produced.
341
456
  * @param denial - this denial's 1-based ordinal.