@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.
- package/CHANGELOG.en.md +44 -1
- package/CHANGELOG.md +44 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +57 -0
- package/lib/client.js +2678 -276
- package/lib/client.js.map +1 -1
- package/lib/command-CESuwlVt.js +1306 -0
- package/lib/command.js +2 -2
- package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
- package/lib/fs.js +2 -1
- package/lib/index.js +10 -8
- package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +7 -2
- package/lib/method-wrapper-CFY3p3j2.js +26 -0
- package/lib/process-fence.js +280 -0
- package/lib/providers.js +87 -0
- package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
- package/lib/registry.js +2 -2
- package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
- package/lib/sandbox-Kup4_ZIb.js +747 -0
- package/lib/sandbox.js +2 -1
- package/lib/scope.js +229 -20
- package/lib/types/client/RootSwitcher.d.ts +63 -0
- package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
- package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
- package/lib/types/client/files-tree/adapter.d.ts +94 -0
- package/lib/types/client/files-tree/directory-node.d.ts +62 -0
- package/lib/types/client/files-tree/face.d.ts +107 -0
- package/lib/types/client/files-tree/locales.d.ts +10 -0
- package/lib/types/client/files-tree/primitives.d.ts +33 -0
- package/lib/types/client/files-tree/store.d.ts +81 -0
- package/lib/types/client/locales.d.ts +56 -10
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
- package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
- package/lib/types/client/open-in-app/adapter.d.ts +46 -0
- package/lib/types/client/open-in-app/applications.d.ts +4 -0
- package/lib/types/client/open-in-app/controller.d.ts +53 -0
- package/lib/types/client/open-in-app/locales.d.ts +10 -0
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
- package/lib/types/client/open-in-app/open-path.d.ts +13 -0
- package/lib/types/client/panel-client.d.ts +2 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +16 -3
- package/lib/types/compat/client-icons.d.ts +18 -0
- package/lib/types/compat/dsh-version.d.ts +1 -1
- package/lib/types/compat/open-in-app-routes.d.ts +38 -0
- package/lib/types/compat/process-entrypoints.d.ts +31 -0
- package/lib/types/compat/process-result.d.ts +14 -0
- package/lib/types/compat/pty-marker.d.ts +83 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/compat/storage-zod.d.ts +65 -0
- package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
- package/lib/types/compat/windows-native.d.ts +68 -0
- package/lib/types/compat/windows-path-guard.d.ts +56 -0
- package/lib/types/compat/workspace-files.d.ts +2 -0
- package/lib/types/contract.d.ts +31 -19
- package/lib/types/dialects.d.ts +99 -3
- package/lib/types/fs.d.ts +32 -10
- package/lib/types/index.d.ts +7 -2
- package/lib/types/process-fence.d.ts +54 -0
- package/lib/types/profile-isolation.d.ts +178 -0
- package/lib/types/providers.d.ts +70 -0
- package/lib/types/registry.d.ts +132 -31
- package/lib/types/roots.d.ts +100 -13
- package/lib/types/sandbox.d.ts +59 -3
- package/lib/types/scope.d.ts +200 -14
- package/lib/types/windows-broker.d.ts +17 -0
- package/lib/types/windows-deny.d.ts +1 -0
- package/lib/types/windows-profile.d.ts +10 -13
- package/lib/types/windows-runner-contract.d.ts +97 -0
- package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
- package/lib/windows-runner.js +1256 -13
- package/lib/workspace-files.js +138 -3
- package/lib/workspace-routing-YPxaQ4V3.js +26 -0
- package/package.json +84 -62
- package/scripts/windows-broker/install.ps1 +89 -0
- package/scripts/windows-broker/rights.cs +58 -0
- package/scripts/windows-broker/service.cs +323 -0
- package/scripts/windows-broker/verify-install.ps1 +80 -0
- package/lib/command-CTdNvwt1.js +0 -791
- package/lib/method-wrapper-Zc-JlDP3.js +0 -51
- package/lib/sandbox-BRXl_FpF.js +0 -132
- package/lib/types/client/RootFileTree.d.ts +0 -12
- package/lib/types/panel-files.d.ts +0 -3
- package/lib/windows-profile-CiFsFYIV.js +0 -216
- 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 };
|
package/lib/providers.js
ADDED
|
@@ -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 };
|