@argszero/cordis-plugin-sandbox-grant-advisor 0.8.0 → 0.9.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 +119 -9
- package/lib/advice.js +75 -1
- package/lib/types/advice.d.ts +18 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,13 +35,60 @@ stuck.
|
|
|
35
35
|
|
|
36
36
|
### 1. Workspace provisioning — the Windows ACL failure (`acl-provisioning`)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
[discussion #7646], [discussion #7720], [discussion #7750], [discussion #7735]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
Twelve reports describe this exact line: [discussion #7538], [discussion #7622],
|
|
39
|
+
[discussion #7646], [discussion #7720], [discussion #7750], [discussion #7735],
|
|
40
|
+
[discussion #7771], [discussion #7804], [discussion #7816], [discussion #8232],
|
|
41
|
+
[discussion #8272] and [discussion #8275] (three of them — [#7750], [#7735],
|
|
42
|
+
[#8232] — on data-volume workspaces, where *no* ACE names the caller at all: the
|
|
43
|
+
inherited `Authenticated Users: Modify` is the whole of their access). In each one
|
|
44
|
+
every sandboxed command fails the same way, before it runs, and the error names
|
|
43
45
|
neither the missing right nor a remedy.
|
|
44
46
|
|
|
47
|
+
`#8232` contributes two facts about the *shape* of the failure rather than its
|
|
48
|
+
cause, and both are in the advisory now. One is that the failure belongs to the
|
|
49
|
+
**workspace, not the command**: there a `Get-Date` failed exactly like anything
|
|
50
|
+
that writes, because the grant is materialized before the command runs at all. The
|
|
51
|
+
other is the **scope of the repair**: the `(OI)(CI)(WO)` line covers *this*
|
|
52
|
+
directory and its children, so a second workspace root on the same volume is a
|
|
53
|
+
sibling rather than a child and needs the same line once more — which is why that
|
|
54
|
+
reporter saw a second workspace fail on a machine whose first one was already
|
|
55
|
+
repaired. The report also reaches the same root cause on its own (the merged write
|
|
56
|
+
wanting `WRITE_OWNER`, which owner-implicit rights do not carry), matching the
|
|
57
|
+
backend's documented prerequisite.
|
|
58
|
+
|
|
59
|
+
`#8272` and `#8275` add neither a cause nor a remedy — both land on the same
|
|
60
|
+
missing right — but each one closes a reading the diagnosis leaves open, and both
|
|
61
|
+
are in the advisory since 0.9.0.
|
|
62
|
+
|
|
63
|
+
`#8272` reached the same conclusion by a **better probe than the one above**: it
|
|
64
|
+
set a Low integrity level on a directory it owned, unelevated
|
|
65
|
+
(`icacls <dir> /setintegritylevel "(OI)(CI)Low"`), and was refused. That isolates
|
|
66
|
+
the label half of the merged call, which is the half that needs `WRITE_OWNER`, and
|
|
67
|
+
it does so without asking anyone to interpret a merged failure. It is quoted here
|
|
68
|
+
as evidence and deliberately **not** offered as a check: where the caller holds Full
|
|
69
|
+
control the same command *succeeds*, and then the label — and its inheritance — is
|
|
70
|
+
already written. A diagnostic that writes when it succeeds is not a diagnostic this
|
|
71
|
+
plugin hands out; the two read-only checks above are.
|
|
72
|
+
|
|
73
|
+
The report then went looking for the backend's own repair
|
|
74
|
+
(`diagnose-windows-sandbox-acl`) on a `0.1.7-rc.2` install and found it missing,
|
|
75
|
+
which it read as the package having dropped the skill from its `files` glob. It has
|
|
76
|
+
not: measured on the published tarballs (2026-09-29), `0.1.7-rc.2` names the skill
|
|
77
|
+
**zero times** in either README, ships no `assets/` directory at all, and carries no
|
|
78
|
+
file containing the registration symbol — while `0.2.0-rc.2` ships
|
|
79
|
+
`assets/diagnose-windows-sandbox-acl/{SKILL.md,scripts/diagnose-windows-sandbox-acl.ps1}`
|
|
80
|
+
and names it three times per README. The skill is **new in `0.2.0`**; a `0.1.7`
|
|
81
|
+
install that found the name was reading a `0.2.0`-era document. So the reach that
|
|
82
|
+
works is the upgrade, not a packaging fix, and the advisory says so — item 4 below.
|
|
83
|
+
|
|
84
|
+
`#8275` is the other direction: it had the skill, and the skill's own remedy
|
|
85
|
+
(Full control) *worked*, after which it noticed that the Low label the successful
|
|
86
|
+
apply writes is what regresses the two side effects it documents. Its proposal is to
|
|
87
|
+
**degrade provisioning to DACL-only** when the label is the half being refused,
|
|
88
|
+
which reads as though the label were one of two independent layers. It is not, and
|
|
89
|
+
the advisory answers that with the mechanism rather than with a refusal — item 6
|
|
90
|
+
below.
|
|
91
|
+
|
|
45
92
|
`#7720` is worth reading for where the failure lands: the grant is materialized
|
|
46
93
|
at sandbox **initialization**, so this is not one refused operation but *every*
|
|
47
94
|
shell tool at once — the reporter could not run `netstat` or even `icacls` to
|
|
@@ -94,6 +141,42 @@ Two consequences follow from that one line:
|
|
|
94
141
|
escape it closed. `#7750` asks for exactly this and explains why it is a
|
|
95
142
|
usability regression traded for a security fix.
|
|
96
143
|
|
|
144
|
+
There is a **second boundary on that same line**, and `#8272` is what made it
|
|
145
|
+
worth stating: the backend's own repair, `diagnose-windows-sandbox-acl`, is not
|
|
146
|
+
part of `0.1.7-*` at all — it arrives with the **`0.2.0`** line, where the
|
|
147
|
+
package starts shipping it under `assets/`. A reader who found the skill named
|
|
148
|
+
in a README while running `0.1.7-rc.2` was reading a `0.2.0`-era document, not a
|
|
149
|
+
package that dropped something: that release's own README names it zero times and
|
|
150
|
+
no file in its tree carries the registration symbol. So the useful reach is the
|
|
151
|
+
upgrade, and the unhelpful one — repairing a `files` glob in a release that has
|
|
152
|
+
no such directory — is not offered.
|
|
153
|
+
5. **The repair is per-directory, and the report is what established that.** `#8232`
|
|
154
|
+
applied the `(WO)` line, watched the workspace start working, and then hit the
|
|
155
|
+
same error on a *second* workspace root on the same volume. `(OI)(CI)` carries
|
|
156
|
+
the ACE into *children* of the directory that received it and nowhere else, so a
|
|
157
|
+
sibling root is untouched by it. One line per workspace root is therefore the
|
|
158
|
+
correct shape of the remedy, not one line per machine — and the advisory says so,
|
|
159
|
+
because the natural reading of "it worked" is "it is fixed".
|
|
160
|
+
6. **A "weaker grant" is a mechanism this backend cannot express, not a policy it
|
|
161
|
+
declines** (added in 0.9.0, from `#8275`). The label is what makes the apply a
|
|
162
|
+
SACL write, so declining it looks like dropping the expensive half — but there is
|
|
163
|
+
no DACL-only path to fall back to: the label rides the **same**
|
|
164
|
+
`SetNamedSecurityInfoW` as the grant (`acl.ts`: the flags are
|
|
165
|
+
`DACL_SECURITY_INFORMATION` alone only when the label edit is `keep`, and
|
|
166
|
+
`grantWrite`'s apply branch always passes `apply`), and the idempotence fast path
|
|
167
|
+
requires the exact label among its three conditions. And dropping the label alone
|
|
168
|
+
would not leave a working workspace: the confined token is itself lowered to Low
|
|
169
|
+
before any child starts (`token.ts`'s `restrictTokenIntegrity`, whose own comment
|
|
170
|
+
calls Low "the level the mandatory labels `grantWrite` applies are matched
|
|
171
|
+
against"), so the directory's Low label is what lets that Low child write there at
|
|
172
|
+
all under no-write-up. A DACL-only mode that keeps the token lowering yields a
|
|
173
|
+
workspace the sandbox can start a command in and the command then cannot write to;
|
|
174
|
+
one that drops the token lowering too — which is what `#8275`'s own appendix
|
|
175
|
+
records a community patch having to do — gives up half the confinement rather than
|
|
176
|
+
one of two independent layers. The advisory says this instead of a bare "no",
|
|
177
|
+
because a reader told only "no" reaches for the workaround without knowing what
|
|
178
|
+
else it has to change.
|
|
179
|
+
|
|
97
180
|
The grant is materialized lazily, on the first confined call, and **nothing is
|
|
98
181
|
cached when it throws** — so the same failure repeats per command (850 calls
|
|
99
182
|
across 39 sessions in #7622; 52,588 output tokens with no output in #7538),
|
|
@@ -115,6 +198,10 @@ Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4) ... `0.1.7-alph
|
|
|
115
198
|
label — and with it the SACL, flag 20 — arrives. ... Rolling back is not the fix either: the label is what
|
|
116
199
|
confines deletes to the workspace, and reverting it reintroduces the escape it closed.
|
|
117
200
|
|
|
201
|
+
The other boundary on that last line: the `diagnose-windows-sandbox-acl` skill is not part of `0.1.7-*` at all
|
|
202
|
+
— it arrives with `0.2.0`, where the backend starts shipping it under `assets/`. The reach that works is the
|
|
203
|
+
upgrade, not a packaging fix.
|
|
204
|
+
|
|
118
205
|
Confirm the cause (unelevated) — `icacls` is a normal user command:
|
|
119
206
|
icacls "D:\ws"
|
|
120
207
|
|
|
@@ -143,6 +230,14 @@ What will NOT fix it on its own — both look like the right move, and both were
|
|
|
143
230
|
supplies the DACL half and still not WRITE_OWNER, the right this call needs.
|
|
144
231
|
icacls "D:\ws" /reset /T /C
|
|
145
232
|
restores inheritance, and inheritance is what supplied the Modify-only ACE above.
|
|
233
|
+
|
|
234
|
+
One thing to know before asking for a weaker grant, because that is the next idea after this diagnosis —
|
|
235
|
+
and it is not a smaller version of the same grant:
|
|
236
|
+
The label rides the SAME `SetNamedSecurityInfoW` as the DACL, so there is no DACL-only path to fall back
|
|
237
|
+
to — it would have to be built. And dropping the label alone would not leave a working workspace: the
|
|
238
|
+
backend lowers the confined token to Low before any child starts, and the directory's Low label is what
|
|
239
|
+
lets that Low child write here at all. Declining the label usefully means declining the token's Low level
|
|
240
|
+
with it, which gives up half the confinement rather than one of two independent layers.
|
|
146
241
|
```
|
|
147
242
|
|
|
148
243
|
**Why the remedy forks** (added in 0.5.0). The same error covers two different
|
|
@@ -614,10 +709,19 @@ the newest of that line.
|
|
|
614
709
|
|
|
615
710
|
The whole set is re-probed whenever this package's source changes rather than
|
|
616
711
|
carried over from an earlier version: the range is a claim about *this* build of
|
|
617
|
-
the plugin, so `0.7.1` re-ran all five lines above
|
|
618
|
-
removed from the range rather than left claimed. The
|
|
619
|
-
versions are the ones to read back when a probe is quoted
|
|
620
|
-
script pins them by exact version, and `--keep` leaves the
|
|
712
|
+
the plugin, so `0.7.1` re-ran all five lines above and `0.9.0` re-ran them again. A
|
|
713
|
+
line whose probe fails is removed from the range rather than left claimed. The
|
|
714
|
+
scratch tree's resolved versions are the ones to read back when a probe is quoted
|
|
715
|
+
as evidence — the probe script pins them by exact version, and `--keep` leaves the
|
|
716
|
+
tree in place to check.
|
|
717
|
+
|
|
718
|
+
**The next line's pre-releases are outside that range on purpose**, and the guard
|
|
719
|
+
in `test/packaging.spec.mjs` enforces it (a range that admits `0.2.0` fails the
|
|
720
|
+
suite). `0.2.0-rc.2` — the build the newest report in this family runs, including
|
|
721
|
+
its Desktop variant — was nevertheless probed by hand (`npm run test:probe-lines --
|
|
722
|
+
0.2.0-rc.2`) and the suite goes green there, which is the honest reason to expect
|
|
723
|
+
the plugin to work on it. It is a measurement, not a claim: the range widens when
|
|
724
|
+
the `0.2.0` line is the released one rather than its pre-release.
|
|
621
725
|
|
|
622
726
|
The mode lookup stays guarded through this version too: the native-init family
|
|
623
727
|
needs the same resolved mode as the PTY family, and it takes it from the same
|
|
@@ -656,6 +760,9 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
656
760
|
[discussion #7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
|
|
657
761
|
[discussion #7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
|
|
658
762
|
[discussion #7816]: https://github.com/deepseek-ai/deepseek-harness/discussions/7816
|
|
763
|
+
[discussion #8232]: https://github.com/deepseek-ai/deepseek-harness/discussions/8232
|
|
764
|
+
[discussion #8272]: https://github.com/deepseek-ai/deepseek-harness/discussions/8272
|
|
765
|
+
[discussion #8275]: https://github.com/deepseek-ai/deepseek-harness/discussions/8275
|
|
659
766
|
[discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
|
|
660
767
|
[discussion #7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
|
|
661
768
|
[discussion #7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
|
|
@@ -665,6 +772,9 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
665
772
|
[#7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
|
|
666
773
|
[#7876]: https://github.com/deepseek-ai/deepseek-harness/discussions/7876
|
|
667
774
|
[#7877]: https://github.com/deepseek-ai/deepseek-harness/discussions/7877
|
|
775
|
+
[#8232]: https://github.com/deepseek-ai/deepseek-harness/discussions/8232
|
|
776
|
+
[#8272]: https://github.com/deepseek-ai/deepseek-harness/discussions/8272
|
|
777
|
+
[#8275]: https://github.com/deepseek-ai/deepseek-harness/discussions/8275
|
|
668
778
|
[#8193]: https://github.com/deepseek-ai/deepseek-harness/discussions/8193
|
|
669
779
|
[#8174]: https://github.com/deepseek-ai/deepseek-harness/discussions/8174
|
|
670
780
|
[#8208]: https://github.com/deepseek-ai/deepseek-harness/discussions/8208
|
package/lib/advice.js
CHANGED
|
@@ -39,6 +39,23 @@
|
|
|
39
39
|
* one-liner would send the second environment to a command that is refused
|
|
40
40
|
* before it runs — the same defect this module exists to answer, a remedy that
|
|
41
41
|
* does not work delivered confidently.
|
|
42
|
+
*
|
|
43
|
+
* **Two later reports add shape and a boundary, not a new cause.** `#8272`
|
|
44
|
+
* arrived at the missing right with an independent probe (it set a Low
|
|
45
|
+
* integrity level on a directory it owned, unelevated, and was refused — the
|
|
46
|
+
* label half isolated from the merged call), and then went looking for the
|
|
47
|
+
* backend's own `diagnose-windows-sandbox-acl` skill on a `0.1.7-rc.2` install,
|
|
48
|
+
* where it does not exist: the skill is new in **`0.2.0`**, and that release's
|
|
49
|
+
* README is where the promise was read. So the boundary section states where
|
|
50
|
+
* the skill actually arrives, and calls the "the package dropped it" reading
|
|
51
|
+
* what it is — version skew, not a `files` glob. `#8275` adds the question the
|
|
52
|
+
* diagnosis invites: whether the label half can simply be declined, since the
|
|
53
|
+
* label is what makes the apply a SACL write. It can't, and the reason is a
|
|
54
|
+
* mechanism rather than a policy — the label rides the *same* call as the grant
|
|
55
|
+
* and the confined token is itself lowered to Low, so a DACL-only mode that
|
|
56
|
+
* keeps the token lowering yields a workspace the confined child cannot write
|
|
57
|
+
* to. That is stated instead of a bare "no", because a reader told only "no"
|
|
58
|
+
* reaches for the workaround without knowing what else it would have to change.
|
|
42
59
|
* - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
|
|
43
60
|
* caller — least of all by the model, which has no shell to run anything in.
|
|
44
61
|
* So its advice says so and stops: the remedy is a user-side preset choice,
|
|
@@ -92,7 +109,7 @@
|
|
|
92
109
|
*/
|
|
93
110
|
import { failureLine, STATUS_DLL_INIT_FAILED } from './signature.js';
|
|
94
111
|
/** The upstream threads the ACL advisory is a stopgap for. */
|
|
95
|
-
export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816';
|
|
112
|
+
export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275';
|
|
96
113
|
/** The upstream thread the persistent-shell advisory is a stopgap for. */
|
|
97
114
|
export const PTY_DISCUSSIONS = '#7638';
|
|
98
115
|
/** The upstream threads the native-init-death advisory is a stopgap for. */
|
|
@@ -239,8 +256,57 @@ function versionBoundary() {
|
|
|
239
256
|
'`0.1.6-alpha.x`-or-older line this exact failure therefore belongs to a different cause space, while on any',
|
|
240
257
|
'`0.1.7-*` line it is this one. Rolling back is not the fix either: the label is what confines deletes to the',
|
|
241
258
|
'workspace, and reverting it reintroduces the escape it closed.',
|
|
259
|
+
'',
|
|
260
|
+
'The other boundary on that last line, for a reader who goes looking for the built-in repair: the',
|
|
261
|
+
'`diagnose-windows-sandbox-acl` skill is not part of `0.1.7-*` at all — it arrives with `0.2.0`, which is where',
|
|
262
|
+
'the backend starts shipping it under `assets/`. A `0.1.7-rc.2` install that found the skill named in a README',
|
|
263
|
+
'was reading a `0.2.0`-era document: that release\'s own README names it zero times, and no file in its tree',
|
|
264
|
+
'carries the registration symbol. The reach that works is the upgrade, not a packaging fix — nothing was',
|
|
265
|
+
'dropped from the `0.1.7` file list, because there was nothing in `0.1.7` to drop.',
|
|
242
266
|
].join('\n');
|
|
243
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Why a "weaker grant" is not a smaller version of the same thing.
|
|
270
|
+
*
|
|
271
|
+
* The natural next question after "this needs `WRITE_OWNER`" is whether the
|
|
272
|
+
* label can simply be declined — the label is what makes the apply a SACL write,
|
|
273
|
+
* so dropping it looks like dropping the expensive half. The answer is that the
|
|
274
|
+
* two halves are one mechanism, and the shape of the answer matters more than
|
|
275
|
+
* the verdict: a reader who is told only "no" reaches for the community workaround
|
|
276
|
+
* without knowing what else it has to change.
|
|
277
|
+
*
|
|
278
|
+
* Everything here is read off the shipped source rather than inferred: the label
|
|
279
|
+
* rides the **same** `SetNamedSecurityInfoW` as the grant (`acl.ts`: the
|
|
280
|
+
* security-information flags are `DACL_SECURITY_INFORMATION` alone only when the
|
|
281
|
+
* label edit is `keep`, and `grantWrite`'s apply branch always passes `apply`),
|
|
282
|
+
* and the confined token is lowered to Low before any child starts
|
|
283
|
+
* (`token.ts`'s `restrictTokenIntegrity`, whose own comment calls Low "the level
|
|
284
|
+
* the mandatory labels `grantWrite` applies are matched against"). The directory's
|
|
285
|
+
* Low label is therefore what lets the Low child write there at all under
|
|
286
|
+
* no-write-up — so "sandbox works, workspace unlabelled" is not a configuration
|
|
287
|
+
* this backend can express.
|
|
288
|
+
*
|
|
289
|
+
* Emitted only for `apply-denied`, the class whose diagnosis is the missing
|
|
290
|
+
* `WRITE_OWNER`: that is the one where the label is what gets refused, and so the
|
|
291
|
+
* only one where declining it is an idea a reader could have.
|
|
292
|
+
* @returns the section's lines.
|
|
293
|
+
*/
|
|
294
|
+
function degradedGrant() {
|
|
295
|
+
return [
|
|
296
|
+
'One thing to know before asking for a weaker grant, because that is the next idea after this diagnosis —',
|
|
297
|
+
'and it is not a smaller version of the same grant:',
|
|
298
|
+
' The label rides the SAME `SetNamedSecurityInfoW` as the DACL (one call, two security-information flags), so',
|
|
299
|
+
' there is no DACL-only path to fall back to — it would have to be built. And dropping the label alone would',
|
|
300
|
+
' not leave a working workspace: the backend lowers the confined token to Low before any child starts, and its',
|
|
301
|
+
' own comment calls Low "the level the mandatory labels `grantWrite` applies are matched against". The',
|
|
302
|
+
' directory\'s Low label is what lets that Low child write here at all; a workspace that keeps the token',
|
|
303
|
+
' lowering but not the label is one the sandbox can start a command in and the command then cannot write to.',
|
|
304
|
+
' Declining the label usefully means declining the token\'s Low level with it — which gives up half the',
|
|
305
|
+
' confinement rather than one of two independent layers, and is a different proposal from dropping a layer',
|
|
306
|
+
' that was never load-bearing.',
|
|
307
|
+
'This is not offered here as a fix, and neither is `danger-full-access`.',
|
|
308
|
+
];
|
|
309
|
+
}
|
|
244
310
|
/**
|
|
245
311
|
* Build the advisory attached to the failing tool result.
|
|
246
312
|
*
|
|
@@ -278,6 +344,8 @@ function aclAdvisory(failure, href) {
|
|
|
278
344
|
const where = href === undefined ? `tracked upstream (discussions ${ACL_DISCUSSIONS})` : `tracked upstream: ${href}`;
|
|
279
345
|
return [
|
|
280
346
|
'Sandbox provisioning failed — no sandboxed command can run in this workspace until its ACL applies.',
|
|
347
|
+
'The failure belongs to the WORKSPACE, not to the command: under this mode a command that only reads',
|
|
348
|
+
'fails identically, so trying a different or more harmless command is not a retry that can succeed.',
|
|
281
349
|
'',
|
|
282
350
|
'What was reported:',
|
|
283
351
|
` ${failureLine(failure)}`,
|
|
@@ -302,6 +370,9 @@ function aclAdvisory(failure, href) {
|
|
|
302
370
|
` cmd: icacls "${path}" /grant "%USERNAME%:(OI)(CI)(WO)"`,
|
|
303
371
|
'WRITE_OWNER is exactly the right the prerequisite names, so this grants nothing the harness did not ask for,',
|
|
304
372
|
'and (OI)(CI) makes the ACE inheritable, so one command reaches the workspace\'s existing subdirectories.',
|
|
373
|
+
'The grant is still scoped to THIS directory and its children: another workspace root on the same volume is',
|
|
374
|
+
'a sibling rather than a child, so it needs the same line once — which is why a second workspace fails on a',
|
|
375
|
+
'machine where the first one was already repaired.',
|
|
305
376
|
'Full control works just as well — the same line with `F` in place of `(WO)`:',
|
|
306
377
|
` icacls "${path}" /grant "$env:USERNAME:(OI)(CI)F"`,
|
|
307
378
|
'Why `(WO)` is the whole of what is missing there: an owner holds READ_CONTROL and WRITE_DAC implicitly,',
|
|
@@ -324,8 +395,11 @@ function aclAdvisory(failure, href) {
|
|
|
324
395
|
'inherits Full control for you — and open the session on that one.',
|
|
325
396
|
'',
|
|
326
397
|
...nonFixes(failure, path),
|
|
398
|
+
...(failure.klass === 'apply-denied' ? [...degradedGrant(), ''] : []),
|
|
327
399
|
'How to read this: the harness documents the prerequisite (' + PREREQUISITE + ') and this',
|
|
328
400
|
'error does not name it yet, so the advice is delivered here instead. This is a stopgap, ' + where + '.',
|
|
401
|
+
'It arrives through the session rather than through a shell, which is the thing this failure has just taken',
|
|
402
|
+
'away — the reason a repair script cannot be the answer at the moment it is needed.',
|
|
329
403
|
'What it is NOT: this plugin neither edits ACLs nor elevates — the command above is yours to run.',
|
|
330
404
|
'Your file read/write tools still work; only sandboxed command execution is blocked.',
|
|
331
405
|
].join('\n');
|
package/lib/types/advice.d.ts
CHANGED
|
@@ -39,6 +39,23 @@
|
|
|
39
39
|
* one-liner would send the second environment to a command that is refused
|
|
40
40
|
* before it runs — the same defect this module exists to answer, a remedy that
|
|
41
41
|
* does not work delivered confidently.
|
|
42
|
+
*
|
|
43
|
+
* **Two later reports add shape and a boundary, not a new cause.** `#8272`
|
|
44
|
+
* arrived at the missing right with an independent probe (it set a Low
|
|
45
|
+
* integrity level on a directory it owned, unelevated, and was refused — the
|
|
46
|
+
* label half isolated from the merged call), and then went looking for the
|
|
47
|
+
* backend's own `diagnose-windows-sandbox-acl` skill on a `0.1.7-rc.2` install,
|
|
48
|
+
* where it does not exist: the skill is new in **`0.2.0`**, and that release's
|
|
49
|
+
* README is where the promise was read. So the boundary section states where
|
|
50
|
+
* the skill actually arrives, and calls the "the package dropped it" reading
|
|
51
|
+
* what it is — version skew, not a `files` glob. `#8275` adds the question the
|
|
52
|
+
* diagnosis invites: whether the label half can simply be declined, since the
|
|
53
|
+
* label is what makes the apply a SACL write. It can't, and the reason is a
|
|
54
|
+
* mechanism rather than a policy — the label rides the *same* call as the grant
|
|
55
|
+
* and the confined token is itself lowered to Low, so a DACL-only mode that
|
|
56
|
+
* keeps the token lowering yields a workspace the confined child cannot write
|
|
57
|
+
* to. That is stated instead of a bare "no", because a reader told only "no"
|
|
58
|
+
* reaches for the workaround without knowing what else it would have to change.
|
|
42
59
|
* - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
|
|
43
60
|
* caller — least of all by the model, which has no shell to run anything in.
|
|
44
61
|
* So its advice says so and stops: the remedy is a user-side preset choice,
|
|
@@ -93,7 +110,7 @@
|
|
|
93
110
|
import type { ProvisioningFailure, RecognizedFailure } from './signature.js';
|
|
94
111
|
import type { SandboxModeName } from './mode.js';
|
|
95
112
|
/** The upstream threads the ACL advisory is a stopgap for. */
|
|
96
|
-
export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816";
|
|
113
|
+
export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816 / #8232 / #8272 / #8275";
|
|
97
114
|
/** The upstream thread the persistent-shell advisory is a stopgap for. */
|
|
98
115
|
export declare const PTY_DISCUSSIONS = "#7638";
|
|
99
116
|
/** The upstream threads the native-init-death advisory is a stopgap for. */
|
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:
|
|
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: twelve reports (#7538, #7622, #7646, #7720, #7750, #7735, #7771, #7804, #7816, #8232, #8272, #8275) of one signature \u2014 every sandboxed command fails before it runs with `SetNamedSecurityInfoW failed (Win32 5): grantWrite(<workspace>)`, because the merged DACL + mandatory-label write needs WRITE_OWNER on the directory (an object right the caller can self-grant), not SeSecurityPrivilege and not elevation; the host grant is materialized lazily and caches nothing on the failure path, so the same failure repeats per command. 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 second boundary the later reports needed (the backend's own `diagnose-windows-sandbox-acl` skill is not part of the 0.1.7 line at all — it arrives with 0.2.0, measured on both published tarballs: 0.1.7-rc.2 names it zero times in either README, ships no assets, and carries no registration symbol anywhere in its tree, while 0.2.0-rc.2 ships assets/diagnose-windows-sandbox-acl/ and names it three times per README — so a reader who found the promise in a README was reading a 0.2.0-era document, and sending them to repair a files glob would be the wrong repair), and why a \"weaker grant\" is a mechanism the backend cannot express rather than a policy it declines (the label rides the SAME SetNamedSecurityInfoW as the grant — there is no DACL-only apply path to fall back to — and the confined token is itself lowered to Low, so the object's Low label is what lets the confined child write there at all; declining the label usefully means declining the token level with it), the discriminator, the remedy forked on an ownership check the user runs (`(Get-Acl \"<dir>\").Owner`), because one command cannot serve both rights situations: where the caller owns the directory, the unelevated `icacls ... :(OI)(CI)(WO)` is the whole of what is missing \u2014 the owner's implicit WRITE_DAC already covers the DACL half \u2014 and where the caller does not own it that same command is refused for want of WRITE_DAC, so the grant has to come from an elevated account, or by taking ownership first, or by moving the workspace under %USERPROFILE% \u2014 and the two remedies that look right and are not (`takeown`, `icacls /reset`), each with the reason it fails — and it states the two things about the failure's shape that the reports had to measure for themselves (#8232): that it belongs to the workspace rather than to the command (a command that only reads fails identically, so there is no harmless retry), and that the grant is scoped to the directory it names and its children, so a sibling workspace root on the same volume needs the line once more; the PTY advisory names the failing combination, 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 \u2014 for the Electron host \u2014 names the fix #8193 measured rather than a wider mode: host the runner on a real node.exe (the desktop ships one under `resources/runtime/primary-runtime/dependencies/node/bin/node.exe`), where the same confined `pwsh`/`cmd` spawns succeed, while `danger-full-access` is described as a way to confirm the diagnosis and not a fix, together with the warning that unsetting `ELECTRON_RUN_AS_NODE` instead would leave the desktop's Electron-hosted runner unable to execute `runner.js` at all (the interaction #8193 records with #8174), and says plainly which producers it does not know \u2014 it never claims the sandbox caused the failure and never offers a widened mode as a fix. An optional, off-by-default `enforceAfter` refuses an identical ACL call this plugin has watched fail, bounded by `maxDenials`; the blocking half is ACL-only by design. It never edits an ACL, never elevates, never sets another process's environment, and never changes a preset or a mode, and it complements repeat-guard-escalation, which keys on call identity rather than on the environment signature.",
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|