@argszero/cordis-plugin-sandbox-grant-advisor 0.6.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 +29 -13
- package/lib/advice.js +21 -7
- package/lib/types/advice.d.ts +10 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -236,8 +236,9 @@ the advisory never names the dead directory.
|
|
|
236
236
|
|
|
237
237
|
### 3. A confined child that never started (`native-init`)
|
|
238
238
|
|
|
239
|
-
|
|
240
|
-
(MSYS2 / Git Bash).
|
|
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
|
|
241
242
|
loader terminated the process while it was initializing its native images, i.e.
|
|
242
243
|
**before the program's entry point**. A command that ran and then failed exits
|
|
243
244
|
with its own status and prints its own output; this one produced neither.
|
|
@@ -258,16 +259,27 @@ Two producers have been measured under a confining mode:
|
|
|
258
259
|
(`@deepseek-ai/dsh-base/cordis.patch.yml`), so the combination is likely
|
|
259
260
|
never covered upstream. The **one conversion a model can make itself** is to
|
|
260
261
|
write the same work as a PowerShell or `cmd` command.
|
|
261
|
-
2. **The packaged desktop app's sandbox runner** ([`#7876`]).
|
|
262
|
+
2. **The packaged desktop app's sandbox runner** ([`#7876`], [`#8193`]).
|
|
262
263
|
`dsh-sandbox-local` launches the runner as `[process.execPath, entry]`, and
|
|
263
|
-
in the packaged build `process.execPath` is the Electron executable
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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.
|
|
271
283
|
|
|
272
284
|
**Why this family is read from a successful result.** The producer never marks
|
|
273
285
|
it an error, and that is a fact about upstream rather than a choice here:
|
|
@@ -325,7 +337,10 @@ backend's own source avoids `CREATE_NO_WINDOW` for). So the advisory diagnoses
|
|
|
325
337
|
the **class** ("the process never reached its entry point") and enumerates the
|
|
326
338
|
producers measured under a confining mode, each with the check that separates
|
|
327
339
|
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).
|
|
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
|
|
329
344
|
offers `danger-full-access` as a fix and never suggests a sandbox setting be
|
|
330
345
|
relaxed.
|
|
331
346
|
|
|
@@ -524,7 +539,7 @@ the newest of that line.
|
|
|
524
539
|
|
|
525
540
|
The whole set is re-probed whenever this package's source changes rather than
|
|
526
541
|
carried over from an earlier version: the range is a claim about *this* build of
|
|
527
|
-
the plugin, so `0.
|
|
542
|
+
the plugin, so `0.7.0` re-ran all five lines above. A line whose probe fails is
|
|
528
543
|
removed from the range rather than left claimed. The scratch tree's resolved
|
|
529
544
|
versions are the ones to read back when a probe is quoted as evidence — the probe
|
|
530
545
|
script pins them by exact version, and `--keep` leaves the tree in place to check.
|
|
@@ -574,3 +589,4 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
574
589
|
[#7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
|
|
575
590
|
[#7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
|
|
576
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
|
@@ -58,7 +58,12 @@
|
|
|
58
58
|
* is guess which producer this is: the code alone cannot say, and the two
|
|
59
59
|
* checks it hands over are facts the reader holds (what program they ran;
|
|
60
60
|
* whether this is the packaged desktop app, which the plugin reports rather
|
|
61
|
-
* than assumes).
|
|
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.
|
|
62
67
|
*
|
|
63
68
|
* Both give a **discriminator, not just a remedy**: applying a fix without
|
|
64
69
|
* confirming the cause teaches nothing when the fix does not work. For the ACL
|
|
@@ -70,7 +75,9 @@
|
|
|
70
75
|
* family it is two checks the reader performs — which program could not start,
|
|
71
76
|
* and whether this host is the packaged desktop app — because the code alone
|
|
72
77
|
* cannot separate the producers and a guess would send half its readers to the
|
|
73
|
-
* wrong remedy.
|
|
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.
|
|
74
81
|
*
|
|
75
82
|
* @module
|
|
76
83
|
*/
|
|
@@ -80,7 +87,7 @@ export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 /
|
|
|
80
87
|
/** The upstream thread the persistent-shell advisory is a stopgap for. */
|
|
81
88
|
export const PTY_DISCUSSIONS = '#7638';
|
|
82
89
|
/** The upstream threads the native-init-death advisory is a stopgap for. */
|
|
83
|
-
export const NATIVE_INIT_DISCUSSIONS = '#7876 / #7877';
|
|
90
|
+
export const NATIVE_INIT_DISCUSSIONS = '#7876 / #7877 / #8193';
|
|
84
91
|
/** The documented prerequisite, quoted from the backend's README. */
|
|
85
92
|
export const PREREQUISITE = 'granted directories must be caller-owned and grant `WRITE_OWNER`';
|
|
86
93
|
/**
|
|
@@ -360,9 +367,16 @@ function nativeInitAdvisory(failure, mode, onElectron, href) {
|
|
|
360
367
|
' continue — do not retry the MSYS2 program.',
|
|
361
368
|
' 2. The packaged desktop application\'s sandbox runner. `dsh-sandbox-local` starts the runner as',
|
|
362
369
|
' `[process.execPath, runner.js]`, and in the packaged build `process.execPath` is the Electron',
|
|
363
|
-
' executable
|
|
364
|
-
'
|
|
365
|
-
'
|
|
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.',
|
|
366
380
|
onElectron
|
|
367
381
|
? ' This process IS an Electron binary (`process.versions.electron` is set), so that producer applies here:'
|
|
368
382
|
: ' This process is NOT an Electron binary (`process.versions.electron` is unset), so the runner is a real',
|
|
@@ -370,7 +384,7 @@ function nativeInitAdvisory(failure, mode, onElectron, href) {
|
|
|
370
384
|
? ' run the same command with `danger-full-access`, or from an unpacked `node apps/cli/lib/bin.js web`'
|
|
371
385
|
: ' Node binary here and that producer cannot be the cause. If the program was not an MSYS2 one either,',
|
|
372
386
|
onElectron
|
|
373
|
-
? ' host where the runner is a real Node binary.
|
|
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.'
|
|
374
388
|
: ' this failure is outside both measured producers: stop and hand it to the user.',
|
|
375
389
|
'',
|
|
376
390
|
'Do not retry this call: the environment has not changed, and the identical call produces the identical',
|
package/lib/types/advice.d.ts
CHANGED
|
@@ -58,7 +58,12 @@
|
|
|
58
58
|
* is guess which producer this is: the code alone cannot say, and the two
|
|
59
59
|
* checks it hands over are facts the reader holds (what program they ran;
|
|
60
60
|
* whether this is the packaged desktop app, which the plugin reports rather
|
|
61
|
-
* than assumes).
|
|
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.
|
|
62
67
|
*
|
|
63
68
|
* Both give a **discriminator, not just a remedy**: applying a fix without
|
|
64
69
|
* confirming the cause teaches nothing when the fix does not work. For the ACL
|
|
@@ -70,7 +75,9 @@
|
|
|
70
75
|
* family it is two checks the reader performs — which program could not start,
|
|
71
76
|
* and whether this host is the packaged desktop app — because the code alone
|
|
72
77
|
* cannot separate the producers and a guess would send half its readers to the
|
|
73
|
-
* wrong remedy.
|
|
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.
|
|
74
81
|
*
|
|
75
82
|
* @module
|
|
76
83
|
*/
|
|
@@ -81,7 +88,7 @@ export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 /
|
|
|
81
88
|
/** The upstream thread the persistent-shell advisory is a stopgap for. */
|
|
82
89
|
export declare const PTY_DISCUSSIONS = "#7638";
|
|
83
90
|
/** The upstream threads the native-init-death advisory is a stopgap for. */
|
|
84
|
-
export declare const NATIVE_INIT_DISCUSSIONS = "#7876 / #7877";
|
|
91
|
+
export declare const NATIVE_INIT_DISCUSSIONS = "#7876 / #7877 / #8193";
|
|
85
92
|
/** The documented prerequisite, quoted from the backend's README. */
|
|
86
93
|
export declare const PREREQUISITE = "granted directories must be caller-owned and grant `WRITE_OWNER`";
|
|
87
94
|
/**
|
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: nine reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816) of one signature \u2014 every sandboxed command fails before it runs with `SetNamedSecurityInfoW failed (Win32 5): grantWrite(<workspace>)`, because the merged DACL + mandatory-label write needs WRITE_OWNER on the directory (an object right the caller can self-grant), not SeSecurityPrivilege and not elevation; the host grant is materialized lazily and caches nothing on the failure path, so the same failure repeats per command. Family 2, the persistent shell (#7638): with the `minimal` preset under a confining sandbox mode every shell call dies instantly with `PTY shell exited during startup` because the terminal backend cannot create the pseudo-console inside the sandbox, retrying never helps, and `minimal` mounts no fallback shell tool. Family 3, a confined Windows child that died during native initialization (#7876, #7877): every command spawned through the sandbox runner can report exit 0xC0000142 STATUS_DLL_INIT_FAILED with the process never reaching its entry point \u2014 the packaged desktop starts that runner as [process.execPath, entry] and Electron launches as an application unless the child's environment carries ELECTRON_RUN_AS_NODE=1 (#7876), and an MSYS2 / Git-Bash program cannot create its signal pipe under the restricted token while cmd.exe and pwsh run fine in the same workspace under the same mode (#7877) \u2014 and because upstream's runner-failure rules admit only exit 127 with the `windows-acl-run: ` signature, the code is never an error: it arrives as the canonical value of a result the pipeline calls a success. The plugin observes the public `tools/post-execute` waterfall, classifies all three signatures narrowly (only the two `...NamedSecurityInfoW` operations; the PTY message matched on a whole line, never as a substring; the loader status read as a 32-bit integer out of the shell tool's own canonical success value, never from rendered text; both mode-gated families advised only under a mode the policy resolver reports as confining), and attaches ONE durable user-role advisory per agent per family through `additionalContexts`. The ACL advisory names the missing right, both environments the identical text can describe (an inherited Modify-only entry, a data volume where no ACE names the caller at all, and a directory owned by another account), the version boundary that arrived with the mandatory label (0.1.7-alpha.1, flag 20, versus the DACL-only flag 4 up to 0.1.6-alpha.x) together with why downgrading is not the remedy, the discriminator, the remedy forked on an ownership check the user runs (`(Get-Acl \"<dir>\").Owner`), because one command cannot serve both rights situations: where the caller owns the directory, the unelevated `icacls ... :(OI)(CI)(WO)` is the whole of what is missing \u2014 the owner's implicit WRITE_DAC already covers the DACL half \u2014 and where the caller does not own it that same command is refused for want of WRITE_DAC, so the grant has to come from an elevated account, or by taking ownership first, or by moving the workspace under %USERPROFILE% \u2014 and the two remedies that look right and are not (`takeown`, `icacls /reset`), each with the reason it fails; the PTY advisory names the failing combination, states the resolved mode, tells the model to stop rather than retry, and hands the user-side preset choice over \u2014 it never names a shell tool the failing composition does not mount; the native-init advisory states the resolved mode, says the process died before its entry point, enumerates the two producers measured under a confining mode 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 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.
|
|
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: nine reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816) of one signature \u2014 every sandboxed command fails before it runs with `SetNamedSecurityInfoW failed (Win32 5): grantWrite(<workspace>)`, because the merged DACL + mandatory-label write needs WRITE_OWNER on the directory (an object right the caller can self-grant), not SeSecurityPrivilege and not elevation; the host grant is materialized lazily and caches nothing on the failure path, so the same failure repeats per command. Family 2, the persistent shell (#7638): with the `minimal` preset under a confining sandbox mode every shell call dies instantly with `PTY shell exited during startup` because the terminal backend cannot create the pseudo-console inside the sandbox, retrying never helps, and `minimal` mounts no fallback shell tool. Family 3, a confined Windows child that died during native initialization (#7876, #7877): every command spawned through the sandbox runner can report exit 0xC0000142 STATUS_DLL_INIT_FAILED with the process never reaching its entry point \u2014 the packaged desktop starts that runner as [process.execPath, entry], and that host has been measured twice with one indistinguishable appearance from inside a session \u2014 either nothing on the runner path ran because the Electron binary launches as an application unless the child's environment carries ELECTRON_RUN_AS_NODE=1 (#7876), or the desktop launcher does set that variable and the child still dies because the restricted token is derived from the Electron process image (#8193), and an MSYS2 / Git-Bash program cannot create its signal pipe under the restricted token while cmd.exe and pwsh run fine in the same workspace under the same mode (#7877) \u2014 and because upstream's runner-failure rules admit only exit 127 with the `windows-acl-run: ` signature, the code is never an error: it arrives as the canonical value of a result the pipeline calls a success. The plugin observes the public `tools/post-execute` waterfall, classifies all three signatures narrowly (only the two `...NamedSecurityInfoW` operations; the PTY message matched on a whole line, never as a substring; the loader status read as a 32-bit integer out of the shell tool's own canonical success value, never from rendered text; both mode-gated families advised only under a mode the policy resolver reports as confining), and attaches ONE durable user-role advisory per agent per family through `additionalContexts`. The ACL advisory names the missing right, both environments the identical text can describe (an inherited Modify-only entry, a data volume where no ACE names the caller at all, and a directory owned by another account), the version boundary that arrived with the mandatory label (0.1.7-alpha.1, flag 20, versus the DACL-only flag 4 up to 0.1.6-alpha.x) together with why downgrading is not the remedy, the discriminator, the remedy forked on an ownership check the user runs (`(Get-Acl \"<dir>\").Owner`), because one command cannot serve both rights situations: where the caller owns the directory, the unelevated `icacls ... :(OI)(CI)(WO)` is the whole of what is missing \u2014 the owner's implicit WRITE_DAC already covers the DACL half \u2014 and where the caller does not own it that same command is refused for want of WRITE_DAC, so the grant has to come from an elevated account, or by taking ownership first, or by moving the workspace under %USERPROFILE% \u2014 and the two remedies that look right and are not (`takeown`, `icacls /reset`), each with the reason it fails; the PTY advisory names the failing combination, states the resolved mode, tells the model to stop rather than retry, and hands the user-side preset choice over \u2014 it never names a shell tool the failing composition does not mount; the native-init advisory states the resolved mode, says the process died before its entry point, enumerates the two producers measured under a confining mode \u2014 naming both measured shapes of the desktop host rather than asserting the one that was measured first \u2014 with the check that separates them (what program the reader ran; whether this host is the packaged desktop binary, which the plugin measures and reports rather than assumes), carries the one conversion a model can make itself (rewrite the work as PowerShell or `cmd` when an MSYS2 program is what could not start), and 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.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|