@argszero/cordis-plugin-sandbox-grant-advisor 0.4.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 +200 -26
- package/lib/advice.js +178 -21
- package/lib/index.js +149 -46
- package/lib/signature.js +143 -12
- package/lib/state.js +5 -4
- package/lib/types/advice.d.ts +62 -12
- package/lib/types/index.d.ts +67 -18
- package/lib/types/signature.d.ts +153 -18
- package/lib/types/state.d.ts +5 -4
- package/package.json +2 -2
package/lib/types/signature.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recognize the
|
|
2
|
+
* Recognize the three environment failures this plugin explains, and refuse
|
|
3
3
|
* everything else.
|
|
4
4
|
*
|
|
5
5
|
* ## The ACL provisioning failure (`acl-provisioning`)
|
|
@@ -30,17 +30,32 @@
|
|
|
30
30
|
* the advisory can quote the exact line the model and the user are looking
|
|
31
31
|
* at, and the path can be re-used in the fix command.
|
|
32
32
|
*
|
|
33
|
-
* One signature covers
|
|
34
|
-
* the classifier cannot and must not try to tell them apart
|
|
35
|
-
*
|
|
36
|
-
* the
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
33
|
+
* One signature covers three environments, the text is identical in all of them,
|
|
34
|
+
* and the classifier cannot and must not try to tell them apart — but the
|
|
35
|
+
* *remedy* is not the same in all of them, which is why the advisory forks on a
|
|
36
|
+
* check the user runs rather than on a guess this module cannot make:
|
|
37
|
+
*
|
|
38
|
+
* - a workspace the caller created with `mkdir` that inherits "Authenticated
|
|
39
|
+
* Users: Modify" from the drive root (`#7622`, `#7646`, `#7720`);
|
|
40
|
+
* - a directory on a data volume where **no ACE names the caller at all**, so
|
|
41
|
+
* the inherited entry is the whole of their access (`#7750` on D:/E:,
|
|
42
|
+
* `#7735`'s second defect);
|
|
43
|
+
* - a directory **the caller does not own** — an installer- or
|
|
44
|
+
* administrator-created one, `#7771` (owner `BUILTIN\Administrators`, held
|
|
45
|
+
* deny-only for that token), which `#7804` reached from the other direction.
|
|
46
|
+
*
|
|
47
|
+
* In the first two the caller is the owner, so their implicit `WRITE_DAC`
|
|
48
|
+
* satisfies the DACL half and `WRITE_OWNER` is the single missing right — one
|
|
49
|
+
* unelevated `icacls /grant` supplies exactly it, and `#7750` measured that
|
|
50
|
+
* remedy working. In the third `WRITE_DAC` is missing as well, so that same
|
|
51
|
+
* `icacls` is refused for the very command that would fix it and `(WO)` alone
|
|
52
|
+
* would not be enough even if it went through. The advisory therefore hands over
|
|
53
|
+
* the ownership check (`(Get-Acl "<dir>").Owner`) as the branch selector and
|
|
54
|
+
* gives each branch the command that works there — the same class, two rights
|
|
55
|
+
* situations, and no guess about which one this is.
|
|
56
|
+
*
|
|
57
|
+
* What the text *does* carry that the failure does not is the version boundary:
|
|
58
|
+
* the merged DACL + label write exists only from `0.1.7-alpha.1` on
|
|
44
59
|
* (`packages/sandbox/sandbox-windows-acl/src/acl.ts`, flag
|
|
45
60
|
* `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`), so on an older line
|
|
46
61
|
* the same string belongs to a different cause space.
|
|
@@ -67,16 +82,85 @@
|
|
|
67
82
|
* different cause space (a slow or blocked shell) with a different remedy, and
|
|
68
83
|
* a classifier that names a wrong cause is worse than one that stays silent.
|
|
69
84
|
*
|
|
85
|
+
* ## The process that never started (`native-init`)
|
|
86
|
+
*
|
|
87
|
+
* The third family is not a message at all: it is a **structured exit code on a
|
|
88
|
+
* result the pipeline calls a success**. Two reports of one code —
|
|
89
|
+
* `STATUS_DLL_INIT_FAILED`, `0xC0000142`, seen as `-1073741502` in a tool result
|
|
90
|
+
* because Windows exit codes are 32-bit NTSTATUS values and Node reports them
|
|
91
|
+
* signed — describe a confined child that died while its native images were
|
|
92
|
+
* initializing, i.e. before its entry point. `#7876` is the packaged desktop app:
|
|
93
|
+
* `sandbox-local` starts the sandbox runner as `[process.execPath, entry]`, and
|
|
94
|
+
* in that build `process.execPath` is the Electron executable, which starts as an
|
|
95
|
+
* *app* unless `ELECTRON_RUN_AS_NODE=1` is in the child's environment — so the
|
|
96
|
+
* runner itself never runs and every confined command reports this code with no
|
|
97
|
+
* output at all. `#7877` is an MSYS2/Git-Bash program under the restricted
|
|
98
|
+
* token: bash cannot create its own signal pipe (`couldn't create signal pipe,
|
|
99
|
+
* Win32 error 5`) and aborts in the same place, while `cmd.exe` and `pwsh` run
|
|
100
|
+
* fine under the identical mode.
|
|
101
|
+
*
|
|
102
|
+
* **This family is the only one that is invisible from the error path**, and that
|
|
103
|
+
* is the whole reason it is classified from the canonical value instead of from
|
|
104
|
+
* text. Upstream's runner-failure rules admit exactly one code —
|
|
105
|
+
* `RUNNER_FAILURE_RULES['windows-acl'] = [{ allowedExitCodes: [127], fatalSignatures:
|
|
106
|
+
* ['windows-acl-run: '] }]` (`packages/sandbox/sandbox-local/src/index.ts`) — and
|
|
107
|
+
* `classifyRunnerFailure` skips any other code before it even looks at stderr
|
|
108
|
+
* (`packages/sandbox/sandbox/src/diagnostics.ts`), so `0xC0000142` is never a
|
|
109
|
+
* runner failure and `SandboxUnavailableError` is never thrown. The renderer then
|
|
110
|
+
* reports it the way it reports any finished command — *"Non-zero exits are
|
|
111
|
+
* reported, not errored … only infrastructure failures (spawn errors, aborts)
|
|
112
|
+
* surface as isError results"* (`packages/shell/tool-pwsh/src/render.ts`) — as
|
|
113
|
+
* `[exit code: …]`. A plugin reading only `isError` results (every version of
|
|
114
|
+
* this one before `0.6.0`) is structurally blind to it, which is exactly why the
|
|
115
|
+
* model retries a command that can never start.
|
|
116
|
+
*
|
|
117
|
+
* The read is `ToolExecutionSuccess.value` — the tool's own canonical output,
|
|
118
|
+
* documented as *"Execution-local canonical value; deliberately omitted from
|
|
119
|
+
* durable events"* (`packages/core/tools/src/index.ts`) — so the code arrives
|
|
120
|
+
* structurally and no line of rendered text can be mistaken for it. Reading it
|
|
121
|
+
* this way is what makes the recognition safe: a command that prints a line
|
|
122
|
+
* saying `0xC0000142` is not this failure, and a call whose arguments merely
|
|
123
|
+
* mention a Windows path is not either.
|
|
124
|
+
*
|
|
125
|
+
* Three narrowings, each of which is a thing that could otherwise make the
|
|
126
|
+
* diagnosis wrong:
|
|
127
|
+
*
|
|
128
|
+
* - **The `foreground` discriminator is required.** The shipped shell tools
|
|
129
|
+
* project a finished foreground run as `{ kind: 'foreground', exitCode, … }`
|
|
130
|
+
* and a still-running background handle as a different shape (`tool-pwsh` /
|
|
131
|
+
* `tool-bash`, mirrored by design), so requiring it keeps a value some other
|
|
132
|
+
* tool happens to build with an `exitCode` field out of this family. A value
|
|
133
|
+
* without it is left alone — the fail-closed direction, since the cost of
|
|
134
|
+
* silence is one missing diagnosis and the cost of a wrong match is a confident
|
|
135
|
+
* wrong cause.
|
|
136
|
+
* - **Only `STATUS_DLL_INIT_FAILED` is classified.** `0xC0000142` has producers
|
|
137
|
+
* this module does not know about (a program that simply cannot load its own
|
|
138
|
+
* DLLs, and the console-hiding that the sandbox backend's own source records as
|
|
139
|
+
* producing it), so the advisory enumerates the measured ones and says so
|
|
140
|
+
* rather than asserting one. The neighbouring statuses are deliberately **not**
|
|
141
|
+
* folded in: `0xC0000409` is the Cygwin/MSYS2 runtime's deliberate fast-fail
|
|
142
|
+
* (a different mechanism with a different story), and `0xC0000135` is a missing
|
|
143
|
+
* DLL (a packaging problem, not a sandbox one).
|
|
144
|
+
* - **No platform gate.** The code is a Windows NTSTATUS: a POSIX process cannot
|
|
145
|
+
* exit with a value above 255, so the number itself is the platform evidence. A
|
|
146
|
+
* `process.platform === 'win32'` check would add nothing a session could
|
|
147
|
+
* observe and would make this family untestable on the host this plugin is
|
|
148
|
+
* built on — which is how a family ships without ever having been run.
|
|
149
|
+
*
|
|
70
150
|
* @module
|
|
71
151
|
*/
|
|
72
152
|
/** Which provisioning operation failed, and which diagnosis follows from it. */
|
|
73
153
|
export type FailureClass =
|
|
74
154
|
/**
|
|
75
155
|
* `SetNamedSecurityInfoW` returned `ERROR_ACCESS_DENIED` (5): the merged
|
|
76
|
-
* DACL + label write was refused.
|
|
77
|
-
* here — the `mkdir`-inherited Modify workspace
|
|
78
|
-
* with no ACE naming the caller
|
|
79
|
-
*
|
|
156
|
+
* DACL + label write was refused. All three environments in the module doc
|
|
157
|
+
* land here — the `mkdir`-inherited Modify workspace, the data-volume
|
|
158
|
+
* directory with no ACE naming the caller, and the directory owned by another
|
|
159
|
+
* account — because the failure text cannot separate them. The first two are
|
|
160
|
+
* the same rights situation (the caller owns it; `WRITE_OWNER` is the whole of
|
|
161
|
+
* what is missing) and share the unelevated remedy; the third is missing
|
|
162
|
+
* `WRITE_DAC` as well, which is why the advisory hands over the ownership
|
|
163
|
+
* check and forks the command on it.
|
|
80
164
|
*/
|
|
81
165
|
'apply-denied'
|
|
82
166
|
/** `SetNamedSecurityInfoW` failed with a Win32 code other than `ERROR_ACCESS_DENIED`. */
|
|
@@ -88,7 +172,9 @@ export type FailureFamily =
|
|
|
88
172
|
/** The Windows sandbox could not provision its workspace (ACL / mandatory label). */
|
|
89
173
|
'acl-provisioning'
|
|
90
174
|
/** The persistent PTY shell could not start under a confining sandbox mode. */
|
|
91
|
-
| 'pty-startup'
|
|
175
|
+
| 'pty-startup'
|
|
176
|
+
/** A confined Windows child died while its native images were initializing. */
|
|
177
|
+
| 'native-init';
|
|
92
178
|
/** One recognized provisioning failure, with the producer's own fields kept. */
|
|
93
179
|
export interface ProvisioningFailure {
|
|
94
180
|
/** Which family this failure belongs to. */
|
|
@@ -120,8 +206,35 @@ export interface PtyStartupFailure {
|
|
|
120
206
|
/** The producer's message, kept as the constant so nothing can drift. */
|
|
121
207
|
readonly line: string;
|
|
122
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* `STATUS_DLL_INIT_FAILED`, the code a Windows process is terminated with when
|
|
211
|
+
* the loader fails while initializing it — before its entry point runs.
|
|
212
|
+
*
|
|
213
|
+
* Written unsigned here, which is how the NTSTATUS is named; a tool result
|
|
214
|
+
* usually carries it as the 32-bit signed number (`-1073741502`), and
|
|
215
|
+
* {@link classifyNativeInitDeath} accepts either because it compares the
|
|
216
|
+
* normalized 32-bit pattern.
|
|
217
|
+
*/
|
|
218
|
+
export declare const STATUS_DLL_INIT_FAILED = 3221225794;
|
|
219
|
+
/**
|
|
220
|
+
* The code a confined Windows child died with, and the forms the caller may have
|
|
221
|
+
* to quote it in.
|
|
222
|
+
*
|
|
223
|
+
* There is no `path` and no `api` here: the producer of this failure is the
|
|
224
|
+
* operating system's loader, which reports only the status. What the diagnosis
|
|
225
|
+
* needs beyond the code — the effective sandbox mode — comes from the policy
|
|
226
|
+
* resolver at the call site, exactly as it does for the PTY family.
|
|
227
|
+
*/
|
|
228
|
+
export interface NativeInitFailure {
|
|
229
|
+
/** Which family this failure belongs to. */
|
|
230
|
+
readonly family: 'native-init';
|
|
231
|
+
/** The exit code exactly as the tool reported it, so it can be quoted back verbatim. */
|
|
232
|
+
readonly rawExitCode: number;
|
|
233
|
+
/** The same code normalized to its unsigned 32-bit form, for comparison and printing. */
|
|
234
|
+
readonly exitCode: number;
|
|
235
|
+
}
|
|
123
236
|
/** Any failure this plugin recognizes, tagged by family. */
|
|
124
|
-
export type RecognizedFailure = ProvisioningFailure | PtyStartupFailure;
|
|
237
|
+
export type RecognizedFailure = ProvisioningFailure | PtyStartupFailure | NativeInitFailure;
|
|
125
238
|
/**
|
|
126
239
|
* Classify one failure message against the ACL family.
|
|
127
240
|
* @param message - the failure text, from the result's `error.message` or its rendered content.
|
|
@@ -142,6 +255,28 @@ export declare function classifyProvisioningFailure(message: string): Provisioni
|
|
|
142
255
|
* @returns the recognized failure, or undefined when this is not one.
|
|
143
256
|
*/
|
|
144
257
|
export declare function classifyPtyStartupFailure(message: string): PtyStartupFailure | undefined;
|
|
258
|
+
/**
|
|
259
|
+
* Classify one **successful** execution's canonical value as a Windows native-init
|
|
260
|
+
* death.
|
|
261
|
+
*
|
|
262
|
+
* This is the only family read from a result the pipeline calls a success, and
|
|
263
|
+
* that is a fact about the producer rather than a choice: the code reaches the
|
|
264
|
+
* tool result as an ordinary nonzero exit status (upstream's runner-failure rules
|
|
265
|
+
* admit only exit `127` with the `windows-acl-run: ` signature, so this one is
|
|
266
|
+
* never reclassified), and the renderer reports nonzero exits without erroring.
|
|
267
|
+
* `ToolExecutionFailure` carries no value at all, so there is nothing to read on
|
|
268
|
+
* the error path — a session sees this failure exactly when its shell tool
|
|
269
|
+
* reports a command that "ran".
|
|
270
|
+
*
|
|
271
|
+
* Recognized structurally, never from text: the value must be the foreground
|
|
272
|
+
* shell projection (`kind: 'foreground'`) with an integer `exitCode` whose 32-bit
|
|
273
|
+
* pattern is {@link STATUS_DLL_INIT_FAILED}. A command's own output claiming the
|
|
274
|
+
* code cannot reach this function, and neither can a value some other tool built
|
|
275
|
+
* with an `exitCode` field.
|
|
276
|
+
* @param value - the settled execution's canonical value (`ToolExecutionSuccess.value`).
|
|
277
|
+
* @returns the recognized failure, or undefined when this is not one.
|
|
278
|
+
*/
|
|
279
|
+
export declare function classifyNativeInitDeath(value: unknown): NativeInitFailure | undefined;
|
|
145
280
|
/**
|
|
146
281
|
* The one-line failure the producer wrote, for quoting back verbatim.
|
|
147
282
|
* @param failure - a recognized failure.
|
package/lib/types/state.d.ts
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* ## One record per family
|
|
10
10
|
*
|
|
11
|
-
* This plugin
|
|
12
|
-
* that cannot be provisioned (`acl-provisioning`)
|
|
13
|
-
* cannot start (`pty-startup`)
|
|
14
|
-
*
|
|
11
|
+
* This plugin recognizes three unrelated environment failures — a workspace
|
|
12
|
+
* that cannot be provisioned (`acl-provisioning`), a persistent shell that
|
|
13
|
+
* cannot start (`pty-startup`), and a confined Windows child that died during
|
|
14
|
+
* native initialization (`native-init`). They are different diagnoses with
|
|
15
|
+
* different remedies, so their bookkeeping is kept apart under one agent
|
|
15
16
|
* ({@link AgentState.families}): an agent that hits both is told about both,
|
|
16
17
|
* and an agent that has already been told about one is still told about the
|
|
17
18
|
* other. Sharing one "already advised" flag would silently swallow the second
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@argszero/cordis-plugin-sandbox-grant-advisor",
|
|
3
|
-
"description": "Turns
|
|
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 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.6.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|