@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.8
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 +51 -1
- package/CHANGELOG.md +51 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +69 -4
- 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-DR2LvmYZ.js +600 -0
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-Nlp2AiRX.js} +56 -24
- 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-CVAcT_gy.js +743 -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 +57 -9
- 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/compat.d.ts +15 -5
- 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 +8 -3
- 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/compat-B6ZqbQBy.js +0 -295
- 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
package/lib/command.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as renderRootsReport, i as parseFoldersCommand, n as apply, o as resolvePanelPrimaryRoot, r as inject, s as
|
|
2
|
-
export { COMMAND_NAME, apply, inject, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealArgv };
|
|
1
|
+
import { a as renderRootsReport, c as revealArgv, i as parseFoldersCommand, n as apply, o as resolvePanelPrimaryRoot, r as inject, s as revealAccepted, t as COMMAND_NAME } from "./command-CESuwlVt.js";
|
|
2
|
+
export { COMMAND_NAME, apply, inject, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealAccepted, revealArgv };
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { dirname, join, sep } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/compat/dsh-version.ts
|
|
7
|
+
/**
|
|
8
|
+
* The DSH compatibility contract: which upstream releases this plugin is
|
|
9
|
+
* actually verified against, and how an installation is judged against that
|
|
10
|
+
* list.
|
|
11
|
+
*
|
|
12
|
+
* This module is the ONE place the support matrix is spelled out. It is
|
|
13
|
+
* deliberately pure — no cordis, no logging, no process exit — so that the
|
|
14
|
+
* runtime gate (`src/compat.ts`), the static gate (`scripts/check-dsh-compat.mjs`)
|
|
15
|
+
* and the tests all judge an installation by the same code instead of three
|
|
16
|
+
* drifting copies of the same list.
|
|
17
|
+
*
|
|
18
|
+
* Why a contract exists at all: this plugin replaces `ctx.fs` and `ctx.sandbox`
|
|
19
|
+
* with subclasses of the upstream providers, and `src/dialects.ts` recognizes
|
|
20
|
+
* the kernel-sandbox profile by OBSERVING the argv shape `super.confine`
|
|
21
|
+
* produced (ADR-0003). Both couplings are to pre-stable upstream internals that
|
|
22
|
+
* carry no semver promise. An npm range like `>=0.1.2-alpha.4 <0.2.0` therefore
|
|
23
|
+
* promises far more than the code was ever verified against; the allowlist
|
|
24
|
+
* below promises exactly what CI has run.
|
|
25
|
+
*
|
|
26
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/dsh-version
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* The releases this plugin is verified against, as EXACT version strings.
|
|
30
|
+
*
|
|
31
|
+
* Not a semver range, on purpose: a range is a prediction about releases that
|
|
32
|
+
* do not exist yet, and this plugin's coupling to upstream argv shapes makes
|
|
33
|
+
* that prediction unsafe. Adding an entry is always a human act performed after
|
|
34
|
+
* the upgrade smoke passed on that exact version — a green CI run on an
|
|
35
|
+
* unlisted release is evidence, not authorization (ADR-0009).
|
|
36
|
+
*/
|
|
37
|
+
const SUPPORTED_DSH_RELEASES = [
|
|
38
|
+
"0.1.5-rc.2",
|
|
39
|
+
"0.1.6-alpha.1",
|
|
40
|
+
"0.1.6-alpha.2",
|
|
41
|
+
"0.1.7-alpha.1",
|
|
42
|
+
"0.1.7-alpha.2",
|
|
43
|
+
"0.1.7-rc.1",
|
|
44
|
+
"0.1.7-rc.2",
|
|
45
|
+
"0.2.0-rc.1",
|
|
46
|
+
"0.2.0-rc.2",
|
|
47
|
+
"0.2.1-alpha.1",
|
|
48
|
+
"0.2.1-alpha.2",
|
|
49
|
+
"0.2.1-alpha.3",
|
|
50
|
+
"0.2.1-alpha.4",
|
|
51
|
+
"0.2.1-alpha.5",
|
|
52
|
+
"0.2.1-rc.1",
|
|
53
|
+
"0.2.1-rc.2"
|
|
54
|
+
];
|
|
55
|
+
/**
|
|
56
|
+
* Packages whose absence means this plugin cannot function at all: every one of
|
|
57
|
+
* them is either subclassed, imported for a value, or the owner of a service
|
|
58
|
+
* key the providers inject.
|
|
59
|
+
*/
|
|
60
|
+
const REQUIRED_CORE_PACKAGES = [
|
|
61
|
+
"@deepseek-ai/dsh-fs",
|
|
62
|
+
"@deepseek-ai/dsh-fs-local",
|
|
63
|
+
"@deepseek-ai/dsh-sandbox",
|
|
64
|
+
"@deepseek-ai/dsh-sandbox-local",
|
|
65
|
+
"@deepseek-ai/dsh-sandbox-policy",
|
|
66
|
+
"@deepseek-ai/dsh-storage-domain"
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Packages a full composition provides and a minimal one legitimately omits.
|
|
70
|
+
* Absence is fine; a version that DISAGREES with the required packages is not —
|
|
71
|
+
* that is the mixed-installation failure this contract exists to catch.
|
|
72
|
+
*/
|
|
73
|
+
const OPTIONAL_CORE_PACKAGES = [
|
|
74
|
+
"@deepseek-ai/dsh-tools",
|
|
75
|
+
"@deepseek-ai/dsh-working-directory",
|
|
76
|
+
"@deepseek-ai/dsh-lsp",
|
|
77
|
+
"@deepseek-ai/dsh-lsp-stdio",
|
|
78
|
+
"@deepseek-ai/dsh-api-workspace-files",
|
|
79
|
+
"@deepseek-ai/dsh-typert-protocol",
|
|
80
|
+
"@deepseek-ai/dsh-sandbox-windows-acl",
|
|
81
|
+
"@deepseek-ai/dsh-agent",
|
|
82
|
+
"@deepseek-ai/dsh-agent-instructions",
|
|
83
|
+
"@deepseek-ai/dsh-client-connection",
|
|
84
|
+
"@deepseek-ai/dsh-commands",
|
|
85
|
+
"@deepseek-ai/dsh-llm",
|
|
86
|
+
"@deepseek-ai/dsh-session",
|
|
87
|
+
"@deepseek-ai/dsh-subprocess",
|
|
88
|
+
"@deepseek-ai/dsh-system-prompt"
|
|
89
|
+
];
|
|
90
|
+
/**
|
|
91
|
+
* The optional packages this plugin loads itself at run time, through dynamic
|
|
92
|
+
* `import()`s inside this package's own modules. The copy this module resolves
|
|
93
|
+
* IS the copy that would execute, so every resolvable hit testifies — a
|
|
94
|
+
* private copy inside this package's own tree genuinely loads, and a version
|
|
95
|
+
* that disagrees with the host is a real mixed installation, not a phantom.
|
|
96
|
+
*/
|
|
97
|
+
const RUNTIME_OPTIONAL_PACKAGES = [
|
|
98
|
+
"@deepseek-ai/dsh-agent-instructions",
|
|
99
|
+
"@deepseek-ai/dsh-llm",
|
|
100
|
+
"@deepseek-ai/dsh-session",
|
|
101
|
+
"@deepseek-ai/dsh-typert-protocol"
|
|
102
|
+
];
|
|
103
|
+
/**
|
|
104
|
+
* The optional packages this plugin consumes only as mounted host services or
|
|
105
|
+
* ambient types — it never imports them, so a copy inside this package's own
|
|
106
|
+
* tree can never execute. The only version that testifies is the one the
|
|
107
|
+
* COMPOSITION resolves (the installation or the profile): a resolution that
|
|
108
|
+
* lands inside this package's own root is a private development or bundled
|
|
109
|
+
* copy and must be reported absent — otherwise a `link:`-installed checkout
|
|
110
|
+
* whose devDependencies carry a newer release phantom-mixes every supported
|
|
111
|
+
* host it is developed against (the report that motivated this split:
|
|
112
|
+
* `dsh-lsp`/`dsh-lsp-stdio`/`dsh-working-directory` resolving to the plugin's
|
|
113
|
+
* devDependencies on a `0.2.0-rc.2` host produced a `mixed` verdict on a
|
|
114
|
+
* uniformly `0.2.0-rc.2` installation).
|
|
115
|
+
*/
|
|
116
|
+
const HOST_SERVICE_PACKAGES = [
|
|
117
|
+
"@deepseek-ai/dsh-agent",
|
|
118
|
+
"@deepseek-ai/dsh-api-workspace-files",
|
|
119
|
+
"@deepseek-ai/dsh-client-connection",
|
|
120
|
+
"@deepseek-ai/dsh-commands",
|
|
121
|
+
"@deepseek-ai/dsh-lsp",
|
|
122
|
+
"@deepseek-ai/dsh-lsp-stdio",
|
|
123
|
+
"@deepseek-ai/dsh-subprocess",
|
|
124
|
+
"@deepseek-ai/dsh-system-prompt",
|
|
125
|
+
"@deepseek-ai/dsh-tools",
|
|
126
|
+
"@deepseek-ai/dsh-working-directory"
|
|
127
|
+
];
|
|
128
|
+
/**
|
|
129
|
+
* Optional packages this plugin executes that are resolved through an anchor
|
|
130
|
+
* OTHER than this module. The standalone Windows ACL runner
|
|
131
|
+
* (`src/windows-runner.ts`) imports `@deepseek-ai/dsh-sandbox-windows-acl` by
|
|
132
|
+
* self-reference from the upstream runner's own install location — a file the
|
|
133
|
+
* installed `dsh-sandbox-local` points at in its confined argv — so the copy
|
|
134
|
+
* that testifies is the one sibling to that required package, not whatever
|
|
135
|
+
* this module can reach. Neither the self-resolution hit nor the
|
|
136
|
+
* `@deepseek-ai/dsh` host anchor reflects execution here: if sandbox-local's
|
|
137
|
+
* own anchor cannot see the package, the runner cannot load it either, and
|
|
138
|
+
* absent is the honest answer.
|
|
139
|
+
*/
|
|
140
|
+
const RUNNER_ANCHORED_PACKAGES = ["@deepseek-ai/dsh-sandbox-windows-acl"];
|
|
141
|
+
/** The required package whose tree produces the upstream runner argv. */
|
|
142
|
+
const RUNNER_ANCHOR_PACKAGE = "@deepseek-ai/dsh-sandbox-local";
|
|
143
|
+
/** Every package the contract inspects, required first. */
|
|
144
|
+
const CORE_PACKAGES = [...REQUIRED_CORE_PACKAGES, ...OPTIONAL_CORE_PACKAGES];
|
|
145
|
+
/** The environment variable that relaxes the gate. */
|
|
146
|
+
const COMPAT_ENFORCEMENT_ENV = "DSH_MULTI_ROOT_COMPAT";
|
|
147
|
+
/**
|
|
148
|
+
* Resolve one package's `package.json` path through a require, or `undefined`
|
|
149
|
+
* when it cannot be resolved. The path — not just the version — matters: the
|
|
150
|
+
* caller needs to know WHERE the copy lives to judge whether it belongs to the
|
|
151
|
+
* inspected installation.
|
|
152
|
+
* @param require - the require to resolve through.
|
|
153
|
+
* @param packageName - the package to locate.
|
|
154
|
+
* @returns the manifest path, or `undefined`.
|
|
155
|
+
*/
|
|
156
|
+
function resolvePackageJson(require, packageName) {
|
|
157
|
+
try {
|
|
158
|
+
return require.resolve(`${packageName}/package.json`);
|
|
159
|
+
} catch {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Read `version` off a resolved manifest path; a broken manifest is absent. */
|
|
164
|
+
function versionOfManifest(manifestPath) {
|
|
165
|
+
try {
|
|
166
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
167
|
+
return typeof manifest.version === "string" ? manifest.version : void 0;
|
|
168
|
+
} catch {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The second resolution anchor: a require rooted at the installed
|
|
174
|
+
* `@deepseek-ai/dsh` meta package. Host-internal packages that never appear as
|
|
175
|
+
* plugin peers are only reachable this way (the pattern the retired
|
|
176
|
+
* `HOST_RESOLVED_PACKAGES` list carved out for `dsh-working-directory`,
|
|
177
|
+
* generalized here to every host-service package).
|
|
178
|
+
* @param require - this module's require, used to locate the meta package.
|
|
179
|
+
* @returns the host-rooted require, or `undefined` when `dsh` itself cannot be
|
|
180
|
+
* resolved.
|
|
181
|
+
*/
|
|
182
|
+
function hostAnchorRequire(require) {
|
|
183
|
+
const anchor = resolvePackageJson(require, "@deepseek-ai/dsh");
|
|
184
|
+
return anchor === void 0 ? void 0 : createRequire(anchor);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* This package's own install root — the boundary between a copy that belongs
|
|
188
|
+
* to the inspected installation and a private copy only this package can see.
|
|
189
|
+
* @param require - this module's require; self-reference through the package's
|
|
190
|
+
* `exports` is tried first, then a walk up from this module's file.
|
|
191
|
+
* @returns the package root, or `undefined` when neither anchor works.
|
|
192
|
+
*/
|
|
193
|
+
function pluginPackageRoot(require) {
|
|
194
|
+
try {
|
|
195
|
+
return dirname(require.resolve("@dsh-electron/dsh-plugin-multi-root-workspace/package.json"));
|
|
196
|
+
} catch {
|
|
197
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
198
|
+
while (true) {
|
|
199
|
+
if (existsSync(join(dir, "package.json"))) return dir;
|
|
200
|
+
const parent = dirname(dir);
|
|
201
|
+
if (parent === dir) return void 0;
|
|
202
|
+
dir = parent;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Whether a resolved manifest path lives inside this package's own root — the
|
|
208
|
+
* mark of a private copy no composition can mount. An unknown root cannot
|
|
209
|
+
* exclude anything, so it answers `false` rather than guessing.
|
|
210
|
+
*/
|
|
211
|
+
function insidePackageRoot(manifestPath, root) {
|
|
212
|
+
return root !== void 0 && (manifestPath === root || manifestPath.startsWith(`${root}${sep}`));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Read one package's declared version through Node resolution.
|
|
216
|
+
*
|
|
217
|
+
* Resolution runs from THIS module — for required and runtime-loaded packages
|
|
218
|
+
* the copy found here is the copy that would execute, which is what makes the
|
|
219
|
+
* answer meaningful: the upstream packages are peer dependencies, so they
|
|
220
|
+
* resolve to the single copy the host provides — the same copy the providers
|
|
221
|
+
* will subclass at runtime.
|
|
222
|
+
*
|
|
223
|
+
* {@link HOST_SERVICE_PACKAGES} are different: the plugin never imports them,
|
|
224
|
+
* so a copy inside this package's own tree can never execute, and counting it
|
|
225
|
+
* would report a mixed installation out of a linked checkout's devDependencies
|
|
226
|
+
* alone. Only a hit OUTSIDE this package's root — a copy the composition could
|
|
227
|
+
* mount — testifies; when this module cannot reach the package at all, the
|
|
228
|
+
* `@deepseek-ai/dsh` host manifest supplies a second anchor for host-internal
|
|
229
|
+
* packages that are never plugin peers.
|
|
230
|
+
*
|
|
231
|
+
* {@link RUNNER_ANCHORED_PACKAGES} are a third policy: executed, but resolved
|
|
232
|
+
* from the upstream-runner anchor rather than this module, so only a hit
|
|
233
|
+
* sibling to the installed `dsh-sandbox-local` testifies.
|
|
234
|
+
*
|
|
235
|
+
* A package that cannot be resolved in either place is reported absent rather
|
|
236
|
+
* than throwing, because "not installed" is a legitimate answer for every
|
|
237
|
+
* optional package.
|
|
238
|
+
* @param packageName - the package to inspect.
|
|
239
|
+
* @returns the declared version, or `undefined` when the package or its
|
|
240
|
+
* manifest cannot be resolved.
|
|
241
|
+
*/
|
|
242
|
+
function readInstalledVersion(packageName) {
|
|
243
|
+
const require = createRequire(import.meta.url);
|
|
244
|
+
if (RUNNER_ANCHORED_PACKAGES.includes(packageName)) {
|
|
245
|
+
const anchor = resolvePackageJson(require, RUNNER_ANCHOR_PACKAGE);
|
|
246
|
+
const hit = anchor === void 0 ? void 0 : resolvePackageJson(createRequire(anchor), packageName);
|
|
247
|
+
return hit === void 0 ? void 0 : versionOfManifest(hit);
|
|
248
|
+
}
|
|
249
|
+
if (!HOST_SERVICE_PACKAGES.includes(packageName)) {
|
|
250
|
+
const hit = resolvePackageJson(require, packageName);
|
|
251
|
+
return hit === void 0 ? void 0 : versionOfManifest(hit);
|
|
252
|
+
}
|
|
253
|
+
const root = pluginPackageRoot(require);
|
|
254
|
+
const selfHit = resolvePackageJson(require, packageName);
|
|
255
|
+
if (selfHit !== void 0 && !insidePackageRoot(selfHit, root)) return versionOfManifest(selfHit);
|
|
256
|
+
const host = hostAnchorRequire(require);
|
|
257
|
+
const hostHit = host === void 0 ? void 0 : resolvePackageJson(host, packageName);
|
|
258
|
+
return hostHit === void 0 || insidePackageRoot(hostHit, root) ? void 0 : versionOfManifest(hostHit);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Judge one installation against the contract.
|
|
262
|
+
*
|
|
263
|
+
* The three checks run in this order, and the first failure wins, because each
|
|
264
|
+
* later check would report nonsense about an installation that failed an
|
|
265
|
+
* earlier one: a missing required package has no version to compare, and a
|
|
266
|
+
* mixed installation has no single version to look up in the allowlist.
|
|
267
|
+
* @param readVersion - version reader; defaults to Node resolution. Tests and
|
|
268
|
+
* the static checker pass their own.
|
|
269
|
+
* @returns the report; never throws.
|
|
270
|
+
*/
|
|
271
|
+
function classifyInstallation(readVersion = readInstalledVersion) {
|
|
272
|
+
const packages = CORE_PACKAGES.map((name) => ({
|
|
273
|
+
name,
|
|
274
|
+
version: readVersion(name),
|
|
275
|
+
required: REQUIRED_CORE_PACKAGES.includes(name)
|
|
276
|
+
}));
|
|
277
|
+
const missing = packages.filter((entry) => entry.required && entry.version === void 0);
|
|
278
|
+
if (missing.length > 0) return report("incomplete", void 0, packages, [
|
|
279
|
+
"multi-root workspace: required DSH packages are not installed.",
|
|
280
|
+
"",
|
|
281
|
+
...missing.map((entry) => ` missing: ${entry.name}`)
|
|
282
|
+
]);
|
|
283
|
+
const releases = [...new Set(packages.flatMap((entry) => entry.version === void 0 ? [] : [entry.version]))].sort();
|
|
284
|
+
const [release, ...extra] = releases;
|
|
285
|
+
if (release === void 0) return report("incomplete", void 0, packages, ["multi-root workspace: no DSH package could be inspected."]);
|
|
286
|
+
if (extra.length > 0) return report("mixed", void 0, packages, [
|
|
287
|
+
"multi-root workspace: this installation mixes several DSH releases.",
|
|
288
|
+
"",
|
|
289
|
+
"The filesystem fence and the kernel-sandbox dialects must agree on one upstream",
|
|
290
|
+
"release; a mixed installation means one half of the sandbox was verified against a",
|
|
291
|
+
"shape the other half no longer produces. Align every @deepseek-ai/dsh* package on",
|
|
292
|
+
"one release.",
|
|
293
|
+
"",
|
|
294
|
+
` releases found: ${releases.join(", ")}`
|
|
295
|
+
]);
|
|
296
|
+
if (!SUPPORTED_DSH_RELEASES.includes(release)) return report("unsupported", release, packages, [
|
|
297
|
+
`multi-root workspace: DSH ${release} is not a supported release.`,
|
|
298
|
+
"",
|
|
299
|
+
"This plugin replaces the workspace filesystem fence and the kernel-sandbox provider,",
|
|
300
|
+
"and it recognizes the sandbox profile by the argv shape the upstream provider produces.",
|
|
301
|
+
"Upstream makes no semver promise, so the plugin refuses to grant additional roots on a",
|
|
302
|
+
"release it has never been verified against rather than guess at that shape.",
|
|
303
|
+
"",
|
|
304
|
+
` supported: ${SUPPORTED_DSH_RELEASES.join(", ")}`,
|
|
305
|
+
` installed: ${release}`
|
|
306
|
+
]);
|
|
307
|
+
return report("supported", release, packages, [`multi-root workspace: DSH ${release} is a supported release.`]);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Read the enforcement mode.
|
|
311
|
+
*
|
|
312
|
+
* `warn` exists for exactly one caller: the upgrade smoke, which must run the
|
|
313
|
+
* whole behavior matrix against a release that is deliberately NOT on the
|
|
314
|
+
* allowlist yet — that run is how a release earns its place on the list. It is
|
|
315
|
+
* not a production escape hatch, and the warning says so.
|
|
316
|
+
* @param env - the environment to read; defaults to this process's.
|
|
317
|
+
* @returns `warn` only when explicitly requested, `enforce` otherwise.
|
|
318
|
+
*/
|
|
319
|
+
function compatEnforcement(env = process.env) {
|
|
320
|
+
return env["DSH_MULTI_ROOT_COMPAT"] === "warn" ? "warn" : "enforce";
|
|
321
|
+
}
|
|
322
|
+
/** Compose a report, appending the package inventory and the recovery pointer. */
|
|
323
|
+
function report(verdict, release, packages, lines) {
|
|
324
|
+
return {
|
|
325
|
+
verdict,
|
|
326
|
+
release,
|
|
327
|
+
packages,
|
|
328
|
+
message: (verdict === "supported" ? [...lines] : [
|
|
329
|
+
...lines,
|
|
330
|
+
"",
|
|
331
|
+
"Inspected packages:",
|
|
332
|
+
...packages.map((entry) => ` ${entry.name} ${entry.version ?? "(not installed)"}`),
|
|
333
|
+
"",
|
|
334
|
+
"See docs/troubleshooting/unsupported-dsh-release.md"
|
|
335
|
+
]).join("\n")
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/compat/pty-marker.ts
|
|
340
|
+
/**
|
|
341
|
+
* Boot-time probe for the agent-PTY marker contract.
|
|
342
|
+
*
|
|
343
|
+
* `multi-root-process-fence` tells an agent terminal from the user's own by
|
|
344
|
+
* one env fact: every supported `dsh-terminal-bash` backend stamps
|
|
345
|
+
* `DSH_PTY_SESSION_ID` (alongside `DSH_SESSION_ID`) into the PTY spawn spec,
|
|
346
|
+
* while the panel's terminal controller stamps only `DSH_SESSION_ID`. That
|
|
347
|
+
* marker is the ONLY discriminator — a backend that ever stops stamping it
|
|
348
|
+
* would reclassify agent PTYs as user terminals and pass them through
|
|
349
|
+
* unconfined under a restricted `danger-full-access`, which is a silent
|
|
350
|
+
* fail-open on a security boundary.
|
|
351
|
+
*
|
|
352
|
+
* The marker is an env key inside upstream's emitted code, not an exported
|
|
353
|
+
* symbol, so the honest structural probe is content: the installed backend
|
|
354
|
+
* package must still write the marker. This module is called from the
|
|
355
|
+
* `multi-root-compat` gate (`src/compat.ts`) BEFORE the service registers —
|
|
356
|
+
* like the version verdict it is a precondition, not a diagnostic, and a
|
|
357
|
+
* breach refuses the composition the same way `mixed`/`incomplete` do: it is
|
|
358
|
+
* not relaxable by `DSH_MULTI_ROOT_COMPAT=warn`.
|
|
359
|
+
*
|
|
360
|
+
* Resolution follows the optional-peer precedent (`agent-instructions.ts`):
|
|
361
|
+
* the backend is not a peer of this plugin, so the plugin anchor is tried
|
|
362
|
+
* first (flat layouts) and the `@deepseek-ai/dsh` host anchor second — the
|
|
363
|
+
* same host-anchor fallback pattern as `dsh-version.ts` — because the
|
|
364
|
+
* backend rides inside the harness meta package's own dependency tree.
|
|
365
|
+
*
|
|
366
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/pty-marker
|
|
367
|
+
*/
|
|
368
|
+
/** The env key that identifies an agent-launched PTY spawn spec. */
|
|
369
|
+
const AGENT_PTY_MARKER = "DSH_PTY_SESSION_ID";
|
|
370
|
+
/** The upstream package whose backend owns the marker stamp. */
|
|
371
|
+
const TERMINAL_BACKEND = "@deepseek-ai/dsh-terminal-bash";
|
|
372
|
+
/** The harness meta package the backend rides inside (its dependency). */
|
|
373
|
+
const HOST_ANCHOR = "@deepseek-ai/dsh/package.json";
|
|
374
|
+
/** Scan budget: enough for any plausible backend layout, bounded forever. */
|
|
375
|
+
const MAX_SOURCE_FILES = 256;
|
|
376
|
+
const MAX_TOTAL_BYTES = 8388608;
|
|
377
|
+
/**
|
|
378
|
+
* Raised when the installed terminal backend does not uphold the marker
|
|
379
|
+
* contract. Thrown from `apply` like `DshCompatUnsupportedError`, so it
|
|
380
|
+
* surfaces as a plugin load failure with the diagnosis attached.
|
|
381
|
+
*/
|
|
382
|
+
var PtyMarkerContractError = class extends Error {
|
|
383
|
+
/** The resolved backend path when one was found. */
|
|
384
|
+
backend;
|
|
385
|
+
constructor(message, backend) {
|
|
386
|
+
super(message);
|
|
387
|
+
this.name = "PtyMarkerContractError";
|
|
388
|
+
this.backend = backend;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
/**
|
|
392
|
+
* Resolve the backend package root through the two anchors the contract can
|
|
393
|
+
* reach: this module first (flat layouts where the backend is a direct
|
|
394
|
+
* install), then the `@deepseek-ai/dsh` host manifest (pnpm-style layouts
|
|
395
|
+
* where it is the meta package's own dependency — the
|
|
396
|
+
* host-anchor fallback pattern in `dsh-version.ts`). A resolution failure that is not a
|
|
397
|
+
* genuine NOT_FOUND propagates: a resolvable-but-broken specifier is not
|
|
398
|
+
* evidence of absence.
|
|
399
|
+
*/
|
|
400
|
+
function resolveBackendRoot() {
|
|
401
|
+
const self = createRequire(import.meta.url);
|
|
402
|
+
const anchors = [self];
|
|
403
|
+
try {
|
|
404
|
+
anchors.push(createRequire(self.resolve(HOST_ANCHOR)));
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (!isNotFound(error)) throw error;
|
|
407
|
+
}
|
|
408
|
+
for (const anchor of anchors) try {
|
|
409
|
+
return dirname(anchor.resolve(`${TERMINAL_BACKEND}/package.json`));
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (!isNotFound(error)) throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function isNotFound(error) {
|
|
415
|
+
const code = error?.code;
|
|
416
|
+
return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
|
|
417
|
+
}
|
|
418
|
+
/** Recursively collect `.js` sources under the package root, within budget. */
|
|
419
|
+
function listSources(root) {
|
|
420
|
+
const files = [];
|
|
421
|
+
const visit = (dir) => {
|
|
422
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
423
|
+
if (files.length >= MAX_SOURCE_FILES) return;
|
|
424
|
+
const path = join(dir, entry.name);
|
|
425
|
+
if (entry.isDirectory() && entry.name !== "node_modules") visit(path);
|
|
426
|
+
else if (entry.isFile() && entry.name.endsWith(".js")) files.push(path);
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
visit(root);
|
|
430
|
+
return files;
|
|
431
|
+
}
|
|
432
|
+
const defaultIo = {
|
|
433
|
+
resolveBackendRoot,
|
|
434
|
+
listSources,
|
|
435
|
+
read: (path) => readFileSync(path, "utf8")
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* Judge the installed terminal backend against the marker contract.
|
|
439
|
+
*
|
|
440
|
+
* Never throws: every outcome — absent package, unreadable source, missing
|
|
441
|
+
* marker — is folded into the verdict so the caller's reaction is a policy
|
|
442
|
+
* decision in one place.
|
|
443
|
+
* @param io - filesystem seams; tests inject them, production uses real fs.
|
|
444
|
+
* @returns the verdict.
|
|
445
|
+
*/
|
|
446
|
+
function agentPtyMarkerVerdict(io = defaultIo) {
|
|
447
|
+
let root;
|
|
448
|
+
try {
|
|
449
|
+
root = io.resolveBackendRoot();
|
|
450
|
+
} catch {
|
|
451
|
+
return "unverifiable";
|
|
452
|
+
}
|
|
453
|
+
if (root === void 0) return "uninstalled";
|
|
454
|
+
let files;
|
|
455
|
+
try {
|
|
456
|
+
files = io.listSources(root);
|
|
457
|
+
} catch {
|
|
458
|
+
return "unverifiable";
|
|
459
|
+
}
|
|
460
|
+
let remaining = MAX_TOTAL_BYTES;
|
|
461
|
+
for (const file of files) {
|
|
462
|
+
let content;
|
|
463
|
+
try {
|
|
464
|
+
content = io.read(file);
|
|
465
|
+
} catch {
|
|
466
|
+
return "unverifiable";
|
|
467
|
+
}
|
|
468
|
+
if (content.length > remaining) return "unverifiable";
|
|
469
|
+
if (content.includes("DSH_PTY_SESSION_ID")) return "stamped";
|
|
470
|
+
remaining -= content.length;
|
|
471
|
+
}
|
|
472
|
+
return "unverifiable";
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Enforce the contract: refuse when the marker cannot be confirmed.
|
|
476
|
+
*
|
|
477
|
+
* @param io - filesystem seams; tests inject them.
|
|
478
|
+
* @throws {PtyMarkerContractError} on `unverifiable`.
|
|
479
|
+
*/
|
|
480
|
+
function assertAgentPtyMarker(io = defaultIo) {
|
|
481
|
+
if (agentPtyMarkerVerdict(io) !== "unverifiable") return;
|
|
482
|
+
let backend;
|
|
483
|
+
try {
|
|
484
|
+
backend = io.resolveBackendRoot();
|
|
485
|
+
} catch {}
|
|
486
|
+
throw new PtyMarkerContractError(`multi-root workspace: the installed ${TERMINAL_BACKEND} does not stamp ${AGENT_PTY_MARKER} on agent PTY spawns, or its sources cannot be verified. That marker is the only fact that separates an agent terminal from the user's own — without it the process fence cannot confine a restricted danger-full-access agent PTY without confining the user's terminal too. The plugin refuses to run rather than let agent terminals pass unconfined. See docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md.`, backend);
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/compat.ts
|
|
490
|
+
/**
|
|
491
|
+
* `multi-root-compat`: the startup gate that turns the DSH compatibility
|
|
492
|
+
* contract from documentation into a precondition.
|
|
493
|
+
*
|
|
494
|
+
* Every widened-authority row of this bundle — `multi-root-providers`,
|
|
495
|
+
* `multi-root-registry`, `multi-root-instructions`, `multi-root-lsp`,
|
|
496
|
+
* `multi-root-workspace-files`, `multi-root-process-fence` — injects
|
|
497
|
+
* `multiRootCompat`. Cordis will not start a plugin whose injected service is
|
|
498
|
+
* absent, so this row is what decides whether they run at all:
|
|
499
|
+
*
|
|
500
|
+
* ```text
|
|
501
|
+
* compat check passes → ctx.multiRootCompat exists → the gated rows start
|
|
502
|
+
* compat check fails → the row throws → they never start
|
|
503
|
+
* ```
|
|
504
|
+
*
|
|
505
|
+
* The two PROVIDER rows (`multi-root-fs`, `multi-root-sandbox`) deliberately
|
|
506
|
+
* do NOT inject the gate: their widening is scope-driven, and the gated
|
|
507
|
+
* registry can never populate the scope on a refused installation — so they
|
|
508
|
+
* degenerate to the upstream providers. That is what keeps a refused gate a
|
|
509
|
+
* loud degradation (the plugin's features absent, the profile intact) instead
|
|
510
|
+
* of a dead profile: upstream `fs-sandbox`/`sandbox` are already disabled by
|
|
511
|
+
* the patch, so holding `ctx.fs`/`ctx.sandbox` back would take `ctx.shell`,
|
|
512
|
+
* `permissionPresets` and every confining tool down with them.
|
|
513
|
+
*
|
|
514
|
+
* That is why the gate is a plain `apply` and not a `Service` subclass mounted
|
|
515
|
+
* directly: the check has to be able to fail BEFORE the service exists. A
|
|
516
|
+
* `Service` constructor has already run `super(ctx, name)` — and therefore
|
|
517
|
+
* already claimed the service key — by the time it could inspect anything.
|
|
518
|
+
*
|
|
519
|
+
* The verdict itself, and the allowlist it is judged against, live in
|
|
520
|
+
* `./compat/dsh-version.ts`. This module owns only the reaction to it.
|
|
521
|
+
*
|
|
522
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/compat
|
|
523
|
+
*/
|
|
524
|
+
/**
|
|
525
|
+
* Raised when the installed DSH release is not one this plugin is verified
|
|
526
|
+
* against. It is thrown from `apply`, so it surfaces as a plugin load failure
|
|
527
|
+
* with the full diagnosis attached — never as a silently degraded sandbox.
|
|
528
|
+
*/
|
|
529
|
+
var DshCompatUnsupportedError = class extends Error {
|
|
530
|
+
/** The report that produced the refusal. */
|
|
531
|
+
report;
|
|
532
|
+
constructor(report) {
|
|
533
|
+
super(report.message);
|
|
534
|
+
this.name = "DshCompatUnsupportedError";
|
|
535
|
+
this.report = report;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
/**
|
|
539
|
+
* The `ctx.multiRootCompat` service: the resolved compatibility verdict, held
|
|
540
|
+
* where the gated providers can read it.
|
|
541
|
+
*
|
|
542
|
+
* It intentionally exposes no per-version capability flags. The adapters under
|
|
543
|
+
* `./compat/` detect the shape they need STRUCTURALLY — whether a return value
|
|
544
|
+
* is a thenable, which export name a module carries — because a structural
|
|
545
|
+
* probe also survives upstream reshaping something within one release, while a
|
|
546
|
+
* version comparison does not. The release string here is for the gate and for
|
|
547
|
+
* diagnostics.
|
|
548
|
+
*/
|
|
549
|
+
var MultiRootCompatService = class extends Service {
|
|
550
|
+
/** The verdict this composition started under. */
|
|
551
|
+
report;
|
|
552
|
+
constructor(ctx, report) {
|
|
553
|
+
super(ctx, "multiRootCompat");
|
|
554
|
+
this.report = report;
|
|
555
|
+
}
|
|
556
|
+
/** The single DSH release every inspected core package agreed on. */
|
|
557
|
+
get release() {
|
|
558
|
+
return this.report.release;
|
|
559
|
+
}
|
|
560
|
+
/** Whether the installation is on the allowlist. */
|
|
561
|
+
get supported() {
|
|
562
|
+
return this.report.verdict === "supported";
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
/** This row needs nothing: it is the thing everything else waits for. */
|
|
566
|
+
const inject = [];
|
|
567
|
+
/**
|
|
568
|
+
* Apply the gate policy to one report.
|
|
569
|
+
*
|
|
570
|
+
* Under `DSH_MULTI_ROOT_COMPAT=warn` exactly one verdict is relaxed: an
|
|
571
|
+
* `unsupported` release that every inspected package agrees on. That is the
|
|
572
|
+
* only shape the upgrade smoke probes — a coherent tree on a release the
|
|
573
|
+
* allowlist does not name yet, which is how a release earns its place there.
|
|
574
|
+
* `mixed` and `incomplete` describe an installation the contract cannot judge
|
|
575
|
+
* at all, so they are refused in either mode; production deployments leave the
|
|
576
|
+
* variable unset and get the refusal for everything.
|
|
577
|
+
* @param report - the verdict to act on.
|
|
578
|
+
* @param enforcement - how to react to a non-`supported` verdict.
|
|
579
|
+
* @returns the warning to log, or `undefined` when there is nothing to say.
|
|
580
|
+
* @throws {DshCompatUnsupportedError} when refusing to run.
|
|
581
|
+
*/
|
|
582
|
+
function assertSupportedInstallation(report, enforcement) {
|
|
583
|
+
if (report.verdict === "supported") return void 0;
|
|
584
|
+
if (enforcement === "enforce" || report.verdict !== "unsupported") throw new DshCompatUnsupportedError(report);
|
|
585
|
+
return `${report.message}\n\nProceeding anyway because ${COMPAT_ENFORCEMENT_ENV}=warn. This is an upgrade-smoke mode: additional workspace roots are about to be granted through a sandbox profile shape that has not been verified on this release. Supported releases: ${SUPPORTED_DSH_RELEASES.join(", ")}.`;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Judge the installation, then either refuse to provide the service or provide
|
|
589
|
+
* it.
|
|
590
|
+
* @param ctx - the row's context.
|
|
591
|
+
*/
|
|
592
|
+
function apply(ctx) {
|
|
593
|
+
const report = classifyInstallation();
|
|
594
|
+
const warning = assertSupportedInstallation(report, compatEnforcement());
|
|
595
|
+
if (warning !== void 0) ctx.logger.warn(warning);
|
|
596
|
+
assertAgentPtyMarker();
|
|
597
|
+
ctx.plugin(MultiRootCompatService, report);
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
export { classifyInstallation as _, inject as a, agentPtyMarkerVerdict as c, HOST_SERVICE_PACKAGES as d, OPTIONAL_CORE_PACKAGES as f, SUPPORTED_DSH_RELEASES as g, RUNTIME_OPTIONAL_PACKAGES as h, assertSupportedInstallation as i, assertAgentPtyMarker as l, RUNNER_ANCHORED_PACKAGES as m, MultiRootCompatService as n, AGENT_PTY_MARKER as o, REQUIRED_CORE_PACKAGES as p, apply as r, PtyMarkerContractError as s, DshCompatUnsupportedError as t, CORE_PACKAGES as u, compatEnforcement as v, readInstalledVersion as y };
|
package/lib/compat.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-
|
|
1
|
+
import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-DR2LvmYZ.js";
|
|
2
2
|
export { DshCompatUnsupportedError, MultiRootCompatService, apply, assertSupportedInstallation, inject };
|