@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 CHANGED
@@ -30,10 +30,12 @@ stuck.
30
30
 
31
31
  ### 1. Workspace provisioning — the Windows ACL failure (`acl-provisioning`)
32
32
 
33
- Four reports describe this exact line: [discussion #7538], [discussion #7622],
34
- [discussion #7646], [discussion #7720]. In each one every sandboxed command fails
35
- the same way, before it runs, and the error names neither the missing right nor
36
- a remedy.
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 Full control to the workspace root needs **no** elevation.
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
- Fix it (unelevated, one line) and then run the command again:
91
- PowerShell: icacls "D:\ws" /grant "$env:USERNAME:(OI)(CI)F"
92
- cmd: icacls "D:\ws" /grant "%USERNAME%:(OI)(CI)F"
93
-
94
- What will NOT fix it — both look like the right move, and both were tried and reported:
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, but ownership's implicit rights are READ_CONTROL and WRITE_DAC only.
97
- The owner does not implicitly hold WRITE_OWNER, which is the right this call needs.
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.1` (the build the third
329
- report ran) — with `npm run test:probe-lines`, which derives those builds from
330
- this range, installs each one from the registry into a scratch tree and runs the
331
- suite against it. A line whose probe fails is removed from the range rather than
332
- left claimed.
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 one-line fix below is safe to try; if 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, but ownership's implicit rights are READ_CONTROL and WRITE_DAC only.",
134
- ' The owner does not implicitly hold WRITE_OWNER, which is the right this call needs.',
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, that is this failure.',
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
- 'Fix it (unelevated, one line) and then run the command again:',
183
- ` PowerShell: icacls "${path}" /grant "$env:USERNAME:(OI)(CI)F"`,
184
- ` cmd: icacls "${path}" /grant "%USERNAME%:(OI)(CI)F"`,
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)F"`,
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
@@ -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. */
@@ -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
- /** `SetNamedSecurityInfoW` returned `ERROR_ACCESS_DENIED` (5): the merged DACL + label write was refused. */
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: four reports (#7538, #7622, #7646, #7720) 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, the discriminator, the unelevated `icacls` fix, 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.3.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",