@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7

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.
Files changed (93) hide show
  1. package/CHANGELOG.en.md +44 -1
  2. package/CHANGELOG.md +44 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +57 -0
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +1 -1
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/contract.d.ts +31 -19
  64. package/lib/types/dialects.d.ts +99 -3
  65. package/lib/types/fs.d.ts +32 -10
  66. package/lib/types/index.d.ts +7 -2
  67. package/lib/types/process-fence.d.ts +54 -0
  68. package/lib/types/profile-isolation.d.ts +178 -0
  69. package/lib/types/providers.d.ts +70 -0
  70. package/lib/types/registry.d.ts +132 -31
  71. package/lib/types/roots.d.ts +100 -13
  72. package/lib/types/sandbox.d.ts +59 -3
  73. package/lib/types/scope.d.ts +200 -14
  74. package/lib/types/windows-broker.d.ts +17 -0
  75. package/lib/types/windows-deny.d.ts +1 -0
  76. package/lib/types/windows-profile.d.ts +10 -13
  77. package/lib/types/windows-runner-contract.d.ts +97 -0
  78. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  79. package/lib/windows-runner.js +1256 -13
  80. package/lib/workspace-files.js +138 -3
  81. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  82. package/package.json +84 -62
  83. package/scripts/windows-broker/install.ps1 +89 -0
  84. package/scripts/windows-broker/rights.cs +58 -0
  85. package/scripts/windows-broker/service.cs +323 -0
  86. package/scripts/windows-broker/verify-install.ps1 +80 -0
  87. package/lib/command-CTdNvwt1.js +0 -791
  88. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  89. package/lib/sandbox-BRXl_FpF.js +0 -132
  90. package/lib/types/client/RootFileTree.d.ts +0 -12
  91. package/lib/types/panel-files.d.ts +0 -3
  92. package/lib/windows-profile-CiFsFYIV.js +0 -216
  93. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -0,0 +1,747 @@
