@argszero/cordis-plugin-sandbox-grant-advisor 0.3.0 → 0.5.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 +103 -17
- package/lib/advice.js +98 -13
- package/lib/signature.js +30 -0
- package/lib/types/advice.d.ts +26 -1
- package/lib/types/signature.d.ts +41 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,10 +30,12 @@ stuck.
|
|
|
30
30
|
|
|
31
31
|
### 1. Workspace provisioning — the Windows ACL failure (`acl-provisioning`)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
[discussion #7646], [discussion #7720]
|
|
35
|
-
the
|
|
36
|
-
|
|
33
|
+
Six reports describe this exact line: [discussion #7538], [discussion #7622],
|
|
34
|
+
[discussion #7646], [discussion #7720], [discussion #7750], [discussion #7735]
|
|
35
|
+
(the last two on data-volume workspaces, where *no* ACE names the caller at all —
|
|
36
|
+
the inherited `Authenticated Users: Modify` is the whole of their access). In each
|
|
37
|
+
one every sandboxed command fails the same way, before it runs, and the error names
|
|
38
|
+
neither the missing right nor a remedy.
|
|
37
39
|
|
|
38
40
|
`#7720` is worth reading for where the failure lands: the grant is materialized
|
|
39
41
|
at sandbox **initialization**, so this is not one refused operation but *every*
|
|
@@ -66,7 +68,26 @@ Two consequences follow from that one line:
|
|
|
66
68
|
is the token-privilege form of the same idea, and it is the hypothesis the
|
|
67
69
|
reports naturally reach for — `whoami /priv` cannot tell the two apart,
|
|
68
70
|
because `WRITE_OWNER` is an object right and never appears in that table.
|
|
69
|
-
Granting
|
|
71
|
+
Granting `WRITE_OWNER` on the workspace root needs **no** elevation. `#7735`
|
|
72
|
+
settles the gate with an isolation table on one machine and one unprivileged
|
|
73
|
+
account: the label write succeeds with Full control *or with Take-ownership
|
|
74
|
+
alone*, and fails with `ChangePermissions`, `ReadPermissions` or `Modify`
|
|
75
|
+
alone — so the gate is `WRITE_OWNER` and nothing else.
|
|
76
|
+
3. **The remedy is the narrowest form of that right.** The advisory recommends
|
|
77
|
+
`icacls <dir> /grant "<user>:(OI)(CI)(WO)"` — `WO` *is* `WRITE_OWNER`, i.e.
|
|
78
|
+
literally the right the documented prerequisite names, so the one-liner grants
|
|
79
|
+
nothing the harness did not ask for — and offers Full control second, as the
|
|
80
|
+
same line with `F` in place of `(WO)`. Both assume the caller owns the
|
|
81
|
+
directory: owner-implicit rights cover the DACL half of the merged write.
|
|
82
|
+
4. **The version boundary is stated, because the error cannot carry it.** Up to
|
|
83
|
+
`0.1.6-alpha.x` the backend's `SetNamedSecurityInfoW` wrote the DACL only
|
|
84
|
+
(flag 4) and a Modify-only workspace provisioned fine; the mandatory label —
|
|
85
|
+
and with it the SACL, flag 20 — arrives in `0.1.7-alpha.1`. So the same string
|
|
86
|
+
on an older line belongs to a different cause space, and the natural reach
|
|
87
|
+
(downgrade to the build that "worked") is refused with its reason: the label is
|
|
88
|
+
what confines deletes to the workspace, and reverting it reintroduces the
|
|
89
|
+
escape it closed. `#7750` asks for exactly this and explains why it is a
|
|
90
|
+
usability regression traded for a security fix.
|
|
70
91
|
|
|
71
92
|
The grant is materialized lazily, on the first confined call, and **nothing is
|
|
72
93
|
cached when it throws** — so the same failure repeats per command (850 calls
|
|
@@ -84,21 +105,64 @@ What was reported:
|
|
|
84
105
|
Why it is refused while the directory looks writable: that call is a MERGED write ...
|
|
85
106
|
... the label half additionally needs WRITE_OWNER on the directory. ...
|
|
86
107
|
|
|
108
|
+
A version boundary worth knowing before reaching for an older build: the label half is new to this package.
|
|
109
|
+
Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4) ... `0.1.7-alpha.1` is where the mandatory
|
|
110
|
+
label — and with it the SACL, flag 20 — arrives. ... Rolling back is not the fix either: the label is what
|
|
111
|
+
confines deletes to the workspace, and reverting it reintroduces the escape it closed.
|
|
112
|
+
|
|
87
113
|
Confirm the cause (unelevated) — `icacls` is a normal user command:
|
|
88
114
|
icacls "D:\ws"
|
|
89
115
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
Ownership decides which of the two commands below can work, so read it first — PowerShell 5.1 or later:
|
|
117
|
+
(Get-Acl "D:\ws").Owner # compare with: whoami
|
|
118
|
+
|
|
119
|
+
IF YOU OWN THE DIRECTORY — the usual workspace, on a data volume as much as on C::
|
|
120
|
+
one unelevated line, then run the command again:
|
|
121
|
+
PowerShell: icacls "D:\ws" /grant "$env:USERNAME:(OI)(CI)(WO)"
|
|
122
|
+
cmd: icacls "D:\ws" /grant "%USERNAME%:(OI)(CI)(WO)"
|
|
123
|
+
... Full control works just as well — the same line with `F` in place of `(WO)`
|
|
124
|
+
|
|
125
|
+
IF YOU DO NOT OWN IT — a directory an installer or another account created, e.g. owner
|
|
126
|
+
`BUILTIN\Administrators`:
|
|
127
|
+
the line above cannot run at all. Changing a DACL takes WRITE_DAC, which you hold neither as owner nor
|
|
128
|
+
through any ACE, so `icacls /grant` is refused with `Access is denied` — for the very command that would
|
|
129
|
+
fix it. ... Run the grant once from an account that already holds both — that is, from an ELEVATED prompt:
|
|
130
|
+
icacls "D:\ws" /grant "<your-account>:(OI)(CI)F"
|
|
131
|
+
... or take ownership first (also elevated; it wants SeTakeOwnership), after which the unelevated `(WO)`
|
|
132
|
+
line above applies: icacls "D:\ws" /setowner "<your-account>"
|
|
133
|
+
... or sidestep the ACL: create the workspace under `%USERPROFILE%`.
|
|
134
|
+
|
|
135
|
+
What will NOT fix it on its own — both look like the right move, and both were tried and reported:
|
|
95
136
|
takeown /F "D:\ws" /R /D Y
|
|
96
|
-
makes you the owner,
|
|
97
|
-
|
|
137
|
+
makes you the owner, and ownership's implicit rights are READ_CONTROL and WRITE_DAC only — so it
|
|
138
|
+
supplies the DACL half and still not WRITE_OWNER, the right this call needs.
|
|
98
139
|
icacls "D:\ws" /reset /T /C
|
|
99
140
|
restores inheritance, and inheritance is what supplied the Modify-only ACE above.
|
|
100
141
|
```
|
|
101
142
|
|
|
143
|
+
**Why the remedy forks** (added in 0.5.0). The same error covers two different
|
|
144
|
+
rights situations, and one command cannot serve both. Where the caller **owns**
|
|
145
|
+
the directory, the owner's implicit `WRITE_DAC` satisfies the DACL half of the
|
|
146
|
+
merged write, `WRITE_OWNER` is the single missing right, and the unelevated
|
|
147
|
+
`icacls /grant` that supplies it can itself run — [#7750] measured exactly that
|
|
148
|
+
fix working. Where the caller **does not own** it ([#7771]: owner
|
|
149
|
+
`BUILTIN\Administrators`, held deny-only for that token), `WRITE_DAC` is missing
|
|
150
|
+
too, so the very same command is refused before it does anything, and `(WO)`
|
|
151
|
+
alone would not be enough even if it went through. The failure text is identical
|
|
152
|
+
in both, so the classifier cannot pick a branch — the advisory hands over the
|
|
153
|
+
**ownership check** as the selector instead of guessing, which is also the
|
|
154
|
+
actionable-guidance half of what [#7771] asked for. Until 0.5.0 a single
|
|
155
|
+
unconditional one-liner was printed, with a sentence noting it assumed
|
|
156
|
+
ownership; that would have sent the second environment to a command that is
|
|
157
|
+
denied — the same defect this plugin exists to answer.
|
|
158
|
+
|
|
159
|
+
**What is deliberately *not* shipped**: `icacls ... /grant "<user>:(OI)(CI)(WD,WO)"`,
|
|
160
|
+
the two needed rights named explicitly. It is the tighter form and it is
|
|
161
|
+
plausibly correct syntax, but this project has no Windows host to run it on, and
|
|
162
|
+
shipping an unverified command in a remedy whose whole point is that it works is
|
|
163
|
+
the failure mode being fixed. `F` (verified by [#7804]'s reporter) and
|
|
164
|
+
`/setowner` (named by both reports) are given instead.
|
|
165
|
+
|
|
102
166
|
### 2. Persistent shell startup (`pty-startup`)
|
|
103
167
|
|
|
104
168
|
[Discussion #7638] reports the second shape: with the **`minimal` preset** on
|
|
@@ -295,6 +359,12 @@ than one that stays silent.
|
|
|
295
359
|
replace it.** That guard keys on **call identity** (identical arguments
|
|
296
360
|
retried); this one keys on the **environment signature**, which is how several
|
|
297
361
|
*different* commands share one cause. Mounting both is sensible.
|
|
362
|
+
- **The plugin cannot see the launcher half of `#7735`.** The Low label's other
|
|
363
|
+
side effect — the shell's publisher confirmation before launching a
|
|
364
|
+
Low-integrity `.bat`/`.cmd`/`.exe` — never appears in a tool result, so it is
|
|
365
|
+
outside the seam this plugin subscribes to. The report and its proposed fix
|
|
366
|
+
stay with the maintainers; all this plugin can do is explain the provisioning
|
|
367
|
+
failure that shares its root.
|
|
298
368
|
- **The real fix is upstream, in both families.** For the ACL failure,
|
|
299
369
|
`grantWrite` already computes `hasExactGrant` / `hasExactDeny` /
|
|
300
370
|
`hasExactLabel` and discards which one was false, so the diagnostic that turns
|
|
@@ -325,11 +395,19 @@ composition that does not mount the service degrades to silence instead of
|
|
|
325
395
|
failing to load. See `src/mode.ts`.
|
|
326
396
|
|
|
327
397
|
Probed at the newest build of every line the range admits — `0.1.2-rc.1`,
|
|
328
|
-
`0.1.3-alpha.2`, `0.1.5-rc.3`, `0.1.6-alpha.2`, `0.1.7-rc.
|
|
329
|
-
|
|
330
|
-
this range, installs each one from the registry
|
|
331
|
-
|
|
332
|
-
|
|
398
|
+
`0.1.3-alpha.2`, `0.1.5-rc.3`, `0.1.6-alpha.2`, `0.1.7-rc.2` (the newest build of
|
|
399
|
+
the line the later Windows reports ran on) — with `npm run test:probe-lines`,
|
|
400
|
+
which derives those builds from this range, installs each one from the registry
|
|
401
|
+
into a scratch tree and runs the suite against it. `0.1.7-rc.1`, the build the
|
|
402
|
+
third report ran, is admitted by the same `||` segment and was probed while it was
|
|
403
|
+
the newest of that line.
|
|
404
|
+
|
|
405
|
+
The whole set is re-probed whenever this package's source changes rather than
|
|
406
|
+
carried over from an earlier version: the range is a claim about *this* build of
|
|
407
|
+
the plugin, so `0.4.0` re-ran all five lines above. A line whose probe fails is
|
|
408
|
+
removed from the range rather than left claimed. The scratch tree's resolved
|
|
409
|
+
versions are the ones to read back when a probe is quoted as evidence — the probe
|
|
410
|
+
script pins them by exact version, and `--keep` leaves the tree in place to check.
|
|
333
411
|
|
|
334
412
|
## Development
|
|
335
413
|
|
|
@@ -357,4 +435,12 @@ the current runtime cannot distinguish rather than counting it as a pass.
|
|
|
357
435
|
[discussion #7622]: https://github.com/deepseek-ai/deepseek-harness/discussions/7622
|
|
358
436
|
[discussion #7646]: https://github.com/deepseek-ai/deepseek-harness/discussions/7646
|
|
359
437
|
[discussion #7720]: https://github.com/deepseek-ai/deepseek-harness/discussions/7720
|
|
438
|
+
[discussion #7750]: https://github.com/deepseek-ai/deepseek-harness/discussions/7750
|
|
439
|
+
[discussion #7735]: https://github.com/deepseek-ai/deepseek-harness/discussions/7735
|
|
440
|
+
[discussion #7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
|
|
441
|
+
[discussion #7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
|
|
442
|
+
[discussion #7816]: https://github.com/deepseek-ai/deepseek-harness/discussions/7816
|
|
443
|
+
[#7750]: https://github.com/deepseek-ai/deepseek-harness/discussions/7750
|
|
444
|
+
[#7771]: https://github.com/deepseek-ai/deepseek-harness/discussions/7771
|
|
445
|
+
[#7804]: https://github.com/deepseek-ai/deepseek-harness/discussions/7804
|
|
360
446
|
[discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
|
package/lib/advice.js
CHANGED
|
@@ -13,6 +13,31 @@
|
|
|
13
13
|
* the caller can grant itself with `icacls`, unelevated. It is **not**
|
|
14
14
|
* `SeSecurityPrivilege`, the token privilege the reports naturally reach for;
|
|
15
15
|
* `whoami /priv` cannot show the difference, and elevation is the wrong lever.
|
|
16
|
+
* The remedy is the **narrowest** form of that grant — `(WO)` alone, which is
|
|
17
|
+
* literally the right the backend's prerequisite names — with Full control
|
|
18
|
+
* offered as the broad alternative; and the advisory carries the **version
|
|
19
|
+
* boundary** the label introduced (`0.1.7-alpha.1`), because that is what
|
|
20
|
+
* separates "this is the label failure" from "this is something else", and
|
|
21
|
+
* because rolling back is the reach it invites while making the very problem
|
|
22
|
+
* it closed come back.
|
|
23
|
+
*
|
|
24
|
+
* **The remedy is forked on ownership, because one command cannot serve both
|
|
25
|
+
* environments.** The reports split into two rights situations behind an
|
|
26
|
+
* identical error: a workspace **the caller owns** (`#7622`, `#7646`, `#7720`,
|
|
27
|
+
* `#7750`, `#7804`), where the owner's implicit `WRITE_DAC` satisfies the DACL
|
|
28
|
+
* half and `(WO)` is the whole of what is missing — so the `icacls /grant`
|
|
29
|
+
* that supplies it *can itself run*, unelevated; and a directory **the caller
|
|
30
|
+
* does not own** (`#7771`: owner `BUILTIN\Administrators`, held deny-only for
|
|
31
|
+
* their token), where `WRITE_DAC` is missing too, so `icacls /grant` is denied
|
|
32
|
+
* for the very command that would fix it, and `(WO)` alone would not be enough
|
|
33
|
+
* even if it went through. The classifier cannot tell these apart — the text is
|
|
34
|
+
* identical — so the advisory does what it can do instead of guessing: it hands
|
|
35
|
+
* over the **ownership check** (`(Get-Acl "<dir>").Owner`) as the branch
|
|
36
|
+
* selector, then gives each branch the command that actually works there, and
|
|
37
|
+
* says why the other branch's command is not a fallback. A single unconditional
|
|
38
|
+
* one-liner would send the second environment to a command that is refused
|
|
39
|
+
* before it runs — the same defect this module exists to answer, a remedy that
|
|
40
|
+
* does not work delivered confidently.
|
|
16
41
|
* - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
|
|
17
42
|
* caller — least of all by the model, which has no shell to run anything in.
|
|
18
43
|
* So its advice says so and stops: the remedy is a user-side preset choice,
|
|
@@ -33,7 +58,7 @@
|
|
|
33
58
|
*/
|
|
34
59
|
import { failureLine } from './signature.js';
|
|
35
60
|
/** The upstream threads the ACL advisory is a stopgap for. */
|
|
36
|
-
export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720';
|
|
61
|
+
export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816';
|
|
37
62
|
/** The upstream thread the persistent-shell advisory is a stopgap for. */
|
|
38
63
|
export const PTY_DISCUSSIONS = '#7638';
|
|
39
64
|
/** The documented prerequisite, quoted from the backend's README. */
|
|
@@ -87,7 +112,10 @@ function diagnosis(failure) {
|
|
|
87
112
|
'mandatory-integrity label go out as one `SetNamedSecurityInfoW`. The label lives in the SACL, and the',
|
|
88
113
|
"owner's implicit rights cover only READ_CONTROL and WRITE_DAC, so the label half additionally needs",
|
|
89
114
|
'WRITE_OWNER on the directory. A workspace created with `mkdir` normally inherits',
|
|
90
|
-
'"Authenticated Users: Modify" (`0x1301bf`) from the drive root — and that mask has neither right
|
|
115
|
+
'"Authenticated Users: Modify" (`0x1301bf`) from the drive root — and that mask has neither right; on a data',
|
|
116
|
+
'volume there may be no ACE naming you at all, so that inherited entry is the whole of your access. The two',
|
|
117
|
+
'halves go out as one call, so a refused label discards the write grant with it, and the sandbox then',
|
|
118
|
+
'refuses to start any command in the workspace instead of running it unconfined.',
|
|
91
119
|
'This is a directory ACL fact, not a token privilege: `whoami /priv` will not show it, and',
|
|
92
120
|
'SeSecurityPrivilege is the wrong lever here.',
|
|
93
121
|
].join('\n');
|
|
@@ -101,7 +129,7 @@ function diagnosis(failure) {
|
|
|
101
129
|
return [
|
|
102
130
|
'Why it is refused: this is the same merged write, but the Win32 code is not ERROR_ACCESS_DENIED (5), so',
|
|
103
131
|
'the missing-rights story above does not apply verbatim — a missing path, a non-directory target, or a',
|
|
104
|
-
'filesystem that does not carry ACLs are all possibilities. The
|
|
132
|
+
'filesystem that does not carry ACLs are all possibilities. The commands below are safe to try; if the',
|
|
105
133
|
'code persists, it is a different failure and worth reporting with the code.',
|
|
106
134
|
].join('\n');
|
|
107
135
|
}
|
|
@@ -119,7 +147,11 @@ function diagnosis(failure) {
|
|
|
119
147
|
* diagnosis.
|
|
120
148
|
*
|
|
121
149
|
* A negative claim still has to be earned: the failure to avoid is advice that
|
|
122
|
-
* is confidently wrong in the other direction.
|
|
150
|
+
* is confidently wrong in the other direction. That is why the `takeown` line
|
|
151
|
+
* claims only what is true in **both** ownership branches: it supplies the DACL
|
|
152
|
+
* half and never `WRITE_OWNER`, so it is not the fix by itself — while still
|
|
153
|
+
* being a legitimate first step (with elevation) where the caller is not the
|
|
154
|
+
* owner. Calling it useless outright would have been the mirror-image error.
|
|
123
155
|
* @param failure - the recognized provisioning failure.
|
|
124
156
|
* @param path - the directory the error named, or the placeholder.
|
|
125
157
|
* @returns the section's lines, or an empty array for a class it does not fit.
|
|
@@ -128,15 +160,38 @@ function nonFixes(failure, path) {
|
|
|
128
160
|
if (failure.klass !== 'apply-denied')
|
|
129
161
|
return [];
|
|
130
162
|
return [
|
|
131
|
-
'What will NOT fix it — both look like the right move, and both were tried and reported:',
|
|
163
|
+
'What will NOT fix it on its own — both look like the right move, and both were tried and reported:',
|
|
132
164
|
` takeown /F "${path}" /R /D Y`,
|
|
133
|
-
" makes you the owner,
|
|
134
|
-
'
|
|
165
|
+
" makes you the owner, and ownership's implicit rights are READ_CONTROL and WRITE_DAC only — so it",
|
|
166
|
+
' supplies the DACL half and still not WRITE_OWNER, the right this call needs. In the second branch above',
|
|
167
|
+
' it is a legitimate first step with elevation; it is never the fix by itself.',
|
|
135
168
|
` icacls "${path}" /reset /T /C`,
|
|
136
169
|
' restores inheritance, and inheritance is what supplied the Modify-only ACE above.',
|
|
137
170
|
'',
|
|
138
171
|
];
|
|
139
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* When the label half arrived, and why an older build is not the remedy.
|
|
175
|
+
*
|
|
176
|
+
* Emitted for every class: the boundary is a fact about the package
|
|
177
|
+
* `sandbox-windows-acl` (whose `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`
|
|
178
|
+
* flag is what needs `WRITE_OWNER`), not about which of its two calls failed, so
|
|
179
|
+
* it is true wherever this module is willing to speak at all. It is also the one
|
|
180
|
+
* fact neither report could get from the error: the failure looks identical on a
|
|
181
|
+
* line where the label does not exist yet, and the build that introduced it is
|
|
182
|
+
* the natural thing to reach for and the wrong one to reach for.
|
|
183
|
+
* @returns the section's lines.
|
|
184
|
+
*/
|
|
185
|
+
function versionBoundary() {
|
|
186
|
+
return [
|
|
187
|
+
'A version boundary worth knowing before reaching for an older build: the label half is new to this package.',
|
|
188
|
+
'Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4), so a Modify-only workspace provisioned',
|
|
189
|
+
'fine; `0.1.7-alpha.1` is where the mandatory label — and with it the SACL, flag 20 — arrives. On a',
|
|
190
|
+
'`0.1.6-alpha.x`-or-older line this exact failure therefore belongs to a different cause space, while on any',
|
|
191
|
+
'`0.1.7-*` line it is this one. Rolling back is not the fix either: the label is what confines deletes to the',
|
|
192
|
+
'workspace, and reverting it reintroduces the escape it closed.',
|
|
193
|
+
].join('\n');
|
|
194
|
+
}
|
|
140
195
|
/**
|
|
141
196
|
* Build the advisory attached to the failing tool result.
|
|
142
197
|
*
|
|
@@ -174,14 +229,44 @@ function aclAdvisory(failure, href) {
|
|
|
174
229
|
'',
|
|
175
230
|
diagnosis(failure),
|
|
176
231
|
'',
|
|
232
|
+
versionBoundary(),
|
|
233
|
+
'',
|
|
177
234
|
'Confirm the cause (unelevated) — `icacls` is a normal user command:',
|
|
178
235
|
` icacls "${path}"`,
|
|
179
|
-
'Look for an ACE that names YOUR OWN account (run `whoami` if unsure) with (F) / Full control
|
|
180
|
-
'If the strongest entry naming you is (M) / Modify
|
|
236
|
+
'Look for an ACE that names YOUR OWN account (run `whoami` if unsure) with (F) / Full control or',
|
|
237
|
+
'(WO) / Write owner. If the strongest entry naming you is (M) / Modify — or no entry names you at all and',
|
|
238
|
+
'your access comes from an inherited `Authenticated Users:(M)` — that is this failure.',
|
|
239
|
+
'',
|
|
240
|
+
'Ownership decides which of the two commands below can work, so read it first — PowerShell 5.1 or later:',
|
|
241
|
+
` (Get-Acl "${path}").Owner # compare with: whoami`,
|
|
242
|
+
'If that is not your own account, take the second branch: the first one is refused before it runs.',
|
|
243
|
+
'',
|
|
244
|
+
'IF YOU OWN THE DIRECTORY — the usual workspace, whether on the system drive or a data volume:',
|
|
245
|
+
' one unelevated line, then run the command again:',
|
|
246
|
+
` PowerShell: icacls "${path}" /grant "$env:USERNAME:(OI)(CI)(WO)"`,
|
|
247
|
+
` cmd: icacls "${path}" /grant "%USERNAME%:(OI)(CI)(WO)"`,
|
|
248
|
+
'WRITE_OWNER is exactly the right the prerequisite names, so this grants nothing the harness did not ask for,',
|
|
249
|
+
'and (OI)(CI) makes the ACE inheritable, so one command reaches the workspace\'s existing subdirectories.',
|
|
250
|
+
'Full control works just as well — the same line with `F` in place of `(WO)`:',
|
|
251
|
+
` icacls "${path}" /grant "$env:USERNAME:(OI)(CI)F"`,
|
|
252
|
+
'Why `(WO)` is the whole of what is missing there: an owner holds READ_CONTROL and WRITE_DAC implicitly,',
|
|
253
|
+
'and WRITE_DAC is what `icacls /grant` itself needs — so the DACL half of the merged write already has what',
|
|
254
|
+
'it wants, and WRITE_OWNER is the single missing piece.',
|
|
181
255
|
'',
|
|
182
|
-
'
|
|
183
|
-
`
|
|
184
|
-
|
|
256
|
+
'IF YOU DO NOT OWN IT — a directory an installer or another account created, e.g. owner',
|
|
257
|
+
'`BUILTIN\\Administrators`:',
|
|
258
|
+
' the line above cannot run at all. Changing a DACL takes WRITE_DAC, which you hold neither as owner nor',
|
|
259
|
+
' through any ACE, so `icacls /grant` is refused with `Access is denied` — for the very command that would',
|
|
260
|
+
' fix it. The merged write wants WRITE_DAC and WRITE_OWNER together, so `(WO)` alone would not be enough',
|
|
261
|
+
' here even if it went through. Run the grant once from an account that already holds both — that is, from an',
|
|
262
|
+
' ELEVATED prompt:',
|
|
263
|
+
` icacls "${path}" /grant "<your-account>:(OI)(CI)F"`,
|
|
264
|
+
'Full control is used because it is the rights set covering both halves; the other reach both reports name is',
|
|
265
|
+
'to take ownership first, which also needs elevation (it wants SeTakeOwnership), after which the unelevated',
|
|
266
|
+
'`(WO)` line above applies:',
|
|
267
|
+
` icacls "${path}" /setowner "<your-account>"`,
|
|
268
|
+
'Or sidestep the ACL entirely: create the workspace under `%USERPROFILE%` — a directory created there',
|
|
269
|
+
'inherits Full control for you — and open the session on that one.',
|
|
185
270
|
'',
|
|
186
271
|
...nonFixes(failure, path),
|
|
187
272
|
'How to read this: the harness documents the prerequisite (' + PREREQUISITE + ') and this',
|
|
@@ -266,7 +351,7 @@ export function denialText(failure, observed, denial, maxDenials) {
|
|
|
266
351
|
'',
|
|
267
352
|
'Retrying cannot succeed — the sandbox cannot start a command until the directory grant applies.',
|
|
268
353
|
'Stop, and either apply the fix or hand the problem to the user:',
|
|
269
|
-
failure.path === undefined ? '' : ` icacls "${failure.path}" /grant "$env:USERNAME:(OI)(CI)
|
|
354
|
+
failure.path === undefined ? '' : ` icacls "${failure.path}" /grant "$env:USERNAME:(OI)(CI)(WO)"`,
|
|
270
355
|
'',
|
|
271
356
|
suffix > 0
|
|
272
357
|
? `This is automatic block ${String(denial)} of ${String(maxDenials)}; after that the call is allowed again.`
|
package/lib/signature.js
CHANGED
|
@@ -30,6 +30,36 @@
|
|
|
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 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
|
|
59
|
+
* (`packages/sandbox/sandbox-windows-acl/src/acl.ts`, flag
|
|
60
|
+
* `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`), so on an older line
|
|
61
|
+
* the same string belongs to a different cause space.
|
|
62
|
+
*
|
|
33
63
|
* ## The persistent-shell startup failure (`pty-startup`)
|
|
34
64
|
*
|
|
35
65
|
* `dsh-terminal-bash` throws `PTY shell exited during startup` when the shell
|
package/lib/types/advice.d.ts
CHANGED
|
@@ -13,6 +13,31 @@
|
|
|
13
13
|
* the caller can grant itself with `icacls`, unelevated. It is **not**
|
|
14
14
|
* `SeSecurityPrivilege`, the token privilege the reports naturally reach for;
|
|
15
15
|
* `whoami /priv` cannot show the difference, and elevation is the wrong lever.
|
|
16
|
+
* The remedy is the **narrowest** form of that grant — `(WO)` alone, which is
|
|
17
|
+
* literally the right the backend's prerequisite names — with Full control
|
|
18
|
+
* offered as the broad alternative; and the advisory carries the **version
|
|
19
|
+
* boundary** the label introduced (`0.1.7-alpha.1`), because that is what
|
|
20
|
+
* separates "this is the label failure" from "this is something else", and
|
|
21
|
+
* because rolling back is the reach it invites while making the very problem
|
|
22
|
+
* it closed come back.
|
|
23
|
+
*
|
|
24
|
+
* **The remedy is forked on ownership, because one command cannot serve both
|
|
25
|
+
* environments.** The reports split into two rights situations behind an
|
|
26
|
+
* identical error: a workspace **the caller owns** (`#7622`, `#7646`, `#7720`,
|
|
27
|
+
* `#7750`, `#7804`), where the owner's implicit `WRITE_DAC` satisfies the DACL
|
|
28
|
+
* half and `(WO)` is the whole of what is missing — so the `icacls /grant`
|
|
29
|
+
* that supplies it *can itself run*, unelevated; and a directory **the caller
|
|
30
|
+
* does not own** (`#7771`: owner `BUILTIN\Administrators`, held deny-only for
|
|
31
|
+
* their token), where `WRITE_DAC` is missing too, so `icacls /grant` is denied
|
|
32
|
+
* for the very command that would fix it, and `(WO)` alone would not be enough
|
|
33
|
+
* even if it went through. The classifier cannot tell these apart — the text is
|
|
34
|
+
* identical — so the advisory does what it can do instead of guessing: it hands
|
|
35
|
+
* over the **ownership check** (`(Get-Acl "<dir>").Owner`) as the branch
|
|
36
|
+
* selector, then gives each branch the command that actually works there, and
|
|
37
|
+
* says why the other branch's command is not a fallback. A single unconditional
|
|
38
|
+
* one-liner would send the second environment to a command that is refused
|
|
39
|
+
* before it runs — the same defect this module exists to answer, a remedy that
|
|
40
|
+
* does not work delivered confidently.
|
|
16
41
|
* - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
|
|
17
42
|
* caller — least of all by the model, which has no shell to run anything in.
|
|
18
43
|
* So its advice says so and stops: the remedy is a user-side preset choice,
|
|
@@ -34,7 +59,7 @@
|
|
|
34
59
|
import type { ProvisioningFailure, RecognizedFailure } from './signature.js';
|
|
35
60
|
import type { SandboxModeName } from './mode.js';
|
|
36
61
|
/** The upstream threads the ACL advisory is a stopgap for. */
|
|
37
|
-
export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720";
|
|
62
|
+
export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735 / #7771 / #7804 / #7816";
|
|
38
63
|
/** The upstream thread the persistent-shell advisory is a stopgap for. */
|
|
39
64
|
export declare const PTY_DISCUSSIONS = "#7638";
|
|
40
65
|
/** The documented prerequisite, quoted from the backend's README. */
|
package/lib/types/signature.d.ts
CHANGED
|
@@ -30,6 +30,36 @@
|
|
|
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 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
|
|
59
|
+
* (`packages/sandbox/sandbox-windows-acl/src/acl.ts`, flag
|
|
60
|
+
* `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`), so on an older line
|
|
61
|
+
* the same string belongs to a different cause space.
|
|
62
|
+
*
|
|
33
63
|
* ## The persistent-shell startup failure (`pty-startup`)
|
|
34
64
|
*
|
|
35
65
|
* `dsh-terminal-bash` throws `PTY shell exited during startup` when the shell
|
|
@@ -56,7 +86,17 @@
|
|
|
56
86
|
*/
|
|
57
87
|
/** Which provisioning operation failed, and which diagnosis follows from it. */
|
|
58
88
|
export type FailureClass =
|
|
59
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* `SetNamedSecurityInfoW` returned `ERROR_ACCESS_DENIED` (5): the merged
|
|
91
|
+
* DACL + label write was refused. All three environments in the module doc
|
|
92
|
+
* land here — the `mkdir`-inherited Modify workspace, the data-volume
|
|
93
|
+
* directory with no ACE naming the caller, and the directory owned by another
|
|
94
|
+
* account — because the failure text cannot separate them. The first two are
|
|
95
|
+
* the same rights situation (the caller owns it; `WRITE_OWNER` is the whole of
|
|
96
|
+
* what is missing) and share the unelevated remedy; the third is missing
|
|
97
|
+
* `WRITE_DAC` as well, which is why the advisory hands over the ownership
|
|
98
|
+
* check and forks the command on it.
|
|
99
|
+
*/
|
|
60
100
|
'apply-denied'
|
|
61
101
|
/** `SetNamedSecurityInfoW` failed with a Win32 code other than `ERROR_ACCESS_DENIED`. */
|
|
62
102
|
| 'apply-other'
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@argszero/cordis-plugin-sandbox-grant-advisor",
|
|
3
|
-
"description": "Turns two 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 two 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 — 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. The plugin observes the public `tools/post-execute` waterfall, classifies both signatures narrowly (only the two `...NamedSecurityInfoW` operations; the PTY message matched on a whole line, never as a substring, and 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 — the owner's implicit WRITE_DAC already covers the DACL half — 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% — 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 — it never names a shell tool the failing composition does not mount. 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, 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.5.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|