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