@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,280 @@
1
+ import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ //#region src/compat/process-result.ts
4
+ const object$1 = (value) => typeof value === "object" && value !== null;
5
+ const thenable = (value) => typeof value?.then === "function";
6
+ const textOf = (value) => typeof value === "string" ? value : object$1(value) && typeof value.text === "string" ? value.text : "";
7
+ const stderrOf = (value) => textOf(value.stderr) + (object$1(value.error) ? textOf(value.error.message) : "") + (object$1(value.failure) ? textOf(value.failure.message) : "");
8
+ const exitCodeOf = (value) => typeof value.exitCode === "number" || value.exitCode === null ? value.exitCode : value.ok === false || object$1(value.error) || object$1(value.failure) ? 1 : 0;
9
+ /** Denial and runner-failure verdicts for one settled process — the same
10
+ * non-zero-exit + stderr-signature rules the sandbox executors apply: a fatal
11
+ * runner line is an infrastructure failure, never a policy denial. */
12
+ const classifyOutcome = (exitCode, stderr, facts) => {
13
+ const nonzero = exitCode !== null && exitCode !== 0;
14
+ const lines = stderr.toLowerCase().split(/\r?\n/);
15
+ const runnerFailed = nonzero && facts.runnerFailureRules.some((rule) => (rule.allowedExitCodes === void 0 || rule.allowedExitCodes.includes(exitCode)) && lines.some((line) => !rule.informationalLines?.some((info) => info.toLowerCase() === line) && rule.fatalSignatures.some((signature) => line.includes(signature.toLowerCase()))));
16
+ return {
17
+ denied: nonzero && !runnerFailed && facts.denialSignatures.some((signature) => stderr.toLowerCase().includes(signature.toLowerCase())),
18
+ runnerFailed
19
+ };
20
+ };
21
+ /** The `sandbox` stamp one settled outcome carries, merged over any stamp the
22
+ * confining executor already installed. */
23
+ const sandboxStamp = (value, facts, policy, stderr) => {
24
+ const { denied, runnerFailed } = classifyOutcome(exitCodeOf(value), stderr ?? stderrOf(value), facts);
25
+ return {
26
+ ...object$1(value.sandbox) ? value.sandbox : {},
27
+ mode: policy.mode,
28
+ enforcement: facts.enforcement,
29
+ denied,
30
+ ...runnerFailed ? { runnerFailed: true } : {}
31
+ };
32
+ };
33
+ function stampProcessResult(value, facts, policy) {
34
+ if (!object$1(value)) return value;
35
+ return {
36
+ ...value,
37
+ sandbox: sandboxStamp(value, facts, policy)
38
+ };
39
+ }
40
+ /** Whether a non-thenable, non-`result()` value is a live process handle
41
+ * rather than settled result data: it owns a `done` settle hook or callable
42
+ * members, or carries a shaped prototype. Copying it would freeze
43
+ * `status`/`exitCode` where consumers poll them and sever `readOutput`/`kill`
44
+ * identity — the model-facing `proc.sandbox` read happens on this very
45
+ * object, so the stamp must land in place.
46
+ */
47
+ const liveHandle = (value) => thenable(value.done) || typeof value.done === "function" || Object.values(value).some((member) => typeof member === "function") || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null;
48
+ function decorateProcessOutcome(value, facts, policy, diagnostics) {
49
+ if (object$1(value) && typeof value.then === "function") return value.then((result) => decorateProcessOutcome(result, facts, policy, diagnostics));
50
+ if (object$1(value) && typeof value.result === "function") {
51
+ let cached;
52
+ let settled = false;
53
+ wrapMethod(value, "result", (original) => function(...args) {
54
+ if (!settled) {
55
+ cached = decorateProcessOutcome(original.apply(this, args), facts, policy, diagnostics);
56
+ settled = true;
57
+ }
58
+ return cached;
59
+ });
60
+ return value;
61
+ }
62
+ if (object$1(value) && liveHandle(value)) {
63
+ const settle = () => {
64
+ try {
65
+ value.sandbox = sandboxStamp(value, facts, policy, diagnostics?.stderr?.());
66
+ } catch {}
67
+ };
68
+ if (thenable(value.done)) {
69
+ value.done = value.done.then((outcome) => {
70
+ settle();
71
+ return decorateProcessOutcome(outcome, facts, policy, diagnostics);
72
+ }, (error) => {
73
+ settle();
74
+ throw error;
75
+ });
76
+ return value;
77
+ }
78
+ if (typeof value.done === "function") {
79
+ wrapMethod(value, "done", (original) => function(...args) {
80
+ const outcome = original.apply(this, args);
81
+ return thenable(outcome) ? outcome.then((resolved) => {
82
+ settle();
83
+ return decorateProcessOutcome(resolved, facts, policy, diagnostics);
84
+ }, (error) => {
85
+ settle();
86
+ throw error;
87
+ }) : (settle(), decorateProcessOutcome(outcome, facts, policy, diagnostics));
88
+ });
89
+ return value;
90
+ }
91
+ settle();
92
+ return value;
93
+ }
94
+ return stampProcessResult(value, facts, policy);
95
+ }
96
+ //#endregion
97
+ //#region src/compat/process-entrypoints.ts
98
+ /**
99
+ * The public shell executor entries present on the mounted service, in this
100
+ * order: `execute`, `run`, `start` (`start` existed through 0.1.6).
101
+ * @param shell - the mounted `ctx.shell` service.
102
+ * @returns the present entries — a structural probe only.
103
+ */
104
+ function shellEntrypoints(shell) {
105
+ const record = shell;
106
+ return [
107
+ "execute",
108
+ "run",
109
+ "start"
110
+ ].filter((key) => typeof record[key] === "function");
111
+ }
112
+ /**
113
+ * The PTC runtime's public entry: `['run']` when it is a function; never the
114
+ * concrete runtime's private `execute` helper.
115
+ * @param runtime - the mounted `ctx.ptcRuntime` service.
116
+ * @returns the present entries — a structural probe only.
117
+ */
118
+ function ptcEntrypoints(runtime) {
119
+ return typeof runtime.run === "function" ? ["run"] : [];
120
+ }
121
+ //#endregion
122
+ //#region src/process-fence.ts
123
+ /**
124
+ * `multi-root-process-fence`: the B9 seam for the one cell `ctx.sandbox.confine`
125
+ * never sees — `danger-full-access` with explicit `read-only` additional roots.
126
+ *
127
+ * Upstream, every confined consumer routes through `ctx.sandbox.confine`, which
128
+ * this bundle's provider already widens (grants, the read-only fixed-write
129
+ * graft, and the workspace-write deny edge). But the shell executor, the PTC
130
+ * runtime and the PTY backend all short-circuit that mode: `policy.mode ===
131
+ * 'danger-full-access'` returns the bare argv, so an explicit `read-only` root
132
+ * would silently stay writable. This row closes the gap without touching any
133
+ * upstream executor:
134
+ *
135
+ * - `ctx.subprocess.spawn` is wrapped to prepend a precomputed restricted
136
+ * profile whenever the AsyncLocalStorage below carries one. `spawn` is
137
+ * synchronous, so the profile — which never depends on the callee's argv —
138
+ * is built by the wrapping consumer, not here.
139
+ * - `ctx.shell.execute` (bash and pwsh alike, and `start` where an older
140
+ * release still exposes it — `compat/process-entrypoints.ts` owns which
141
+ * entries a mounted executor actually carries) resolves the per-call policy
142
+ * exactly the way the executor itself does, and — under Full-access with
143
+ * deny roots — asks the sandbox provider for the restricted prefix and runs
144
+ * the call inside the store, so every subprocess the executor launches is
145
+ * confined.
146
+ * - `ctx.ptcRuntime.run` does the same for `run_code`.
147
+ * - `ctx.subprocess.spawnTerminal` keys on the `DSH_PTY_SESSION_ID` marker:
148
+ * every supported `dsh-terminal-bash` backend stamps it (alongside
149
+ * `DSH_SESSION_ID`) when it launches an agent's PTY, and no other caller —
150
+ * above all the panel's terminal controller — ever sets it. A spec without
151
+ * the marker is a USER terminal, passed through untouched in every mode:
152
+ * the sandbox protects agent behavior, and upstream never confines the
153
+ * user's own terminal. Because that marker is the ONLY discriminator, the
154
+ * `multi-root-compat` gate asserts it at boot (`compat/pty-marker.ts`):
155
+ * an installed backend whose emitted code no longer stamps it fails the
156
+ * plugin load rather than silently reclassifying agent PTYs as the user's.
157
+ * `ctx.terminals` itself is deliberately NOT wrapped:
158
+ * upstream mounts it inside `optional-persistent-terminals` with
159
+ * `isolate: { terminals: true }`, so the registry entry is unreachable from
160
+ * this row — the shared `spawnTerminal` seam plus the env discriminator
161
+ * covers both the un-isolated SDK mount and the isolated web mount.
162
+ *
163
+ * The same seam carries the unknown-authority downgrade: while the scope
164
+ * reports `authority: 'unavailable'` — a contended, storage-failed or
165
+ * config-invalid registry publishes an empty deny set that is NOT evidence —
166
+ * `danger-full-access` executions run inside a primary-root-only
167
+ * profile (with the deny set applied) instead of ambient.
168
+ *
169
+ * Windows uses the standalone ambient deny-token runner. Landlock cannot subtract a subtree, so that combination selects bubblewrap.
170
+ * An unusable mount runner fails without starting an unconfined process.
171
+ *
172
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/process-fence
173
+ */
174
+ const inject = [
175
+ "multiRootCompat",
176
+ "multiRootProviders",
177
+ "multiRootScope",
178
+ "sandbox",
179
+ "sandboxPolicy"
180
+ ];
181
+ const processDenyStore = new AsyncLocalStorage();
182
+ /** Whether a value is a thenable, i.e. whether the installed `confine` is async. */
183
+ function isThenable(value) {
184
+ return typeof value?.then === "function";
185
+ }
186
+ const object = (value) => typeof value === "object" && value !== null;
187
+ function apply(ctx) {
188
+ const policyOf = (spec) => spec?.sandboxPolicy ?? ctx.sandboxPolicy.resolve();
189
+ /**
190
+ * The confined facts for one Full-access call, or `undefined` when the call
191
+ * needs no confinement (not full-access, and nothing to deny while the
192
+ * registry is the store's authority). An `unavailable` scope authority
193
+ * clamps the call to a primary-root-only profile: the empty deny
194
+ * set a contended or failed registry publishes is not evidence that nothing
195
+ * is restricted. May throw synchronously — `SandboxUnavailableError` from a
196
+ * dialect that cannot express the confinement, which must propagate to the
197
+ * caller, never be swallowed.
198
+ */
199
+ const prepare = (policy, signal) => {
200
+ if (policy.mode !== "danger-full-access") return void 0;
201
+ const scope = ctx.multiRootScope.resolve(policy);
202
+ if (scope.authority === "unavailable") return ctx.sandbox.clampedExecution(policy, scope.deniedRoots, signal);
203
+ if (scope.deniedRoots.length === 0) return void 0;
204
+ return ctx.sandbox.restrictedExecution(policy, scope.deniedRoots, signal, scope.protectedHome);
205
+ };
206
+ /**
207
+ * Run `body` under the restricted deny context when `prepared` is a prefix —
208
+ * preserving the sync/async shape of the prepared value itself.
209
+ */
210
+ const runDenied = (prepared, policy, body) => {
211
+ const run = (facts) => {
212
+ const store = { argvPrefix: facts.argv };
213
+ return processDenyStore.run(store, () => decorateProcessOutcome(body(), facts, policy, { stderr: () => store.stderr?.() }));
214
+ };
215
+ return isThenable(prepared) ? prepared.then(run) : run(prepared);
216
+ };
217
+ ctx.inject(["subprocess"], (scope) => {
218
+ scope.effect(() => wrapMethod(scope.subprocess, "spawn", (original) => function(spec) {
219
+ const store = processDenyStore.getStore();
220
+ if (store === void 0) return original.call(this, spec);
221
+ const handle = original.call(this, {
222
+ ...spec,
223
+ argv: [...store.argvPrefix, ...spec.argv]
224
+ });
225
+ const stderr = object(handle) && object(handle.collected) && object(handle.collected.stderr) && typeof handle.collected.stderr.readFrom === "function" ? handle.collected.stderr : void 0;
226
+ if (stderr !== void 0) store.stderr = () => {
227
+ try {
228
+ return stderr.readFrom(0).text;
229
+ } catch {
230
+ return;
231
+ }
232
+ };
233
+ return handle;
234
+ }));
235
+ scope.effect(() => wrapMethod(scope.subprocess, "spawnTerminal", (original) => async function(spec) {
236
+ const ptySessionId = spec.env?.DSH_PTY_SESSION_ID;
237
+ if (ptySessionId === void 0 || ptySessionId === "") return await original.call(this, spec);
238
+ const sessionId = spec.env?.DSH_SESSION_ID;
239
+ const session = sessionId === void 0 || sessionId === "" ? void 0 : ctx.get("sessions")?.get(sessionId);
240
+ if (session === void 0) {
241
+ if (!ctx.multiRootScope.hasActiveDenyRoots()) return await original.call(this, spec);
242
+ throw new Error(`multi-root workspace: agent terminal spawn carries a session marker that does not resolve to a live session (${JSON.stringify(sessionId ?? ptySessionId)}), and explicit read-only roots are or may be registered — refusing to spawn an unconfined terminal`);
243
+ }
244
+ const policy = ctx.sandboxPolicy.resolve({ session });
245
+ const prepared = prepare(policy, spec.signal);
246
+ if (prepared === void 0) return await original.call(this, spec);
247
+ const argvPrefix = (await prepared).argv;
248
+ return await original.call(this, {
249
+ ...spec,
250
+ argv: [...argvPrefix, ...spec.argv]
251
+ });
252
+ }));
253
+ });
254
+ /**
255
+ * The wrapper every execute-style consumer entry shares: resolve the policy
256
+ * the way the upstream consumer does, and when the call runs ambient under
257
+ * Full-access with roots to deny, execute it inside the restricted store so
258
+ * every subprocess it launches is confined.
259
+ */
260
+ const deniedEntry = (scope, target, key) => {
261
+ scope.effect(() => wrapMethod(target, key, (original) => function(spec, ...rest) {
262
+ const policy = policyOf(spec);
263
+ const prepared = prepare(policy, spec?.signal);
264
+ if (prepared === void 0) return original.call(this, spec, ...rest);
265
+ return runDenied(prepared, policy, () => original.call(this, spec, ...rest));
266
+ }));
267
+ };
268
+ ctx.inject(["shell"], (scope) => {
269
+ const shell = scope.get("shell");
270
+ if (shell === void 0) return;
271
+ for (const key of shellEntrypoints(shell)) deniedEntry(scope, shell, key);
272
+ });
273
+ ctx.inject(["ptcRuntime"], (scope) => {
274
+ const runtime = scope.get("ptcRuntime");
275
+ if (runtime === void 0) return;
276
+ for (const key of ptcEntrypoints(runtime)) deniedEntry(scope, runtime, key);
277
+ });
278
+ }
279
+ //#endregion
280
+ export { apply, inject };
@@ -0,0 +1,87 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ //#region src/providers.ts
3
+ /**
4
+ * `MultiRootProviders`: the fail-loud guard that proves the services mounted as
5
+ * `ctx.fs` and `ctx.sandbox` are THIS bundle's providers — the only ones whose
6
+ * widening the registry, the instruction row, LSP routing and workspace-files
7
+ * are allowed to attach to.
8
+ *
9
+ * Why a brand and not `instanceof`: the plugin may be installed twice (a
10
+ * duplicated install forks the class identity), and cordis private fields are
11
+ * not a public contract. A well-known symbol survives both. The brand is
12
+ * non-enumerable so it never leaks into spreads, JSON, or surface payloads.
13
+ *
14
+ * Deterministic outcome under a provider conflict: cordis refuses a second
15
+ * `ctx.provide` of an already-registered key, so whoever mounts the colliding
16
+ * provider second loses loudly — but a conflicting plugin can also DISABLE this
17
+ * bundle's rows (the same mechanism this bundle uses on upstream), leaving a
18
+ * foreign `ctx.fs`/`ctx.sandbox` in place. In that case this service's
19
+ * `[Service.init]` logs which service is foreign and throws: the row never
20
+ * activates, so every row injecting `multiRootProviders` — the registry above
21
+ * all — never starts, the scope stays empty, and nothing is ever granted,
22
+ * regardless of patch order. Cordis re-evaluates an injecting fiber when an
23
+ * injected service is unprovided and re-provided, so a LATE replacement re-runs
24
+ * the same check.
25
+ *
26
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/providers
27
+ */
28
+ /**
29
+ * The well-known brand key both providers carry. `Symbol.for` makes it
30
+ * identical across duplicated installs of this package.
31
+ */
32
+ const MULTI_ROOT_PROVIDER_BRAND = Symbol.for("@dsh-electron/dsh-plugin-multi-root-workspace/provider");
33
+ /**
34
+ * Stamp the multi-root provider brand on a service value. Non-enumerable:
35
+ * the mark must not travel into structural copies.
36
+ * @param value - the service instance being mounted.
37
+ * @param kind - which provider key it claims.
38
+ * @returns the same value, branded.
39
+ */
40
+ function brandMultiRootProvider(value, kind) {
41
+ Object.defineProperty(value, MULTI_ROOT_PROVIDER_BRAND, {
42
+ value: kind,
43
+ enumerable: false,
44
+ writable: false,
45
+ configurable: false
46
+ });
47
+ return value;
48
+ }
49
+ /**
50
+ * Whether `value` is this bundle's provider of `kind`. Structural on purpose:
51
+ * no `instanceof`, so a duplicated install still recognizes the brand.
52
+ * @param value - the service mounted under the key.
53
+ * @param kind - the provider key being checked.
54
+ */
55
+ function isMultiRootProvider(value, kind) {
56
+ return value !== null && (typeof value === "object" || typeof value === "function") && value[MULTI_ROOT_PROVIDER_BRAND] === kind;
57
+ }
58
+ /**
59
+ * The conflict gate: `ctx.multiRootProviders` exists only while `ctx.fs` and
60
+ * `ctx.sandbox` are this bundle's branded providers. Every row that widens an
61
+ * authority surface injects it, so a foreign provider composition loses the
62
+ * whole gated set instead of silently widening an unverified one.
63
+ */
64
+ var MultiRootProviders = class extends Service {
65
+ static provide = "multiRootProviders";
66
+ static inject = [
67
+ "multiRootCompat",
68
+ "fs",
69
+ "sandbox"
70
+ ];
71
+ /**
72
+ * Verify the provider identities once per activation. A foreign service is
73
+ * named in the log and the throw leaves this row — and every dependent —
74
+ * unstarted: fail loud, never a widened grant.
75
+ */
76
+ [Service.init]() {
77
+ const foreign = [];
78
+ if (!isMultiRootProvider(this.ctx.fs, "fs")) foreign.push("fs");
79
+ if (!isMultiRootProvider(this.ctx.sandbox, "sandbox")) foreign.push("sandbox");
80
+ if (foreign.length === 0) return;
81
+ const names = foreign.map((name) => `ctx.${name}`).join(" and ");
82
+ this.ctx.logger.error(`multi-root-workspace: ${names} is provided by another plugin, not this bundle's multi-root provider; refusing to start the multi-root rows so no widened grant can attach to an unverified provider. Resolve the conflict by disabling the other plugin's provider rows.`);
83
+ throw new Error(`multi-root-workspace: ${names} is a foreign provider; multi-root rows are disabled`);
84
+ }
85
+ };
86
+ //#endregion
87
+ export { MULTI_ROOT_PROVIDER_BRAND, MultiRootProviders, MultiRootProviders as default, brandMultiRootProvider, isMultiRootProvider };