@argszero/cordis-plugin-sandbox-grant-advisor 0.2.0 → 0.4.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,9 +30,22 @@ stuck.
30
30
 
31
31
  ### 1. Workspace provisioning — the Windows ACL failure (`acl-provisioning`)
32
32
 
33
- Three reports describe this exact line: [discussion #7538], [discussion #7622],
34
- [discussion #7646]. In each one every sandboxed command fails the same way,
35
- before it runs, and the error names neither the missing right nor 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.
39
+
40
+ `#7720` is worth reading for where the failure lands: the grant is materialized
41
+ at sandbox **initialization**, so this is not one refused operation but *every*
42
+ shell tool at once — the reporter could not run `netstat` or even `icacls` to
43
+ diagnose the error they were staring at (on `0.1.5-rc.3` the same directory
44
+ worked, because confinement was skipped silently rather than failing closed).
45
+ They also report the two remedies that look right and are not
46
+ (`takeown /F <dir> /R /D Y`, `icacls <dir> /reset /T /C`), which is why the
47
+ advisory names them with the reason each fails instead of leaving the reader to
48
+ discover it.
36
49
 
37
50
  The Windows backend provisions a workspace by writing the directory's DACL and
38
51
  its mandatory-integrity label in **one** `SetNamedSecurityInfoW` call
@@ -55,7 +68,26 @@ Two consequences follow from that one line:
55
68
  is the token-privilege form of the same idea, and it is the hypothesis the
56
69
  reports naturally reach for — `whoami /priv` cannot tell the two apart,
57
70
  because `WRITE_OWNER` is an object right and never appears in that table.
58
- 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.
59
91
 
60
92
  The grant is materialized lazily, on the first confined call, and **nothing is
61
93
  cached when it throws** — so the same failure repeats per command (850 calls
@@ -73,12 +105,24 @@ What was reported:
73
105
  Why it is refused while the directory looks writable: that call is a MERGED write ...
74
106
  ... the label half additionally needs WRITE_OWNER on the directory. ...
75
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
+
76
113
  Confirm the cause (unelevated) — `icacls` is a normal user command:
77
114
  icacls "D:\ws"
78
115
 
79
116
  Fix it (unelevated, one line) and then run the command again:
80
- PowerShell: icacls "D:\ws" /grant "$env:USERNAME:(OI)(CI)F"
81
- cmd: icacls "D:\ws" /grant "%USERNAME%:(OI)(CI)F"
117
+ PowerShell: icacls "D:\ws" /grant "$env:USERNAME:(OI)(CI)(WO)"
118
+ cmd: icacls "D:\ws" /grant "%USERNAME%:(OI)(CI)(WO)"
119
+
120
+ What will NOT fix it — both look like the right move, and both were tried and reported:
121
+ takeown /F "D:\ws" /R /D Y
122
+ makes you the owner, but ownership's implicit rights are READ_CONTROL and WRITE_DAC only.
123
+ The owner does not implicitly hold WRITE_OWNER, which is the right this call needs.
124
+ icacls "D:\ws" /reset /T /C
125
+ restores inheritance, and inheritance is what supplied the Modify-only ACE above.
82
126
  ```
83
127
 
84
128
  ### 2. Persistent shell startup (`pty-startup`)
@@ -277,6 +321,12 @@ than one that stays silent.
277
321
  replace it.** That guard keys on **call identity** (identical arguments
278
322
  retried); this one keys on the **environment signature**, which is how several
279
323
  *different* commands share one cause. Mounting both is sensible.
324
+ - **The plugin cannot see the launcher half of `#7735`.** The Low label's other
325
+ side effect — the shell's publisher confirmation before launching a
326
+ Low-integrity `.bat`/`.cmd`/`.exe` — never appears in a tool result, so it is
327
+ outside the seam this plugin subscribes to. The report and its proposed fix
328
+ stay with the maintainers; all this plugin can do is explain the provisioning
329
+ failure that shares its root.
280
330
  - **The real fix is upstream, in both families.** For the ACL failure,
281
331
  `grantWrite` already computes `hasExactGrant` / `hasExactDeny` /
282
332
  `hasExactLabel` and discards which one was false, so the diagnostic that turns
@@ -307,11 +357,19 @@ composition that does not mount the service degrades to silence instead of
307
357
  failing to load. See `src/mode.ts`.
308
358
 
309
359
  Probed at the newest build of every line the range admits — `0.1.2-rc.1`,
310
- `0.1.3-alpha.2`, `0.1.5-rc.3`, `0.1.6-alpha.2`, `0.1.7-rc.1` (the build the third
311
- report ran) — with `npm run test:probe-lines`, which derives those builds from
312
- this range, installs each one from the registry into a scratch tree and runs the
313
- suite against it. A line whose probe fails is removed from the range rather than
314
- left claimed.
360
+ `0.1.3-alpha.2`, `0.1.5-rc.3`, `0.1.6-alpha.2`, `0.1.7-rc.2` (the newest build of
361
+ the line the later Windows reports ran on) — with `npm run test:probe-lines`,
362
+ which derives those builds from this range, installs each one from the registry
363
+ into a scratch tree and runs the suite against it. `0.1.7-rc.1`, the build the
364
+ third report ran, is admitted by the same `||` segment and was probed while it was
365
+ the newest of that line.
366
+
367
+ The whole set is re-probed whenever this package's source changes rather than
368
+ carried over from an earlier version: the range is a claim about *this* build of
369
+ the plugin, so `0.4.0` re-ran all five lines above. A line whose probe fails is
370
+ removed from the range rather than left claimed. The scratch tree's resolved
371
+ versions are the ones to read back when a probe is quoted as evidence — the probe
372
+ script pins them by exact version, and `--keep` leaves the tree in place to check.
315
373
 
316
374
  ## Development
317
375
 
@@ -338,4 +396,7 @@ the current runtime cannot distinguish rather than counting it as a pass.
338
396
  [discussion #7538]: https://github.com/deepseek-ai/deepseek-harness/discussions/7538
339
397
  [discussion #7622]: https://github.com/deepseek-ai/deepseek-harness/discussions/7622
340
398
  [discussion #7646]: https://github.com/deepseek-ai/deepseek-harness/discussions/7646
399
+ [discussion #7720]: https://github.com/deepseek-ai/deepseek-harness/discussions/7720
400
+ [discussion #7750]: https://github.com/deepseek-ai/deepseek-harness/discussions/7750
401
+ [discussion #7735]: https://github.com/deepseek-ai/deepseek-harness/discussions/7735
341
402
  [discussion #7638]: https://github.com/deepseek-ai/deepseek-harness/discussions/7638
package/lib/advice.js CHANGED
@@ -13,6 +13,13 @@
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.
16
23
  * - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
17
24
  * caller — least of all by the model, which has no shell to run anything in.
18
25
  * So its advice says so and stops: the remedy is a user-side preset choice,
@@ -33,7 +40,7 @@
33
40
  */
34
41
  import { failureLine } from './signature.js';
35
42
  /** The upstream threads the ACL advisory is a stopgap for. */
36
- export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646';
43
+ export const ACL_DISCUSSIONS = '#7538 / #7622 / #7646 / #7720 / #7750 / #7735';
37
44
  /** The upstream thread the persistent-shell advisory is a stopgap for. */
38
45
  export const PTY_DISCUSSIONS = '#7638';
39
46
  /** The documented prerequisite, quoted from the backend's README. */
@@ -57,6 +64,21 @@ export const GLOBAL_PATCH = '$DSH_HOME/cordis.patch.yml';
57
64
  export const MINIMAL_PRESET_ROW = 'preset-minimal';
58
65
  /** The one-shot shell tool the `standard` preset mounts on Windows. */
59
66
  export const ONE_SHOT_SHELL = '@deepseek-ai/dsh-tool-pwsh';
67
+ /**
68
+ * The two remedies that look like the fix and are not.
69
+ *
70
+ * Both were applied by the reporter of `#7720` before finding the one that
71
+ * works, and both are the *natural* reach: making yourself the owner and
72
+ * resetting the directory's ACL are how one normally repairs a Windows
73
+ * permission problem. They fail here for two different reasons, and naming the
74
+ * reason is what makes this section worth its lines — a reader who already
75
+ * tried them learns why, and a reader who has not is spared the attempt. See
76
+ * {@link nonFixes} for when this is emitted.
77
+ */
78
+ export const NOT_FIXES = [
79
+ 'takeown /F "<dir>" /R /D Y',
80
+ 'icacls "<dir>" /reset /T /C',
81
+ ];
60
82
  /** Placeholder the user replaces with the directory the error named. */
61
83
  const PLACEHOLDER = '<the directory from the error line above>';
62
84
  /**
@@ -72,7 +94,10 @@ function diagnosis(failure) {
72
94
  'mandatory-integrity label go out as one `SetNamedSecurityInfoW`. The label lives in the SACL, and the',
73
95
  "owner's implicit rights cover only READ_CONTROL and WRITE_DAC, so the label half additionally needs",
74
96
  'WRITE_OWNER on the directory. A workspace created with `mkdir` normally inherits',
75
- '"Authenticated Users: Modify" (`0x1301bf`) from the drive root — and that mask has neither right.',
97
+ '"Authenticated Users: Modify" (`0x1301bf`) from the drive root — and that mask has neither right; on a data',
98
+ 'volume there may be no ACE naming you at all, so that inherited entry is the whole of your access. The two',
99
+ 'halves go out as one call, so a refused label discards the write grant with it, and the sandbox then',
100
+ 'refuses to start any command in the workspace instead of running it unconfined.',
76
101
  'This is a directory ACL fact, not a token privilege: `whoami /priv` will not show it, and',
77
102
  'SeSecurityPrivilege is the wrong lever here.',
78
103
  ].join('\n');
@@ -91,6 +116,59 @@ function diagnosis(failure) {
91
116
  ].join('\n');
92
117
  }
93
118
  }
119
+ /**
120
+ * The "this is not the fix" lines for one class of failure.
121
+ *
122
+ * Emitted only for `apply-denied`, the class whose whole diagnosis is the
123
+ * missing `WRITE_OWNER` right — because only there is the claim true:
124
+ *
125
+ * - `read-denied` wants `READ_CONTROL`, and taking ownership *does* carry it,
126
+ * so calling `takeown` a non-fix there would be false.
127
+ * - `apply-other` already says the missing-rights story does not apply
128
+ * verbatim, so a section that presupposes it would contradict its own
129
+ * diagnosis.
130
+ *
131
+ * A negative claim still has to be earned: the failure to avoid is advice that
132
+ * is confidently wrong in the other direction.
133
+ * @param failure - the recognized provisioning failure.
134
+ * @param path - the directory the error named, or the placeholder.
135
+ * @returns the section's lines, or an empty array for a class it does not fit.
136
+ */
137
+ function nonFixes(failure, path) {
138
+ if (failure.klass !== 'apply-denied')
139
+ return [];
140
+ return [
141
+ 'What will NOT fix it — both look like the right move, and both were tried and reported:',
142
+ ` takeown /F "${path}" /R /D Y`,
143
+ " makes you the owner, but ownership's implicit rights are READ_CONTROL and WRITE_DAC only.",
144
+ ' The owner does not implicitly hold WRITE_OWNER, which is the right this call needs.',
145
+ ` icacls "${path}" /reset /T /C`,
146
+ ' restores inheritance, and inheritance is what supplied the Modify-only ACE above.',
147
+ '',
148
+ ];
149
+ }
150
+ /**
151
+ * When the label half arrived, and why an older build is not the remedy.
152
+ *
153
+ * Emitted for every class: the boundary is a fact about the package
154
+ * `sandbox-windows-acl` (whose `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`
155
+ * flag is what needs `WRITE_OWNER`), not about which of its two calls failed, so
156
+ * it is true wherever this module is willing to speak at all. It is also the one
157
+ * fact neither report could get from the error: the failure looks identical on a
158
+ * line where the label does not exist yet, and the build that introduced it is
159
+ * the natural thing to reach for and the wrong one to reach for.
160
+ * @returns the section's lines.
161
+ */
162
+ function versionBoundary() {
163
+ return [
164
+ 'A version boundary worth knowing before reaching for an older build: the label half is new to this package.',
165
+ 'Up to `0.1.6-alpha.x` the backend touched the DACL only (flag 4), so a Modify-only workspace provisioned',
166
+ 'fine; `0.1.7-alpha.1` is where the mandatory label — and with it the SACL, flag 20 — arrives. On a',
167
+ '`0.1.6-alpha.x`-or-older line this exact failure therefore belongs to a different cause space, while on any',
168
+ '`0.1.7-*` line it is this one. Rolling back is not the fix either: the label is what confines deletes to the',
169
+ 'workspace, and reverting it reintroduces the escape it closed.',
170
+ ].join('\n');
171
+ }
94
172
  /**
95
173
  * Build the advisory attached to the failing tool result.
96
174
  *
@@ -128,15 +206,27 @@ function aclAdvisory(failure, href) {
128
206
  '',
129
207
  diagnosis(failure),
130
208
  '',
209
+ versionBoundary(),
210
+ '',
131
211
  'Confirm the cause (unelevated) — `icacls` is a normal user command:',
132
212
  ` icacls "${path}"`,
133
- 'Look for an ACE that names YOUR OWN account (run `whoami` if unsure) with (F) / Full control.',
134
- 'If the strongest entry naming you is (M) / Modify, that is this failure.',
213
+ 'Look for an ACE that names YOUR OWN account (run `whoami` if unsure) with (F) / Full control or',
214
+ '(WO) / Write owner. If the strongest entry naming you is (M) / Modify — or no entry names you at all and',
215
+ 'your access comes from an inherited `Authenticated Users:(M)` — that is this failure.',
135
216
  '',
136
217
  'Fix it (unelevated, one line) and then run the command again:',
137
- ` PowerShell: icacls "${path}" /grant "$env:USERNAME:(OI)(CI)F"`,
138
- ` cmd: icacls "${path}" /grant "%USERNAME%:(OI)(CI)F"`,
218
+ ` PowerShell: icacls "${path}" /grant "$env:USERNAME:(OI)(CI)(WO)"`,
219
+ ` cmd: icacls "${path}" /grant "%USERNAME%:(OI)(CI)(WO)"`,
220
+ 'WRITE_OWNER is exactly the right the prerequisite names, so this grants nothing the harness did not ask for,',
221
+ 'and (OI)(CI) makes the ACE inheritable, so one command reaches the workspace\'s existing subdirectories.',
222
+ 'Full control works just as well — the same line with `F` in place of `(WO)`:',
223
+ ` icacls "${path}" /grant "$env:USERNAME:(OI)(CI)F"`,
224
+ 'Both assume you own the directory: owner-implicit rights cover the DACL half of the merged write, so',
225
+ 'WRITE_OWNER is the single missing piece. A directory owned by someone else is a bigger change than a',
226
+ 'one-liner — that is the harness\'s documented prerequisite, and it is why this failure is loud instead of',
227
+ 'silently skipped.',
139
228
  '',
229
+ ...nonFixes(failure, path),
140
230
  'How to read this: the harness documents the prerequisite (' + PREREQUISITE + ') and this',
141
231
  'error does not name it yet, so the advice is delivered here instead. This is a stopgap, ' + where + '.',
142
232
  'What it is NOT: this plugin neither edits ACLs nor elevates — the command above is yours to run.',
@@ -219,7 +309,7 @@ export function denialText(failure, observed, denial, maxDenials) {
219
309
  '',
220
310
  'Retrying cannot succeed — the sandbox cannot start a command until the directory grant applies.',
221
311
  'Stop, and either apply the fix or hand the problem to the user:',
222
- failure.path === undefined ? '' : ` icacls "${failure.path}" /grant "$env:USERNAME:(OI)(CI)F"`,
312
+ failure.path === undefined ? '' : ` icacls "${failure.path}" /grant "$env:USERNAME:(OI)(CI)(WO)"`,
223
313
  '',
224
314
  suffix > 0
225
315
  ? `This is automatic block ${String(denial)} of ${String(maxDenials)}; after that the call is allowed again.`
package/lib/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * ## The two failures it recognizes
6
6
  *
7
- * **Workspace provisioning (Windows ACL).** Three reports of one signature
8
- * (`#7538`, `#7622`, `#7646`) describe the same shape: the host-side write grant
7
+ * **Workspace provisioning (Windows ACL).** Four reports of one signature
8
+ * (`#7538`, `#7622`, `#7646`, `#7720`) describe the same shape: the host-side write grant
9
9
  * for a sandboxed workspace cannot be applied, every sandboxed command then
10
10
  * fails identically **before it runs**, and the error text is a bare Win32 line:
11
11
  *
@@ -18,6 +18,13 @@
18
18
  * caller `WRITE_OWNER` — never reaches the user, so sessions escape into
19
19
  * `danger-full-access` or die on the model's output cap.
20
20
  *
21
+ * `#7720` sharpens where this lands: because the grant is materialized at
22
+ * sandbox *initialization*, that failure takes **every** shell tool with it, not
23
+ * one operation — the reporter could not run `netstat` or `icacls` to diagnose
24
+ * the failure they were looking at. It also contributes the two remedies that
25
+ * look right and are not (`takeown /R /D Y`, `icacls /reset /T /C`), which the
26
+ * ACL advisory now names along with the reason each fails.
27
+ *
21
28
  * **Persistent shell startup (#7638).** With the `minimal` preset on Windows the
22
29
  * only shell tool is a persistent PTY (`dsh-terminal-bash` +
23
30
  * `dsh-tool-pwsh-persistent`), and under a *confining* sandbox mode every call
@@ -59,8 +66,9 @@
59
66
  * failure of a family, the failing tool result is enriched with a user-role
60
67
  * notice. For the ACL family it names the missing right (`WRITE_OWNER` on the
61
68
  * directory, not `SeSecurityPrivilege`), gives the unelevated one-line
62
- * `icacls` remedy, and gives the discriminator that separates a Modify-only
63
- * directory from a wrong prerequisite. For the PTY family it names the
69
+ * `icacls` remedy, gives the discriminator that separates a Modify-only
70
+ * directory from a wrong prerequisite, and names the two remedies that look
71
+ * right and are not (`takeown`, `icacls /reset`), each with its reason. For the PTY family it names the
64
72
  * combination that fails (persistent PTY × a confining mode), states the
65
73
  * resolved mode, says plainly that no command can fix it, and hands the
66
74
  * user-side preset choice over. Both ride `additionalContexts`, so the model
package/lib/signature.js CHANGED
@@ -30,6 +30,21 @@
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 two environments, and the text is identical in both, so
34
+ * the classifier cannot and must not try to tell them apart: a workspace the
35
+ * caller created with `mkdir` that inherits "Authenticated Users: Modify" from
36
+ * the drive root (`#7622`, `#7646`, `#7720`), and a directory on a data volume
37
+ * where **no ACE names the caller at all** so that the inherited entry is the
38
+ * whole of their access (`#7750` on D:/E:, `#7735`'s second defect). Both are
39
+ * the same gate — `WRITE_OWNER` on the directory — and the same one-line remedy
40
+ * satisfies both, which is why they share a class and the advisory names both
41
+ * shapes instead of guessing which one it is looking at. What the text *does*
42
+ * carry that the failure does not is the version boundary: the merged
43
+ * DACL + label write exists only from `0.1.7-alpha.1` on
44
+ * (`packages/sandbox/sandbox-windows-acl/src/acl.ts`, flag
45
+ * `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`), so on an older line
46
+ * the same string belongs to a different cause space.
47
+ *
33
48
  * ## The persistent-shell startup failure (`pty-startup`)
34
49
  *
35
50
  * `dsh-terminal-bash` throws `PTY shell exited during startup` when the shell
@@ -13,6 +13,13 @@
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.
16
23
  * - **The persistent-shell failure** (`pty-startup`) is *not* fixable by the
17
24
  * caller — least of all by the model, which has no shell to run anything in.
18
25
  * So its advice says so and stops: the remedy is a user-side preset choice,
@@ -34,7 +41,7 @@
34
41
  import type { ProvisioningFailure, RecognizedFailure } from './signature.js';
35
42
  import type { SandboxModeName } from './mode.js';
36
43
  /** The upstream threads the ACL advisory is a stopgap for. */
37
- export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646";
44
+ export declare const ACL_DISCUSSIONS = "#7538 / #7622 / #7646 / #7720 / #7750 / #7735";
38
45
  /** The upstream thread the persistent-shell advisory is a stopgap for. */
39
46
  export declare const PTY_DISCUSSIONS = "#7638";
40
47
  /** The documented prerequisite, quoted from the backend's README. */
@@ -58,6 +65,18 @@ export declare const GLOBAL_PATCH = "$DSH_HOME/cordis.patch.yml";
58
65
  export declare const MINIMAL_PRESET_ROW = "preset-minimal";
59
66
  /** The one-shot shell tool the `standard` preset mounts on Windows. */
60
67
  export declare const ONE_SHOT_SHELL = "@deepseek-ai/dsh-tool-pwsh";
68
+ /**
69
+ * The two remedies that look like the fix and are not.
70
+ *
71
+ * Both were applied by the reporter of `#7720` before finding the one that
72
+ * works, and both are the *natural* reach: making yourself the owner and
73
+ * resetting the directory's ACL are how one normally repairs a Windows
74
+ * permission problem. They fail here for two different reasons, and naming the
75
+ * reason is what makes this section worth its lines — a reader who already
76
+ * tried them learns why, and a reader who has not is spared the attempt. See
77
+ * {@link nonFixes} for when this is emitted.
78
+ */
79
+ export declare const NOT_FIXES: readonly ["takeown /F \"<dir>\" /R /D Y", "icacls \"<dir>\" /reset /T /C"];
61
80
  /** What the caller knows about the failing call, beyond the failure text. */
62
81
  export interface AdvisoryContext {
63
82
  /** URL quoted in place of the discussions list; optional. */
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * ## The two failures it recognizes
6
6
  *
7
- * **Workspace provisioning (Windows ACL).** Three reports of one signature
8
- * (`#7538`, `#7622`, `#7646`) describe the same shape: the host-side write grant
7
+ * **Workspace provisioning (Windows ACL).** Four reports of one signature
8
+ * (`#7538`, `#7622`, `#7646`, `#7720`) describe the same shape: the host-side write grant
9
9
  * for a sandboxed workspace cannot be applied, every sandboxed command then
10
10
  * fails identically **before it runs**, and the error text is a bare Win32 line:
11
11
  *
@@ -18,6 +18,13 @@
18
18
  * caller `WRITE_OWNER` — never reaches the user, so sessions escape into
19
19
  * `danger-full-access` or die on the model's output cap.
20
20
  *
21
+ * `#7720` sharpens where this lands: because the grant is materialized at
22
+ * sandbox *initialization*, that failure takes **every** shell tool with it, not
23
+ * one operation — the reporter could not run `netstat` or `icacls` to diagnose
24
+ * the failure they were looking at. It also contributes the two remedies that
25
+ * look right and are not (`takeown /R /D Y`, `icacls /reset /T /C`), which the
26
+ * ACL advisory now names along with the reason each fails.
27
+ *
21
28
  * **Persistent shell startup (#7638).** With the `minimal` preset on Windows the
22
29
  * only shell tool is a persistent PTY (`dsh-terminal-bash` +
23
30
  * `dsh-tool-pwsh-persistent`), and under a *confining* sandbox mode every call
@@ -59,8 +66,9 @@
59
66
  * failure of a family, the failing tool result is enriched with a user-role
60
67
  * notice. For the ACL family it names the missing right (`WRITE_OWNER` on the
61
68
  * directory, not `SeSecurityPrivilege`), gives the unelevated one-line
62
- * `icacls` remedy, and gives the discriminator that separates a Modify-only
63
- * directory from a wrong prerequisite. For the PTY family it names the
69
+ * `icacls` remedy, gives the discriminator that separates a Modify-only
70
+ * directory from a wrong prerequisite, and names the two remedies that look
71
+ * right and are not (`takeown`, `icacls /reset`), each with its reason. For the PTY family it names the
64
72
  * combination that fails (persistent PTY × a confining mode), states the
65
73
  * resolved mode, says plainly that no command can fix it, and hands the
66
74
  * user-side preset choice over. Both ride `additionalContexts`, so the model
@@ -30,6 +30,21 @@
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 two environments, and the text is identical in both, so
34
+ * the classifier cannot and must not try to tell them apart: a workspace the
35
+ * caller created with `mkdir` that inherits "Authenticated Users: Modify" from
36
+ * the drive root (`#7622`, `#7646`, `#7720`), and a directory on a data volume
37
+ * where **no ACE names the caller at all** so that the inherited entry is the
38
+ * whole of their access (`#7750` on D:/E:, `#7735`'s second defect). Both are
39
+ * the same gate — `WRITE_OWNER` on the directory — and the same one-line remedy
40
+ * satisfies both, which is why they share a class and the advisory names both
41
+ * shapes instead of guessing which one it is looking at. What the text *does*
42
+ * carry that the failure does not is the version boundary: the merged
43
+ * DACL + label write exists only from `0.1.7-alpha.1` on
44
+ * (`packages/sandbox/sandbox-windows-acl/src/acl.ts`, flag
45
+ * `DACL_SECURITY_INFORMATION | LABEL_SECURITY_INFORMATION`), so on an older line
46
+ * the same string belongs to a different cause space.
47
+ *
33
48
  * ## The persistent-shell startup failure (`pty-startup`)
34
49
  *
35
50
  * `dsh-terminal-bash` throws `PTY shell exited during startup` when the shell
@@ -56,7 +71,13 @@
56
71
  */
57
72
  /** Which provisioning operation failed, and which diagnosis follows from it. */
58
73
  export type FailureClass =
59
- /** `SetNamedSecurityInfoW` returned `ERROR_ACCESS_DENIED` (5): the merged DACL + label write was refused. */
74
+ /**
75
+ * `SetNamedSecurityInfoW` returned `ERROR_ACCESS_DENIED` (5): the merged
76
+ * DACL + label write was refused. Both environments in the module doc land
77
+ * here — the `mkdir`-inherited Modify workspace and the data-volume directory
78
+ * with no ACE naming the caller — because the failure text cannot separate
79
+ * them and the remedy is the same `WRITE_OWNER` grant.
80
+ */
60
81
  'apply-denied'
61
82
  /** `SetNamedSecurityInfoW` failed with a Win32 code other than `ERROR_ACCESS_DENIED`. */
62
83
  | '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: three reports (#7538, #7622, #7646) 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, and the unelevated `icacls` fix; 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.2.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: six reports (#7538, #7622, #7646, #7720, #7750, #7735) 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, and a data volume where no ACE names the caller at all), 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 unelevated `icacls ... :(OI)(CI)(WO)` fix — the narrowest form of the exact right the backend's prerequisite names, with Full control offered second — 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.4.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",