1
+ import { a as parseWindowsProfile, i as isWindowsNetworkOrDevicePath, n as assertWindowsDenyCapacity, t as DialectUnrecognizedError } from "./windows-runner-contract-CEfmZzu0.js";
2
+ import { l as isCanonicallyUnder } from "./roots-B-0l8mNG.js";
3
+ import { brandMultiRootProvider } from "./providers.js";
4
+ import { posix } from "node:path";
5
+ import { SandboxUnavailableError, canonicalPath } from "@deepseek-ai/dsh-sandbox";
6
+ import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
7
+ import { fileURLToPath } from "node:url";
8
+ //#region src/compat/sandbox-confine.ts
9
+ /** Whether a value is a thenable, i.e. whether the installed `confine` is async. */
10
+ function isThenable(value) {
11
+ return typeof value?.then === "function";
12
+ }
13
+ /** Map an installed confine result without changing its synchronous shape. */
14
+ function mapConfined(value, map) {
15
+ return isThenable(value) ? value.then(map) : map(value);
16
+ }
17
+ /**
18
+ * Call the upstream `confine` and widen its result, preserving sync/async shape.
19
+ *
20
+ * @param base - the upstream `confine`, already bound to its provider.
21
+ * @param widen - the pure transformation to apply to the upstream result. It
22
+ * runs exactly once per call, on whichever release; it may throw, and the
23
+ * throw surfaces synchronously or as a rejection to match the base shape.
24
+ * @param argv - the caller's argv, forwarded verbatim.
25
+ * @param policy - the per-call policy, forwarded verbatim.
26
+ * @param signal - forwarded when the installed release accepts it.
27
+ * @returns the widened result, in the shape the installed release uses.
28
+ */
29
+ function widenConfined(base, widen, argv, policy, signal) {
30
+ const confined = base(argv, policy, signal);
31
+ return isThenable(confined) ? confined.then(widen) : widen(confined);
32
+ }
33
+ //#endregion
34
+ //#region src/dialects.ts
35
+ /**
36
+ * Dialect adaptation: widening a confined argv with additional workspace roots.
37
+ *
38
+ * The upstream kernel-sandbox provider expresses `workspace-write` as a
39
+ * per-dialect profile built from the SINGLE policy root (plus the platform temp
40
+ * areas). Widening that profile is the one thing this plugin must do outside the
41
+ * filesystem fence, and it is deliberately done by OBSERVATION: the upstream
42
+ * dialect profile builders are unreachable in the published package (only
43
+ * `lib/index.js` and `lib/types/**` ship — see ADR-0002), and
44
+ * `@deepseek-ai/dsh-sandbox-local`'s `runnerArgv` / `landlockLauncher` /
45
+ * `seatbeltExec` members are TS-private, so `confine()` and the `internals` test
46
+ * hook are the ONLY public surface.
47
+ *
48
+ * Every dialect is therefore handled by
49
+ * 1. splitting the wrapped argv at the separator `confine` itself inserted,
50
+ * 2. recognizing the dialect from STRUCTURAL markers in the profile arguments,
51
+ * 3. cloning the grant spelling — and the insertion point — from the arguments
52
+ * the upstream provider actually produced, and
53
+ * 4. failing loudly (never silently confining to the primary root alone) when
54
+ * the shape is not one this module knows.
55
+ *
56
+ * The per-dialect profile shapes this recognizes (verified against dsh
57
+ * `0.1.5-rc.2`, recorded in `docs/reference/multi-root-workspace-research.md`
58
+ * §10) are:
59
+ *
60
+ * ```text
61
+ * seatbelt [sandbox-exec, -p, "<SBPL>" ] SBPL allow-forms
62
+ * bwrap [bwrap, --ro-bind / /, …, --bind <root> <root>]
63
+ * landlock [<launcher>, --ro /, --rw /dev/null, --rw <root>]
64
+ * windows-acl [node, <runner>, --workspace …, --mode <mode>]
65
+ * runnerCommand is spelled exactly like bwrap (the upstream config contract
66
+ * appends bwrap-compatible profile arguments to the operator's runner).
67
+ * ```
68
+ *
69
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/dialects
70
+ */
71
+ /** Profiles carry their target platform's spellings even in cross-OS tests. */
72
+ function profileContains(candidate, root) {
73
+ if (!root.startsWith("/")) return isCanonicallyUnder(candidate, root);
74
+ return candidate === root || candidate.startsWith(root.endsWith("/") ? root : root + "/");
75
+ }
76
+ /** Both containment directions overlap: a deny ancestor covers a grant too. */
77
+ function profileOverlaps(left, right) {
78
+ return profileContains(left, right) || profileContains(right, left);
79
+ }
80
+ /**
81
+ * Every proper ancestor directory of each denied root, deduplicated and sorted
82
+ * shallow → deep, minus the filesystem root and the roots themselves.
83
+ *
84
+ * A `(subpath R)` deny or a `--ro-bind R` pins a DIRECTORY, not a path: the
85
+ * kernel denies writes beneath it but says nothing about the ancestors, so
86
+ * `rename`ing an ancestor moves the whole tree out from under its deny while
87
+ * every profile (and the fs fence's recorded `recordedPath`) still names the
88
+ * old spelling. Denying/binding each ancestor itself is what keeps the
89
+ * boundary attached to the directory the operator chose.
90
+ *
91
+ * Profiles carry target-platform spellings and Seatbelt/bwrap are always
92
+ * POSIX, so the walk is posix `dirname`; a non-POSIX spelling contributes no
93
+ * ancestors (`dirname('C:\\x')` is `.`).
94
+ * @param roots - canonical denied roots.
95
+ * @returns the ancestor directories, shallowest first.
96
+ */
97
+ function denyAncestors(roots) {
98
+ const denied = new Set(roots);
99
+ const ancestors = /* @__PURE__ */ new Set();
100
+ for (const root of roots) {
101
+ let ancestor = posix.dirname(root.replace(/\/+$/, "") || "/");
102
+ while (posix.dirname(ancestor) !== ancestor && !ancestors.has(ancestor)) {
103
+ ancestors.add(ancestor);
104
+ ancestor = posix.dirname(ancestor);
105
+ }
106
+ }
107
+ return [...ancestors].filter((ancestor) => !denied.has(ancestor)).sort((left, right) => left.split("/").length - right.split("/").length || (left < right ? -1 : 1));
108
+ }
109
+ /** The one SBPL form that carries the writable subpath grants. */
110
+ const SBPL_SUBPATH_FORM = /\(allow file-write\*\s*(?:\(subpath\s+"(?:[^"\\]|\\.)*"\)\s*)+\)/;
111
+ /** One SBPL `(subpath "…")` token inside that form. */
112
+ const SBPL_SUBPATH_TOKEN = /\(subpath\s+("(?:[^"\\]|\\.)*")\)/g;
113
+ /**
114
+ * Split a `confine()` result at the separator by arithmetic, then VERIFY the
115
+ * split: `confine` builds `[...profileArgs, '--', ...argv]`, so the separator
116
+ * sits exactly one element before the caller's argv. A mismatch means the
117
+ * provider no longer wraps argv the way this module assumes.
118
+ * @param confinedArgv - the argv `confine` returned.
119
+ * @param commandArgv - the argv the caller passed to `confine`.
120
+ * @returns the recognized dialect and the split parts.
121
+ */
122
+ function splitConfined(confinedArgv, commandArgv) {
123
+ const separator = confinedArgv.length - commandArgv.length - 1;
124
+ if (separator < 1 || confinedArgv[separator] !== "--") throw new DialectUnrecognizedError(`confined argv is not "[...profile, --, ...argv]" (separator expected at ${separator}, got ${JSON.stringify(confinedArgv[separator])})`);
125
+ const commandArgs = confinedArgv.slice(separator + 1);
126
+ if (commandArgs.length !== commandArgv.length || commandArgs.some((argument, index) => argument !== commandArgv[index])) throw new DialectUnrecognizedError("confined argv does not end with the caller's argv verbatim");
127
+ const profileArgs = confinedArgv.slice(0, separator);
128
+ return {
129
+ dialect: detectDialect(profileArgs),
130
+ profileArgs,
131
+ separator,
132
+ commandArgs
133
+ };
134
+ }
135
+ /**
136
+ * Recognize the runner dialect from the profile arguments alone. Exactly one
137
+ * marker must match; zero or several are both unrecognized.
138
+ * @param profileArgs - everything before the separator.
139
+ * @returns the dialect.
140
+ */
141
+ function detectDialect(profileArgs) {
142
+ const matches = [];
143
+ if (profileArgs.length >= 2 && profileArgs[profileArgs.length - 2] === "-p") matches.push("seatbelt");
144
+ if (profileArgs.includes("--mode")) matches.push("windows-acl");
145
+ if (profileArgs.includes("--rw")) matches.push("landlock");
146
+ if (profileArgs.includes("--ro-bind")) matches.push("bwrap");
147
+ const [first, ...rest] = matches;
148
+ if (first === void 0) throw new DialectUnrecognizedError(`unrecognized sandbox profile arguments: ${JSON.stringify(profileArgs)}`);
149
+ if (rest.length > 0) throw new DialectUnrecognizedError(`ambiguous sandbox profile arguments (${matches.join(", ")}): ${JSON.stringify(profileArgs)}`);
150
+ return first;
151
+ }
152
+ /**
153
+ * Widen one dialect's profile arguments with the additional roots.
154
+ *
155
+ * Only `workspace-write` grants roots, mirroring the upstream mode meaning and
156
+ * the filesystem fence: `read-only` grants nothing, and `danger-full-access`
157
+ * never confines at all (bash and the PTY backend skip `confine` in that mode).
158
+ *
159
+ * A root the dialect ALREADY grants is not granted twice — that keeps this
160
+ * function aligned with the fence's own deduplication and, on bwrap, avoids
161
+ * binding the real `/tmp` over the ephemeral `--tmpfs /tmp` mount.
162
+ * @param dialect - the recognized dialect.
163
+ * @param profileArgs - the profile arguments to widen.
164
+ * @param policy - the per-call policy (its root is the policy root, never a new grant).
165
+ * @param additionalRoots - canonical additional roots, in scope order.
166
+ * @returns the widened profile arguments.
167
+ */
168
+ function widenProfileArgs(dialect, profileArgs, policy, additionalRoots) {
169
+ if (policy.mode !== "workspace-write") throw new DialectUnrecognizedError(`dialect "${dialect}" grants write roots only under workspace-write, got mode "${policy.mode}"`);
170
+ if (additionalRoots.length === 0) return [...profileArgs];
171
+ switch (dialect) {
172
+ case "seatbelt": return widenSeatbelt(profileArgs, additionalRoots);
173
+ case "bwrap": return widenBwrap(profileArgs, policy.workspaceRoot, additionalRoots);
174
+ case "landlock": return widenLandlock(profileArgs, policy.workspaceRoot, additionalRoots);
175
+ case "windows-acl": throw new DialectUnrecognizedError("Windows ACL widening requires widenWindowsProfile");
176
+ }
177
+ }
178
+ /**
179
+ * Graft writable grants onto a `read-only` profile — the B9 cell where a fixed
180
+ * `read-write` additional root stays writable while the session mode denies
181
+ * every other write. Each dialect appends its own grant spelling AFTER
182
+ * upstream's deny-all baseline, so the appended form wins:
183
+ *
184
+ * - seatbelt: SBPL is last-match-wins — a trailing `(allow file-write*
185
+ * (subpath …))` overrides the blanket `(deny file-write*)` for those roots;
186
+ * - bwrap: mounts apply in order — a `--bind W W` after `--ro-bind / /`
187
+ * re-mounts that subtree writable inside the read-only namespace;
188
+ * - landlock: grants union — an extra `--rw W` widens exactly that subtree.
189
+ *
190
+ * `windows-acl` cannot be spelled this way and goes through
191
+ * `widenWindowsProfile` (the standalone runner grants only the listed roots).
192
+ * @param dialect - the recognized dialect.
193
+ * @param profileArgs - the upstream read-only profile arguments.
194
+ * @param additionalRoots - canonical roots the effective scope keeps writable.
195
+ * @returns the grafted profile arguments.
196
+ */
197
+ function graftProfileArgs(dialect, profileArgs, additionalRoots) {
198
+ if (additionalRoots.length === 0) return [...profileArgs];
199
+ switch (dialect) {
200
+ case "seatbelt": return graftSeatbelt(profileArgs, additionalRoots);
201
+ case "bwrap": return [...profileArgs, ...additionalRoots.flatMap((root) => [
202
+ "--bind",
203
+ root,
204
+ root
205
+ ])];
206
+ case "landlock": {
207
+ const granted = /* @__PURE__ */ new Set();
208
+ for (let index = 0; index + 1 < profileArgs.length; index += 1) if (profileArgs[index] === "--rw") granted.add(profileArgs[index + 1]);
209
+ const added = additionalRoots.filter((root) => !granted.has(root)).flatMap((root) => ["--rw", root]);
210
+ return [...profileArgs, ...added];
211
+ }
212
+ case "windows-acl": throw new DialectUnrecognizedError("Windows ACL read-only grafting requires widenWindowsProfile");
213
+ }
214
+ }
215
+ /** Append a trailing allow form to the SBPL — last-match-wins overrides the blanket deny. */
216
+ function graftSeatbelt(profileArgs, additionalRoots) {
217
+ const profile = profileArgs[profileArgs.length - 1];
218
+ if (profile === void 0 || profileArgs[profileArgs.length - 2] !== "-p") throw new DialectUnrecognizedError(`seatbelt profile is not "[<sandbox-exec>, …, -p, <SBPL>]": ${JSON.stringify(profileArgs)}`);
219
+ const granted = new Set([...profile.matchAll(SBPL_SUBPATH_TOKEN)].map((token) => sbplValue(token[1])));
220
+ const added = additionalRoots.filter((root) => !granted.has(root));
221
+ if (added.length === 0) return [...profileArgs];
222
+ const widened = profile + ` (allow file-write* ${added.map((root) => `(subpath ${sbplString(root)})`).join(" ")})`;
223
+ return [...profileArgs.slice(0, -1), widened];
224
+ }
225
+ /** Quote one path exactly the way the upstream Seatbelt profile builder does. */
226
+ function sbplString(path) {
227
+ return `"${path.replaceAll("\\", String.raw`\\`).replaceAll("\"", String.raw`\"`)}"`;
228
+ }
229
+ /** Unquote one SBPL string literal (including its surrounding quotes). */
230
+ function sbplValue(literal) {
231
+ return literal.slice(1, -1).replaceAll(String.raw`\"`, "\"").replaceAll(String.raw`\\`, "\\");
232
+ }
233
+ /** Append the additional roots to the SBPL form that carries the subpath grants. */
234
+ function widenSeatbelt(profileArgs, additionalRoots) {
235
+ const profile = profileArgs[profileArgs.length - 1];
236
+ if (profile === void 0 || profileArgs[profileArgs.length - 2] !== "-p") throw new DialectUnrecognizedError(`seatbelt profile is not "[<sandbox-exec>, …, -p, <SBPL>]": ${JSON.stringify(profileArgs)}`);
237
+ const form = SBPL_SUBPATH_FORM.exec(profile);
238
+ if (form === null) throw new DialectUnrecognizedError(`seatbelt profile has no "(allow file-write* (subpath …))" form to extend: ${JSON.stringify(profile)}`);
239
+ const matched = form[0];
240
+ const granted = new Set([...matched.matchAll(SBPL_SUBPATH_TOKEN)].map((token) => sbplValue(token[1])));
241
+ const added = additionalRoots.filter((root) => !granted.has(root)).map((root) => `(subpath ${sbplString(root)})`);
242
+ if (added.length === 0) return [...profileArgs];
243
+ const widened = profile.slice(0, form.index) + matched.slice(0, -1) + ` ${added.join(" ")})` + profile.slice(form.index + matched.length);
244
+ return [...profileArgs.slice(0, -1), widened];
245
+ }
246
+ /**
247
+ * The observed bwrap bind flag: the flag of the `[flag, <root>, <root>]` triple
248
+ * upstream built for the policy root.
249
+ */
250
+ function bwrapBindFlag(profileArgs, workspaceRoot) {
251
+ for (let index = 2; index < profileArgs.length; index += 1) {
252
+ if (profileArgs[index] !== workspaceRoot || profileArgs[index - 1] !== workspaceRoot) continue;
253
+ const flag = profileArgs[index - 2];
254
+ if (flag === void 0 || !flag.startsWith("--") || flag.startsWith("--ro")) continue;
255
+ return flag;
256
+ }
257
+ throw new DialectUnrecognizedError(`bwrap profile has no writable bind of the policy root ${JSON.stringify(workspaceRoot)}: ${JSON.stringify(profileArgs)}`);
258
+ }
259
+ /** Append the additional roots as binds, after upstream's own root bind. */
260
+ function widenBwrap(profileArgs, workspaceRoot, additionalRoots) {
261
+ const flag = bwrapBindFlag(profileArgs, workspaceRoot);
262
+ const granted = /* @__PURE__ */ new Set();
263
+ for (let index = 0; index + 2 < profileArgs.length; index += 1) if (profileArgs[index] === flag && profileArgs[index + 1] === profileArgs[index + 2]) granted.add(profileArgs[index + 2]);
264
+ for (let index = 0; index + 1 < profileArgs.length; index += 1) if (profileArgs[index] === "--tmpfs") granted.add(profileArgs[index + 1]);
265
+ const added = additionalRoots.filter((root) => !granted.has(root)).flatMap((root) => [
266
+ flag,
267
+ root,
268
+ root
269
+ ]);
270
+ return [...profileArgs, ...added];
271
+ }
272
+ /**
273
+ * The observed Landlock read-write flag: the flag preceding the policy root in
274
+ * the launcher's grant arguments.
275
+ */
276
+ function landlockReadWriteFlag(profileArgs, workspaceRoot) {
277
+ for (let index = 1; index < profileArgs.length; index += 1) {
278
+ if (profileArgs[index] !== workspaceRoot) continue;
279
+ const flag = profileArgs[index - 1];
280
+ if (flag === void 0 || !flag.startsWith("--") || flag.startsWith("--ro")) continue;
281
+ return flag;
282
+ }
283
+ throw new DialectUnrecognizedError(`landlock profile has no read-write grant of the policy root ${JSON.stringify(workspaceRoot)}: ${JSON.stringify(profileArgs)}`);
284
+ }
285
+ /** Append the additional roots as read-write grants, after upstream's own root grant. */
286
+ function widenLandlock(profileArgs, workspaceRoot, additionalRoots) {
287
+ const flag = landlockReadWriteFlag(profileArgs, workspaceRoot);
288
+ const granted = /* @__PURE__ */ new Set();
289
+ for (let index = 0; index + 1 < profileArgs.length; index += 1) if (profileArgs[index] === flag) granted.add(profileArgs[index + 1]);
290
+ const added = additionalRoots.filter((root) => !granted.has(root)).flatMap((root) => [flag, root]);
291
+ return [...profileArgs, ...added];
292
+ }
293
+ /**
294
+ * The bwrap mount points a workspace-write profile makes writable, split by
295
+ * mechanism: `binds` are the `[flag, X, X]` triples whose flag is not `--ro*`
296
+ * (a host directory stays reachable and writable), `tmpfs` are the `--tmpfs`
297
+ * targets (a fresh private filesystem that hides whatever the host has there).
298
+ * The distinction matters for the ancestor pinning in {@link denyProfileArgs}:
299
+ * a directory inside a tmpfs is renamable only within the namespace and never
300
+ * touches the host tree — while self-binding it would expose and make writable
301
+ * the host content the tmpfs exists to hide.
302
+ * @returns the writable bind targets and the tmpfs targets, in profile order.
303
+ */
304
+ function bwrapWritableMounts(profileArgs) {
305
+ const binds = [];
306
+ const tmpfs = [];
307
+ for (let index = 0; index + 2 < profileArgs.length; index += 1) {
308
+ const flag = profileArgs[index];
309
+ if (!flag.startsWith("--") || flag.startsWith("--ro")) continue;
310
+ if (flag === "--tmpfs") {
311
+ tmpfs.push(profileArgs[index + 1]);
312
+ index += 1;
313
+ continue;
314
+ }
315
+ if (profileArgs[index + 1] === profileArgs[index + 2]) binds.push(profileArgs[index + 2]);
316
+ }
317
+ return {
318
+ binds,
319
+ tmpfs
320
+ };
321
+ }
322
+ /** The paths a Landlock profile grants read-write (`--rw` flag values). */
323
+ function landlockWritablePaths(profileArgs) {
324
+ const granted = [];
325
+ for (let index = 0; index + 1 < profileArgs.length; index += 1) if (profileArgs[index] === "--rw") granted.push(profileArgs[index + 1]);
326
+ return granted;
327
+ }
328
+ /**
329
+ * Deny `deniedRoots` inside an upstream `workspace-write` profile — the B9 edge
330
+ * where an explicit `read-only` (or tool-capped) additional root sits under a
331
+ * directory the profile grants writable (the platform temp area, which an
332
+ * additional root may legitimately be registered beneath).
333
+ *
334
+ * A denied root outside every grant needs no profile work at all: the mode
335
+ * already denies it. Per dialect:
336
+ *
337
+ * - seatbelt appends a trailing `(deny file-write* (subpath …) (literal …))`
338
+ * form — last match wins over every earlier allow, and a redundant deny on
339
+ * an ungranted root is harmless. The literals pin every ancestor of a denied
340
+ * root: Seatbelt checks the operated path only, so `rename`ing an ancestor
341
+ * would otherwise move the whole tree out from under its `subpath` deny;
342
+ * - bwrap re-mounts the root `--ro-bind` AFTER the writable mounts (a later
343
+ * mount wins inside the namespace). The source is mandatory: if a denied
344
+ * root disappears, namespace setup fails before the command can recreate it.
345
+ * This also restores read access to denied roots under a `--tmpfs` target.
346
+ * Ancestors of a denied root that lie strictly inside a writable BIND target
347
+ * are self-bound (`--bind A A`) BEFORE the ro-binds: the kernel refuses to
348
+ * rename a mountpoint, so the deny can no longer be walked off by renaming
349
+ * the tree's parent. Ancestors inside a `--tmpfs` target are never bound —
350
+ * a rename there only moves namespace-private mounts, while the self-bind
351
+ * would expose and make writable the host content the tmpfs hides — and
352
+ * ancestors outside every writable target are not renamable anyway;
353
+ * - landlock cannot subtract: a denied root under an `--rw` grant throws —
354
+ * union semantics have no deny primitive;
355
+ * - windows-acl goes through `widenWindowsProfile`, whose runner-side ACL
356
+ * grant cannot subtract either — its caller checks the same overlap. The
357
+ * deny ACE does follow its directory object through a parent rename, but
358
+ * the moved tree's NEW path no longer matches the recorded deny set — so
359
+ * the ambient deny runner pins every ancestor of each deny root with a
360
+ * non-delete-sharing handle for the child lifetime (`windows-path-guard.ts`,
361
+ * `windows-deny.ts`).
362
+ * @param dialect - the recognized dialect.
363
+ * @param profileArgs - the upstream workspace-write profile arguments.
364
+ * @param deniedRoots - canonical roots this call must not write.
365
+ * @returns the profile arguments with the in-profile denies applied.
366
+ */
367
+ function denyProfileArgs(dialect, profileArgs, deniedRoots) {
368
+ if (deniedRoots.length === 0) return [...profileArgs];
369
+ switch (dialect) {
370
+ case "seatbelt": {
371
+ const profile = profileArgs[profileArgs.length - 1];
372
+ if (profile === void 0 || profileArgs[profileArgs.length - 2] !== "-p") throw new DialectUnrecognizedError(`seatbelt profile is not "[<sandbox-exec>, …, -p, <SBPL>]": ${JSON.stringify(profileArgs)}`);
373
+ const granted = [...profile.matchAll(SBPL_SUBPATH_TOKEN)].map((token) => sbplValue(token[1]));
374
+ const covered = deniedRoots.filter((root) => granted.some((target) => profileOverlaps(root, target)));
375
+ if (covered.length === 0) return [...profileArgs];
376
+ const denied = [...covered.map((root) => `(subpath ${sbplString(root)})`), ...denyAncestors(covered).map((ancestor) => `(literal ${sbplString(ancestor)})`)].join(" ");
377
+ return [...profileArgs.slice(0, -1), `${profile} (deny file-write* ${denied})`];
378
+ }
379
+ case "bwrap": {
380
+ const writable = bwrapWritableMounts(profileArgs);
381
+ const covered = deniedRoots.filter((root) => [...writable.binds, ...writable.tmpfs].some((target) => profileOverlaps(root, target)));
382
+ const pins = denyAncestors(covered).filter((ancestor) => writable.binds.some((target) => ancestor !== target && profileContains(ancestor, target)) && !writable.tmpfs.some((target) => profileContains(ancestor, target))).flatMap((ancestor) => [
383
+ "--bind",
384
+ ancestor,
385
+ ancestor
386
+ ]);
387
+ const binds = covered.flatMap((root) => [
388
+ "--ro-bind",
389
+ root,
390
+ root
391
+ ]);
392
+ return [
393
+ ...profileArgs,
394
+ ...pins,
395
+ ...binds
396
+ ];
397
+ }
398
+ case "landlock": {
399
+ const granted = landlockWritablePaths(profileArgs);
400
+ const covered = deniedRoots.filter((root) => granted.some((target) => profileOverlaps(root, target)));
401
+ if (covered.length > 0) throw new DialectUnrecognizedError(`landlock cannot deny [${covered.join(", ")}] inside a writable grant — its launcher only supports allowlist union`);
402
+ return [...profileArgs];
403
+ }
404
+ case "windows-acl": throw new DialectUnrecognizedError("Windows ACL deny overlap is checked by widenWindowsProfile");
405
+ }
406
+ }
407
+ /**
408
+ * Restrict an ambient-writable execution to everything EXCEPT `deniedRoots` —
409
+ * the B9 cell `danger-full-access` + explicit `read-only`. The consumers of
410
+ * this profile (shell, PTY, PTC) bypass `confine()` entirely under that mode,
411
+ * so the process fence builds this profile from an upstream skeleton instead:
412
+ * the caller probes `confine` once under `read-only` to obtain the runner's
413
+ * argument prefix, and this function rewrites the file-effect part.
414
+ *
415
+ * - seatbelt: `(version 1) (allow default)` kept from the profile head, plus a
416
+ * trailing `(deny file-write* (subpath …) (literal …))` — allow-default with
417
+ * subpath carve-outs, the inverse of the confined profiles. The literals
418
+ * pin every ancestor of a denied root, so `rename`ing the tree's parent
419
+ * cannot walk it out from under the deny (Seatbelt checks the operated
420
+ * path only — verified natively on macOS);
421
+ * - bwrap: `--ro-bind / /` becomes `--bind / /` (ambient writable namespace),
422
+ * each denied root is re-mounted `--ro-bind` after it, and every ancestor
423
+ * of a denied root is self-bound `--bind A A` between the root mount and the
424
+ * ro-binds — a mountpoint cannot be renamed (EBUSY), so the deny follows no
425
+ * rename of the tree's ancestors. A missing source fails the launch,
426
+ * retaining the registered negative authorization;
427
+ * - landlock cannot express "everything except" — its launcher only unions
428
+ * allowlists, so it throws. windows-acl expresses the cell through the
429
+ * standalone ambient-deny runner instead of this profile rewrite: standing
430
+ * deny ACEs on every denied object, plus ancestor pin handles held through
431
+ * the child and its drained Job descendants so no rename can vacate the
432
+ * registered spelling (`windows-deny.ts`, `windows-path-guard.ts`).
433
+ * @param dialect - the recognized dialect.
434
+ * @param profileArgs - the upstream read-only skeleton's profile arguments.
435
+ * @param deniedRoots - canonical roots the execution must not write.
436
+ * @returns the restricted profile arguments.
437
+ */
438
+ function restrictProfileArgs(dialect, profileArgs, deniedRoots) {
439
+ if (deniedRoots.length === 0) return [...profileArgs];
440
+ switch (dialect) {
441
+ case "seatbelt": {
442
+ const profile = profileArgs[profileArgs.length - 1];
443
+ if (profile === void 0 || profileArgs[profileArgs.length - 2] !== "-p") throw new DialectUnrecognizedError(`seatbelt profile is not "[<sandbox-exec>, …, -p, <SBPL>]": ${JSON.stringify(profileArgs)}`);
444
+ const anchor = profile.indexOf("(allow default)");
445
+ if (anchor < 0) throw new DialectUnrecognizedError(`seatbelt profile has no "(allow default)" to restrict from: ${JSON.stringify(profile)}`);
446
+ const denied = [...deniedRoots.map((root) => `(subpath ${sbplString(root)})`), ...denyAncestors(deniedRoots).map((ancestor) => `(literal ${sbplString(ancestor)})`)].join(" ");
447
+ const restricted = `${profile.slice(0, anchor + 15)} (deny file-write* ${denied})`;
448
+ return [...profileArgs.slice(0, -1), restricted];
449
+ }
450
+ case "bwrap": {
451
+ const base = profileArgs.indexOf("--ro-bind");
452
+ if (base < 1 || profileArgs[base + 1] !== "/" || profileArgs[base + 2] !== "/") throw new DialectUnrecognizedError("bwrap ambient profile requires an observed root mount");
453
+ const args = [
454
+ ...profileArgs.slice(0, base),
455
+ "--bind",
456
+ "/",
457
+ "/",
458
+ "--die-with-parent"
459
+ ];
460
+ for (const ancestor of denyAncestors(deniedRoots)) args.push("--bind", ancestor, ancestor);
461
+ for (const root of deniedRoots) args.push("--ro-bind", root, root);
462
+ return args;
463
+ }
464
+ case "landlock": return [
465
+ "bwrap",
466
+ "--bind",
467
+ "/",
468
+ "/",
469
+ "--die-with-parent",
470
+ ...denyAncestors(deniedRoots).flatMap((ancestor) => [
471
+ "--bind",
472
+ ancestor,
473
+ ancestor
474
+ ]),
475
+ ...deniedRoots.flatMap((root) => [
476
+ "--ro-bind",
477
+ root,
478
+ root
479
+ ])
480
+ ];
481
+ case "windows-acl": throw new DialectUnrecognizedError("Windows ACL cannot express an ambient grant minus subtree denies — the restricted-token model only grants");
482
+ }
483
+ }
484
+ //#endregion
485
+ //#region src/windows-profile.ts
486
+ /** Windows runner argv and capability identity. No Win32 calls at module load. */
487
+ /** Recognize the upstream runner, then select our ambient deny-token path.
488
+ *
489
+ * `protectedHome` is a diagnostics label only: when it names one of the deny
490
+ * roots it is forwarded as `--protected-home` so the runner can word its
491
+ * hardlink/scan remediation for the protected `$DSH_HOME`. It never changes
492
+ * which roots are denied or how.
493
+ */
494
+ function restrictWindowsProfile(profile, deniedRoots, protectedHome) {
495
+ assertWindowsDenyCapacity(deniedRoots.length);
496
+ if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
497
+ const parsed = parseWindowsProfile(profile.slice(2));
498
+ if (deniedRoots.length === 0) throw new DialectUnrecognizedError("Windows ambient restriction requires at least one deny root");
499
+ for (const root of [parsed.workspace, ...deniedRoots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
500
+ const label = protectedHome !== void 0 && deniedRoots.some((root) => root.toLowerCase() === protectedHome.toLowerCase()) ? ["--protected-home", protectedHome] : [];
501
+ return [
502
+ profile[0],
503
+ fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
504
+ "--upstream-runner",
505
+ profile[1],
506
+ "--workspace",
507
+ parsed.workspace,
508
+ "--temp",
509
+ parsed.temp,
510
+ "--mode",
511
+ "danger-full-access",
512
+ ...deniedRoots.flatMap((root) => ["--deny-root", root]),
513
+ ...label
514
+ ];
515
+ }
516
+ function widenWindowsProfile(profile, roots, deniedRoots, primaryRoot) {
517
+ if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
518
+ const parsed = parseWindowsProfile(profile.slice(2));
519
+ for (const root of [parsed.workspace, ...roots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
520
+ if (primaryRoot !== void 0 && canonicalPath(parsed.workspace) !== primaryRoot) throw new DialectUnrecognizedError("Windows ACL profile workspace does not match the policy root");
521
+ {
522
+ const granted = parsed.mode === "workspace-write" ? [
523
+ parsed.workspace,
524
+ parsed.temp,
525
+ ...roots
526
+ ] : roots;
527
+ const covered = deniedRoots.filter((root) => granted.some((target) => windowsContained(root, target) || windowsContained(target, root)));
528
+ if (covered.length > 0) throw new DialectUnrecognizedError(`Windows ACL cannot deny [${covered.join(", ")}] inside a writable grant — the restricted-token model only grants`);
529
+ }
530
+ if (roots.length === 0) return [...profile];
531
+ return [
532
+ profile[0],
533
+ fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
534
+ "--upstream-runner",
535
+ profile[1],
536
+ ...profile.slice(2),
537
+ ...roots.flatMap((root) => ["--additional-root", root])
538
+ ];
539
+ }
540
+ /**
541
+ * Containment over Windows SPELLINGS, not the host OS: the profile this
542
+ * function rewrites may be produced for a Windows host while the check runs
543
+ * in tests on POSIX, so the comparison cannot borrow the host's separator or
544
+ * case rules.
545
+ */
546
+ function windowsContained(candidate, root) {
547
+ const normalize = (path) => path.replaceAll("/", "\\").replace(/\\+$/, "").toLowerCase();
548
+ const target = normalize(candidate);
549
+ const base = normalize(root);
550
+ return target === base || target.startsWith(`${base}\\`);
551
+ }
552
+ //#endregion
553
+ //#region src/sandbox.ts
554
+ /**
555
+ * Local process-sandbox provider over the multi-root scope. Registers as
556
+ * `ctx.sandbox`; the runner chain, probing, denial dialects, and Windows ACL
557
+ * grant mechanics stay the upstream implementation's.
558
+ */
559
+ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
560
+ static inject = [
561
+ "multiRootCompat",
562
+ "sandboxPolicy",
563
+ "multiRootScope"
564
+ ];
565
+ constructor(ctx, config) {
566
+ super(ctx, config);
567
+ brandMultiRootProvider(this, "sandbox");
568
+ }
569
+ /**
570
+ * Wrap `argv` so it executes confined under `policy` on this host, granting
571
+ * the scope's additional roots inside the SAME dialect profile.
572
+ *
573
+ * Under `read-only` nothing is added (the mode denies every write, additional
574
+ * roots included), and `danger-full-access` never reaches here at all: the
575
+ * bash executor and the PTY backend both hand their argv straight to the local
576
+ * runtime in that mode. Widening therefore only ever ADDS the scope's
577
+ * additional roots to an upstream `workspace-write` profile.
578
+ *
579
+ * The return shape is the INSTALLED upstream provider's: synchronous on a
580
+ * release with a synchronous `confine`, a promise on a release with an
581
+ * asynchronous one. `widenConfined` preserves whichever it is — see
582
+ * `./compat/sandbox-confine.ts`.
583
+ * @param argv - the exact argv the caller is about to spawn.
584
+ * @param policy - the file-effect policy this execution runs under.
585
+ * @param signal - cancellation, forwarded to releases that accept it.
586
+ * @returns the argv to spawn instead, plus the selected backend's facts.
587
+ */
588
+ confine(argv, policy, signal) {
589
+ const scope = this.ctx.multiRootScope.resolve(policy);
590
+ if (policy.mode === "workspace-write" && scope.authority === "unavailable") {
591
+ const prefix = this.clampedExecution(policy, scope.deniedRoots, signal);
592
+ return widenConfined(() => prefix, (facts) => ({
593
+ ...facts,
594
+ argv: [...facts.argv, ...argv]
595
+ }), argv, policy, signal);
596
+ }
597
+ const base = super.confine;
598
+ return widenConfined(base.bind(this), (confined) => this.widenForScope(confined, argv, policy, scope), argv, policy, signal);
599
+ }
600
+ /**
601
+ * Graft the scope's effective grants and denies onto one upstream `confine`
602
+ * result.
603
+ *
604
+ * Pure with respect to the upstream call: it only ever reads the scope and
605
+ * rewrites `argv`. `enforcement`, `denialSignatures` and `runnerFailureRules`
606
+ * are passed through untouched, because the bash executor derives its denial
607
+ * and enforcement reporting from them.
608
+ *
609
+ * Mode behavior (B9, R2): `workspace-write` widens the grant with the
610
+ * effective writable roots AND denies any restricted root the upstream
611
+ * grants could reach (a `read-only` root registered under the platform
612
+ * temp). `read-only` grafts the fixed `read-write` roots back onto the
613
+ * deny-all baseline — the three-state matrix's standout cell — and then
614
+ * applies the SAME deny pass: a grafted writable parent must not silently
615
+ * swallow a denied child (Seatbelt appends the deny after the allow, bwrap
616
+ * adds a mandatory `--ro-bind`, Landlock / windows-acl refuse the
617
+ * unrepresentable subtraction). `danger-full-access` never reaches
618
+ * here (consumers bypass `confine`); its deny semantics live in
619
+ * {@link restrictedArgvPrefix}.
620
+ * @param confined - the upstream result for this call.
621
+ * @param argv - the caller's argv, needed to re-split the wrapped form.
622
+ * @param policy - the per-call policy.
623
+ * @param scope - the authorization snapshot taken at dispatch.
624
+ * @returns the upstream result element for element, or one with a rewritten argv.
625
+ */
626
+ widenForScope(confined, argv, policy, scope) {
627
+ if (scope.authority === "unavailable" || policy.mode !== "workspace-write" && policy.mode !== "read-only") return confined;
628
+ const writable = scope.writableRoots;
629
+ const denied = scope.deniedRoots;
630
+ if (writable.length === 0 && (denied.length === 0 || policy.mode === "read-only")) return confined;
631
+ try {
632
+ const shape = splitConfined(confined.argv, argv);
633
+ if (shape.dialect === "windows-acl") return {
634
+ ...confined,
635
+ argv: [
636
+ ...widenWindowsProfile(shape.profileArgs, writable, denied, scope.primaryRoot),
637
+ "--",
638
+ ...argv
639
+ ]
640
+ };
641
+ const widened = policy.mode === "workspace-write" ? widenProfileArgs(shape.dialect, shape.profileArgs, policy, writable) : graftProfileArgs(shape.dialect, shape.profileArgs, writable);
642
+ const profileArgs = denyProfileArgs(shape.dialect, widened, denied);
643
+ return {
644
+ ...confined,
645
+ argv: [
646
+ ...profileArgs,
647
+ "--",
648
+ ...argv
649
+ ]
650
+ };
651
+ } catch (error) {
652
+ if (!(error instanceof DialectUnrecognizedError)) throw error;
653
+ throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot apply the additional workspace roots (writable: [${writable.join(", ")}], denied: [${denied.join(", ")}]) under the primary root ${scope.primaryRoot} — ${error.message}`);
654
+ }
655
+ }
656
+ /**
657
+ * Build the runner-prefix that confines an ambient-writable execution to
658
+ * everything EXCEPT `deniedRoots` — the `danger-full-access` + explicit
659
+ * `read-only` cell, which the shell, PTY and PTC consumers never route
660
+ * through `confine()` at all.
661
+ *
662
+ * The restricted profile does not depend on the callee's argv, so the prefix
663
+ * is computed once per call here and the synchronous `ctx.subprocess.spawn`
664
+ * seam only has to prepend it. The upstream profile builders stay private:
665
+ * this probes `confine` under `read-only` for the runner's argument skeleton
666
+ * and rewrites the file-effect part per dialect. Windows selects the
667
+ * standalone ambient deny-token runner. Landlock cannot express
668
+ * "everything except", so this combination selects bubblewrap; unusable
669
+ * runners fail without spawning an unconfined command.
670
+ *
671
+ * The return preserves the installed `confine` shape: a synchronous result
672
+ * on a synchronous release, a promise on an asynchronous one.
673
+ * @param policy - the caller's policy (mode must be `danger-full-access`).
674
+ * @param deniedRoots - canonical roots this execution must not write.
675
+ * @param signal - cancellation, forwarded to releases that accept it.
676
+ * @param protectedHome - diagnostics label: which deny root is the
677
+ * protected `$DSH_HOME`. Forwarded to the Windows runner only when it is
678
+ * a member of `deniedRoots`; it never changes authorization.
679
+ * @returns the argv prefix, ending at the `--` separator.
680
+ */
681
+ restrictedArgvPrefix(policy, deniedRoots, signal, protectedHome) {
682
+ return mapConfined(this.restrictedExecution(policy, deniedRoots, signal, protectedHome), (confined) => confined.argv);
683
+ }
684
+ /** Full-access restriction and the actual selected backend's diagnostics. */
685
+ restrictedExecution(policy, deniedRoots, signal, protectedHome) {
686
+ const probePolicy = {
687
+ mode: "read-only",
688
+ workspaceRoot: policy.workspaceRoot,
689
+ ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
690
+ };
691
+ const probe = super.confine.call(this, ["true"], probePolicy, signal);
692
+ const restrict = (confined) => {
693
+ try {
694
+ const shape = splitConfined(confined.argv, ["true"]);
695
+ const argv = [...shape.dialect === "windows-acl" ? restrictWindowsProfile(shape.profileArgs, deniedRoots, protectedHome) : restrictProfileArgs(shape.dialect, shape.profileArgs, deniedRoots), "--"];
696
+ return shape.dialect === "landlock" ? {
697
+ argv,
698
+ enforcement: "full",
699
+ denialSignatures: ["read-only file system"],
700
+ runnerFailureRules: [{ fatalSignatures: ["bwrap: "] }]
701
+ } : {
702
+ ...confined,
703
+ argv
704
+ };
705
+ } catch (error) {
706
+ if (!(error instanceof DialectUnrecognizedError)) throw error;
707
+ throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot confine an ambient execution away from the denied roots [${deniedRoots.join(", ")}] — ${error.message}`);
708
+ }
709
+ };
710
+ return widenConfined(() => probe, restrict, ["true"], probePolicy, signal);
711
+ }
712
+ /**
713
+ * Limit an execution with unknown registry authority to the primary root.
714
+ * Shared temp grants are unsafe: another process may have registered a
715
+ * read-only root beneath them. Harvest the upstream read-only skeleton and
716
+ * graft only the primary root, retaining diagnostics and the sync/async shape.
717
+ * Explicit denies still win; an unrepresentable overlap refuses launch.
718
+ * @param policy - the caller's execution policy.
719
+ * @param deniedRoots - known negative boundaries, including protected home.
720
+ * @param signal - cancellation forwarded to upstream.
721
+ * @returns the confined prefix ending at `--`, without the caller's command.
722
+ */
723
+ clampedExecution(policy, deniedRoots, signal) {
724
+ const probePolicy = {
725
+ mode: "read-only",
726
+ workspaceRoot: policy.workspaceRoot,
727
+ ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
728
+ };
729
+ const probe = super.confine.call(this, ["true"], probePolicy, signal);
730
+ const clamp = (confined) => {
731
+ try {
732
+ const shape = splitConfined(confined.argv, ["true"]);
733
+ const profileArgs = shape.dialect === "windows-acl" ? widenWindowsProfile(shape.profileArgs, [canonicalPath(policy.workspaceRoot)], deniedRoots, canonicalPath(policy.workspaceRoot)) : denyProfileArgs(shape.dialect, graftProfileArgs(shape.dialect, shape.profileArgs, [canonicalPath(policy.workspaceRoot)]), deniedRoots);
734
+ return {
735
+ ...confined,
736
+ argv: [...profileArgs, "--"]
737
+ };
738
+ } catch (error) {
739
+ if (!(error instanceof DialectUnrecognizedError)) throw error;
740
+ throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot limit an execution to the primary root while the registry authority is unavailable — ${error.message}`);
741
+ }
742
+ };
743
+ return widenConfined(() => probe, clamp, ["true"], probePolicy, signal);
744
+ }
745
+ };
746
+ //#endregion
747
+ export { widenConfined as n, MultiRootSandboxProvider as t };