@dsh-electron/dsh-plugin-multi-root-workspace 0.1.14 → 0.1.15

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 (61) hide show
  1. package/CHANGELOG.en.md +20 -1
  2. package/CHANGELOG.md +20 -1
  3. package/README.en.md +24 -7
  4. package/README.md +24 -7
  5. package/cordis.patch.yml +5 -1
  6. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  7. package/lib/auto-review.js +2 -2
  8. package/lib/client.js +208 -26
  9. package/lib/client.js.map +1 -1
  10. package/lib/{command-Dcz6nULo.js → command-y5tfcAeC.js} +21 -10
  11. package/lib/command.js +1 -1
  12. package/lib/compat-CTJbpCxu.js +267 -0
  13. package/lib/compat.js +1 -1
  14. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  15. package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
  16. package/lib/index.js +10 -8
  17. package/lib/{instructions-KsUwUFE0.js → instructions-Ck9wp4J4.js} +695 -26
  18. package/lib/instructions.js +1 -1
  19. package/lib/llm-message-CepyzEBQ.js +84 -0
  20. package/lib/lsp.js +3 -3
  21. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  22. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  23. package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
  24. package/lib/ops-compatibility.js +58 -0
  25. package/lib/ops-settings-UN2oOKEp.js +40 -0
  26. package/lib/ops-tools-k4JYaOrM.js +152 -0
  27. package/lib/process-fence.js +100 -24
  28. package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
  29. package/lib/{registry-DR_sMuvS.js → registry-DyDNDSJZ.js} +235 -79
  30. package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
  31. package/lib/registry.js +2 -2
  32. package/lib/{review-context-NDpKeMCc.js → review-context-B-oHkU6a.js} +24 -7
  33. package/lib/review-context.js +1 -1
  34. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  35. package/lib/{sandbox-C-vCJCp3.js → sandbox-CZElTNPz.js} +1 -1
  36. package/lib/sandbox.js +1 -1
  37. package/lib/scope.js +72 -9
  38. package/lib/skills-utyuvmwr.js +104 -0
  39. package/lib/skills.js +4 -105
  40. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  41. package/lib/types/authority/protocol.d.ts +5 -1
  42. package/lib/types/client/locales.d.ts +28 -0
  43. package/lib/types/client/ops-settings.d.ts +6 -0
  44. package/lib/types/client/skills-settings.d.ts +3 -2
  45. package/lib/types/command.d.ts +3 -3
  46. package/lib/types/compat/instruction-activity.d.ts +32 -1
  47. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  48. package/lib/types/compat/ops-settings.d.ts +11 -0
  49. package/lib/types/compat/ops-tools.d.ts +25 -0
  50. package/lib/types/compat/skills-client.d.ts +1 -0
  51. package/lib/types/compat/tool-context.d.ts +4 -0
  52. package/lib/types/compat.d.ts +2 -2
  53. package/lib/types/instructions.d.ts +6 -1
  54. package/lib/types/ops-compatibility.d.ts +34 -0
  55. package/lib/types/registry.d.ts +67 -8
  56. package/lib/types/review-context.d.ts +2 -0
  57. package/lib/types/roots.d.ts +114 -20
  58. package/lib/types/scope.d.ts +28 -2
  59. package/lib/workspace-files.js +3 -3
  60. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  61. package/package.json +5 -1
@@ -1,12 +1,11 @@
1
1
  import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
- import { r as canonicalPath$1 } from "./windows-runner-contract-B5R2K3mM.js";
3
- import { i as canonicalRoot, o as entryRootRef, p as resolveRootRef, r as availableRoots, t as RootValidationError } from "./roots-Ca_W1S9n.js";
2
+ import { r as canonicalPath } from "./windows-runner-contract-B5R2K3mM.js";
3
+ import { g as resolveRootRef, i as canonicalRoot, o as entryRootRef, r as availableRoots, t as RootValidationError } from "./roots-CmaWxRVy.js";
4
4
  import { c as skillsRequestSchema, i as parsePanelCall, r as effectiveRootAccess, s as SKILLS_ENDPOINTS, t as PANEL_CHANNEL } from "./contract-CStSEOcK.js";
5
- import { n as AuthorityRpcError } from "./protocol-CnR1KTXy.js";
5
+ import { n as AuthorityRpcError } from "./protocol-CwDnFEyK.js";
6
6
  import { closeSync, lstatSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { isAbsolute, join } from "node:path";
8
8
  import "@deepseek-ai/cordis";
9
- import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
10
9
  //#region src/compat/connection-rpc.ts
11
10
  /**
12
11
  * Newer Cordis keeps a service getter's definition site when tracing it, so
@@ -397,7 +396,7 @@ function inspectHome(home) {
397
396
  throw new Error(`the DSH home ${home} cannot be inspected: ${describeError(error)}`);
398
397
  }
399
398
  if (stat.isSymbolicLink()) throw new Error(`the DSH home ${home} is a reparse point — refusing to work through it`);
400
- const canonical = canonicalPath$1(home);
399
+ const canonical = canonicalPath(home);
401
400
  if (!stat.isDirectory()) throw new Error(`the DSH home ${canonical} is not a directory`);
402
401
  const scan = {
403
402
  directories: 0,
@@ -649,8 +648,8 @@ function remediationSteps(report) {
649
648
  //#endregion
650
649
  //#region src/command.ts
651
650
  /**
652
- * The user-facing surface: the `/workspace-folders` command and the panel's
653
- * RPC channel.
651
+ * The user-facing surface: the `/workspace-folders` command, its `/add-dirs`
652
+ * alias for adding a root, and the panel's RPC channel.
654
653
  *
655
654
  * They share one module because they are one thing — the operator-facing edge
656
655
  * over `ctx.multiRootRegistry`. The command serves the text surfaces (the
@@ -685,6 +684,8 @@ const inject = [
685
684
  ];
686
685
  /** The command name, without the leading slash. */
687
686
  const COMMAND_NAME = "workspace-folders";
687
+ /** The shortcut for `/workspace-folders add`, without the leading slash. */
688
+ const ADD_COMMAND_NAME = "add-dirs";
688
689
  /**
689
690
  * Parse the text that follows `/workspace-folders`.
690
691
  * @param rawInput - the verbatim `rawInput` of the command invocation.
@@ -772,6 +773,15 @@ function apply(ctx, config = {}) {
772
773
  input: { hint: "[list|add [path] [--inherit|--read-only|--read-write]|remove <n|path>|alias <n|path> [name]|alias-id <n|path> <id>|skills <n|path> on|off|access <n|path> <level>|reveal <n|path>|isolate-profiles [--apply]|help]" },
773
774
  handler: async (invocation) => await runCommand(ctx, invocation, config)
774
775
  });
776
+ ctx.commands.register({
777
+ name: ADD_COMMAND_NAME,
778
+ description: `Add an additional workspace root (alias for /${COMMAND_NAME} add)`,
779
+ input: { hint: "[absolute path] [--inherit|--read-only|--read-write]" },
780
+ handler: async (invocation) => await runCommand(ctx, {
781
+ ...invocation,
782
+ rawInput: `add ${invocation.rawInput}`
783
+ }, config)
784
+ });
775
785
  ctx.inject(["connection", "webServer"], (connectionCtx) => {
776
786
  const connection = connectionCtx.get("connection");
777
787
  if (connection === void 0) {
@@ -957,6 +967,7 @@ function helpText() {
957
967
  `/${COMMAND_NAME} — additional workspace roots of this session`,
958
968
  ` /${COMMAND_NAME} list`,
959
969
  ` /${COMMAND_NAME} add [absolute path] [--inherit|--read-only|--read-write] (no path opens the directory picker when one is composed)`,
970
+ ` /${ADD_COMMAND_NAME} [absolute path] [--inherit|--read-only|--read-write] (alias for /${COMMAND_NAME} add)`,
960
971
  ` /${COMMAND_NAME} remove <n|path>`,
961
972
  ` /${COMMAND_NAME} alias <n|path> [name] (no name clears the alias)`,
962
973
  ` /${COMMAND_NAME} access <n|path> inherit|read-only|read-write`,
@@ -1092,15 +1103,15 @@ async function targetOf(registry, primaryRoot, text) {
1092
1103
  *
1093
1104
  * @param ctx - the host context that may carry `sessions`.
1094
1105
  * @param sessionId - the id the panel sent; already required by the wire schema.
1095
- * @returns the canonical cwd of that session.
1106
+ * @returns the session's cwd spelling, verbatim — the Registry key.
1096
1107
  * @throws {RootValidationError} `session-not-found` when the service is absent,
1097
1108
  * the id does not name a live session, or that session has no cwd.
1098
1109
  */
1099
1110
  function resolvePanelPrimaryRoot(ctx, sessionId) {
1100
1111
  const session = ctx.get("sessions")?.get(sessionId);
1101
1112
  const cwd = session?.header.cwd;
1102
- if (session === void 0 || cwd === void 0 || cwd === "") throw new RootValidationError("session-not-found", `session "${sessionId}" is not an active host session`, { reference: sessionId });
1103
- return canonicalPath(cwd);
1113
+ if (session === void 0 || cwd === void 0 || cwd === "" || !isAbsolute(cwd)) throw new RootValidationError("session-not-found", `session "${sessionId}" is not an active host session`, { reference: sessionId });
1114
+ return cwd;
1104
1115
  }
1105
1116
  /** Project one status into the panel's view. */
1106
1117
  function toRootView(status, index) {
package/lib/command.js CHANGED
@@ -1,2 +1,2 @@
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-Dcz6nULo.js";
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-y5tfcAeC.js";
2
2
  export { COMMAND_NAME, apply, inject, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealAccepted, revealArgv };
@@ -0,0 +1,267 @@
1
+ import { c as SUPPORTED_DSH_RELEASES, l as classifyInstallation, t as COMPAT_ENFORCEMENT_ENV, u as compatEnforcement } from "./dsh-version-DCwF7REk.js";
2
+ import { createRequire } from "node:module";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { Service } from "@deepseek-ai/cordis";
6
+ //#region src/compat/pty-marker.ts
7
+ /**
8
+ * Boot-time probe for the agent-PTY marker contract.
9
+ *
10
+ * `multi-root-process-fence` tells an agent terminal from the user's own by
11
+ * one env fact: every supported `dsh-terminal-bash` backend stamps
12
+ * `DSH_PTY_SESSION_ID` (alongside `DSH_SESSION_ID`) into the PTY spawn spec,
13
+ * while the panel's terminal controller stamps only `DSH_SESSION_ID`. That
14
+ * marker is the ONLY discriminator — a backend that ever stops stamping it
15
+ * would reclassify agent PTYs as user terminals and pass them through
16
+ * unconfined under a restricted `danger-full-access`, which is a silent
17
+ * fail-open on a security boundary.
18
+ *
19
+ * The marker is an env key inside upstream's emitted code, not an exported
20
+ * symbol, so the honest structural probe is content: the installed backend
21
+ * package must still write the marker. This module is called from the
22
+ * `multi-root-compat` gate (`src/compat.ts`) BEFORE the service registers —
23
+ * like the version verdict it is a precondition, not a diagnostic, and a
24
+ * breach refuses the composition the same way `mixed`/`incomplete` do: it is
25
+ * not relaxable by `DSH_MULTI_ROOT_COMPAT=warn`.
26
+ *
27
+ * Resolution follows the optional-peer precedent (`agent-instructions.ts`):
28
+ * the backend is not a peer of this plugin, so the plugin anchor is tried
29
+ * first (flat layouts) and the `@deepseek-ai/dsh` host anchor second — the
30
+ * same host-anchor fallback pattern as `dsh-version.ts` — because the
31
+ * backend rides inside the harness meta package's own dependency tree.
32
+ *
33
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/pty-marker
34
+ */
35
+ /** The env key that identifies an agent-launched PTY spawn spec. */
36
+ const AGENT_PTY_MARKER = "DSH_PTY_SESSION_ID";
37
+ /** The upstream package whose backend owns the marker stamp. */
38
+ const TERMINAL_BACKEND = "@deepseek-ai/dsh-terminal-bash";
39
+ /** The harness meta package the backend rides inside (its dependency). */
40
+ const HOST_ANCHOR = "@deepseek-ai/dsh/package.json";
41
+ /** Scan budget: enough for any plausible backend layout, bounded forever. */
42
+ const MAX_SOURCE_FILES = 256;
43
+ const MAX_TOTAL_BYTES = 8388608;
44
+ /**
45
+ * Raised when the installed terminal backend does not uphold the marker
46
+ * contract. Thrown from `apply` like `DshCompatUnsupportedError`, so it
47
+ * surfaces as a plugin load failure with the diagnosis attached.
48
+ */
49
+ var PtyMarkerContractError = class extends Error {
50
+ /** The resolved backend path when one was found. */
51
+ backend;
52
+ constructor(message, backend) {
53
+ super(message);
54
+ this.name = "PtyMarkerContractError";
55
+ this.backend = backend;
56
+ }
57
+ };
58
+ /**
59
+ * Resolve the backend package root through the two anchors the contract can
60
+ * reach: this module first (flat layouts where the backend is a direct
61
+ * install), then the `@deepseek-ai/dsh` host manifest (pnpm-style layouts
62
+ * where it is the meta package's own dependency — the
63
+ * host-anchor fallback pattern in `dsh-version.ts`). A resolution failure that is not a
64
+ * genuine NOT_FOUND propagates: a resolvable-but-broken specifier is not
65
+ * evidence of absence.
66
+ */
67
+ function resolveBackendRoot() {
68
+ const self = createRequire(import.meta.url);
69
+ const anchors = [self];
70
+ try {
71
+ anchors.push(createRequire(self.resolve(HOST_ANCHOR)));
72
+ } catch (error) {
73
+ if (!isNotFound(error)) throw error;
74
+ }
75
+ for (const anchor of anchors) try {
76
+ return dirname(anchor.resolve(`${TERMINAL_BACKEND}/package.json`));
77
+ } catch (error) {
78
+ if (!isNotFound(error)) throw error;
79
+ }
80
+ }
81
+ function isNotFound(error) {
82
+ const code = error?.code;
83
+ return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
84
+ }
85
+ /** Recursively collect `.js` sources under the package root, within budget. */
86
+ function listSources(root) {
87
+ const files = [];
88
+ const visit = (dir) => {
89
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
90
+ if (files.length >= MAX_SOURCE_FILES) return;
91
+ const path = join(dir, entry.name);
92
+ if (entry.isDirectory() && entry.name !== "node_modules") visit(path);
93
+ else if (entry.isFile() && entry.name.endsWith(".js")) files.push(path);
94
+ }
95
+ };
96
+ visit(root);
97
+ return files;
98
+ }
99
+ const defaultIo = {
100
+ resolveBackendRoot,
101
+ listSources,
102
+ read: (path) => readFileSync(path, "utf8")
103
+ };
104
+ /**
105
+ * Judge the installed terminal backend against the marker contract.
106
+ *
107
+ * Never throws: every outcome — absent package, unreadable source, missing
108
+ * marker — is folded into the verdict so the caller's reaction is a policy
109
+ * decision in one place.
110
+ * @param io - filesystem seams; tests inject them, production uses real fs.
111
+ * @returns the verdict.
112
+ */
113
+ function agentPtyMarkerVerdict(io = defaultIo) {
114
+ let root;
115
+ try {
116
+ root = io.resolveBackendRoot();
117
+ } catch {
118
+ return "unverifiable";
119
+ }
120
+ if (root === void 0) return "uninstalled";
121
+ let files;
122
+ try {
123
+ files = io.listSources(root);
124
+ } catch {
125
+ return "unverifiable";
126
+ }
127
+ let remaining = MAX_TOTAL_BYTES;
128
+ for (const file of files) {
129
+ let content;
130
+ try {
131
+ content = io.read(file);
132
+ } catch {
133
+ return "unverifiable";
134
+ }
135
+ if (content.length > remaining) return "unverifiable";
136
+ if (content.includes("DSH_PTY_SESSION_ID")) return "stamped";
137
+ remaining -= content.length;
138
+ }
139
+ return "unverifiable";
140
+ }
141
+ /**
142
+ * Enforce the contract: refuse when the marker cannot be confirmed.
143
+ *
144
+ * @param io - filesystem seams; tests inject them.
145
+ * @throws {PtyMarkerContractError} on `unverifiable`.
146
+ */
147
+ function assertAgentPtyMarker(io = defaultIo) {
148
+ if (agentPtyMarkerVerdict(io) !== "unverifiable") return;
149
+ let backend;
150
+ try {
151
+ backend = io.resolveBackendRoot();
152
+ } catch {}
153
+ 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);
154
+ }
155
+ //#endregion
156
+ //#region src/compat.ts
157
+ /**
158
+ * `multi-root-compat`: the startup gate that turns the DSH compatibility
159
+ * contract from documentation into a precondition.
160
+ *
161
+ * Every widened-authority row of this bundle — `multi-root-providers`,
162
+ * `multi-root-registry`, `multi-root-instructions`, `multi-root-lsp`,
163
+ * `multi-root-workspace-files`, `multi-root-process-fence` — injects
164
+ * `multiRootCompat`. Cordis will not start a plugin whose injected service is
165
+ * absent, so this row is what decides whether they run at all:
166
+ *
167
+ * ```text
168
+ * compat check passes → ctx.multiRootCompat exists → the gated rows start
169
+ * compat check fails → the row throws → they never start
170
+ * ```
171
+ *
172
+ * The two PROVIDER rows (`multi-root-fs`, `multi-root-sandbox`) deliberately
173
+ * do NOT inject the gate: their widening is scope-driven, and the gated
174
+ * registry can never populate the scope on a refused installation — so they
175
+ * degenerate to the upstream providers. That is what keeps a refused gate a
176
+ * loud degradation (the plugin's features absent, the profile intact) instead
177
+ * of a dead profile: upstream `fs-sandbox`/`sandbox` are already disabled by
178
+ * the patch, so holding `ctx.fs`/`ctx.sandbox` back would take `ctx.shell`,
179
+ * `permissionPresets` and every confining tool down with them.
180
+ *
181
+ * That is why the gate is a plain `apply` and not a `Service` subclass mounted
182
+ * directly: the check has to be able to fail BEFORE the service exists. A
183
+ * `Service` constructor has already run `super(ctx, name)` — and therefore
184
+ * already claimed the service key — by the time it could inspect anything.
185
+ *
186
+ * The verdict itself, and the allowlist it is judged against, live in
187
+ * `./compat/dsh-version.ts`. This module owns only the reaction to it.
188
+ *
189
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/compat
190
+ */
191
+ /**
192
+ * Raised when the installed DSH release is not one this plugin is verified
193
+ * against. It is thrown from `apply`, so it surfaces as a plugin load failure
194
+ * with the full diagnosis attached — never as a silently degraded sandbox.
195
+ */
196
+ var DshCompatUnsupportedError = class extends Error {
197
+ /** The report that produced the refusal. */
198
+ report;
199
+ constructor(report) {
200
+ super(report.message);
201
+ this.name = "DshCompatUnsupportedError";
202
+ this.report = report;
203
+ }
204
+ };
205
+ /**
206
+ * The `ctx.multiRootCompat` service: the resolved compatibility verdict, held
207
+ * where the gated providers can read it.
208
+ *
209
+ * It intentionally exposes no per-version capability flags. The adapters under
210
+ * `./compat/` detect the shape they need STRUCTURALLY — whether a return value
211
+ * is a thenable, which export name a module carries — because a structural
212
+ * probe also survives upstream reshaping something within one release, while a
213
+ * version comparison does not. The release string here is for installation and
214
+ * feature-availability gates (such as the OPS 0.2.* policy), and diagnostics.
215
+ */
216
+ var MultiRootCompatService = class extends Service {
217
+ /** The verdict this composition started under. */
218
+ report;
219
+ constructor(ctx, report) {
220
+ super(ctx, "multiRootCompat");
221
+ this.report = report;
222
+ }
223
+ /** The single DSH release every inspected core package agreed on. */
224
+ get release() {
225
+ return this.report.release;
226
+ }
227
+ /** Whether the installation is on the allowlist. */
228
+ get supported() {
229
+ return this.report.verdict === "supported";
230
+ }
231
+ };
232
+ /** This row needs nothing: it is the thing everything else waits for. */
233
+ const inject = [];
234
+ /**
235
+ * Apply the gate policy to one report.
236
+ *
237
+ * Under `DSH_MULTI_ROOT_COMPAT=warn` exactly one verdict is relaxed: an
238
+ * `unsupported` release that every inspected package agrees on. That is the
239
+ * only shape the upgrade smoke probes — a coherent tree on a release the
240
+ * allowlist does not name yet, which is how a release earns its place there.
241
+ * `mixed` and `incomplete` describe an installation the contract cannot judge
242
+ * at all, so they are refused in either mode; production deployments leave the
243
+ * variable unset and get the refusal for everything.
244
+ * @param report - the verdict to act on.
245
+ * @param enforcement - how to react to a non-`supported` verdict.
246
+ * @returns the warning to log, or `undefined` when there is nothing to say.
247
+ * @throws {DshCompatUnsupportedError} when refusing to run.
248
+ */
249
+ function assertSupportedInstallation(report, enforcement) {
250
+ if (report.verdict === "supported") return void 0;
251
+ if (enforcement === "enforce" || report.verdict !== "unsupported") throw new DshCompatUnsupportedError(report);
252
+ 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(", ")}.`;
253
+ }
254
+ /**
255
+ * Judge the installation, then either refuse to provide the service or provide
256
+ * it.
257
+ * @param ctx - the row's context.
258
+ */
259
+ function apply(ctx) {
260
+ const report = classifyInstallation();
261
+ const warning = assertSupportedInstallation(report, compatEnforcement());
262
+ if (warning !== void 0) ctx.logger.warn(warning);
263
+ assertAgentPtyMarker();
264
+ ctx.plugin(MultiRootCompatService, report);
265
+ }
266
+ //#endregion
267
+ export { inject as a, agentPtyMarkerVerdict as c, assertSupportedInstallation as i, assertAgentPtyMarker as l, MultiRootCompatService as n, AGENT_PTY_MARKER as o, apply as r, PtyMarkerContractError as s, DshCompatUnsupportedError as t };
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-veDqCOoa.js";
1
+ import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-CTJbpCxu.js";
2
2
  export { DshCompatUnsupportedError, MultiRootCompatService, apply, assertSupportedInstallation, inject };
@@ -1,5 +1,5 @@
1
1
  import { t as DialectUnrecognizedError } from "./windows-runner-contract-B5R2K3mM.js";
2
- import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
2
+ import { l as isCanonicallyUnder } from "./roots-CmaWxRVy.js";
3
3
  import { posix } from "node:path";
4
4
  //#region src/compat/sandbox-confine.ts
5
5
  /** Whether a value is a thenable, i.e. whether the installed `confine` is async. */
@@ -1,7 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, sep } from "node:path";
4
- import { Service } from "@deepseek-ai/cordis";
5
4
  import { fileURLToPath } from "node:url";
6
5
  //#region src/compat/dsh-version.ts
7
6
  /**
@@ -354,265 +353,4 @@ function report(verdict, release, packages, lines) {
354
353
  };
355
354
  }
356
355
  //#endregion
357
- //#region src/compat/pty-marker.ts
358
- /**
359
- * Boot-time probe for the agent-PTY marker contract.
360
- *
361
- * `multi-root-process-fence` tells an agent terminal from the user's own by
362
- * one env fact: every supported `dsh-terminal-bash` backend stamps
363
- * `DSH_PTY_SESSION_ID` (alongside `DSH_SESSION_ID`) into the PTY spawn spec,
364
- * while the panel's terminal controller stamps only `DSH_SESSION_ID`. That
365
- * marker is the ONLY discriminator — a backend that ever stops stamping it
366
- * would reclassify agent PTYs as user terminals and pass them through
367
- * unconfined under a restricted `danger-full-access`, which is a silent
368
- * fail-open on a security boundary.
369
- *
370
- * The marker is an env key inside upstream's emitted code, not an exported
371
- * symbol, so the honest structural probe is content: the installed backend
372
- * package must still write the marker. This module is called from the
373
- * `multi-root-compat` gate (`src/compat.ts`) BEFORE the service registers —
374
- * like the version verdict it is a precondition, not a diagnostic, and a
375
- * breach refuses the composition the same way `mixed`/`incomplete` do: it is
376
- * not relaxable by `DSH_MULTI_ROOT_COMPAT=warn`.
377
- *
378
- * Resolution follows the optional-peer precedent (`agent-instructions.ts`):
379
- * the backend is not a peer of this plugin, so the plugin anchor is tried
380
- * first (flat layouts) and the `@deepseek-ai/dsh` host anchor second — the
381
- * same host-anchor fallback pattern as `dsh-version.ts` — because the
382
- * backend rides inside the harness meta package's own dependency tree.
383
- *
384
- * @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/pty-marker
385
- */
386
- /** The env key that identifies an agent-launched PTY spawn spec. */
387
- const AGENT_PTY_MARKER = "DSH_PTY_SESSION_ID";
388
- /** The upstream package whose backend owns the marker stamp. */
389
- const TERMINAL_BACKEND = "@deepseek-ai/dsh-terminal-bash";
390
- /** The harness meta package the backend rides inside (its dependency). */
391
- const HOST_ANCHOR = "@deepseek-ai/dsh/package.json";
392
- /** Scan budget: enough for any plausible backend layout, bounded forever. */
393
- const MAX_SOURCE_FILES = 256;
394
- const MAX_TOTAL_BYTES = 8388608;
395
- /**
396
- * Raised when the installed terminal backend does not uphold the marker
397
- * contract. Thrown from `apply` like `DshCompatUnsupportedError`, so it
398
- * surfaces as a plugin load failure with the diagnosis attached.
399
- */
400
- var PtyMarkerContractError = class extends Error {
401
- /** The resolved backend path when one was found. */
402
- backend;
403
- constructor(message, backend) {
404
- super(message);
405
- this.name = "PtyMarkerContractError";
406
- this.backend = backend;
407
- }
408
- };
409
- /**
410
- * Resolve the backend package root through the two anchors the contract can
411
- * reach: this module first (flat layouts where the backend is a direct
412
- * install), then the `@deepseek-ai/dsh` host manifest (pnpm-style layouts
413
- * where it is the meta package's own dependency — the
414
- * host-anchor fallback pattern in `dsh-version.ts`). A resolution failure that is not a
415
- * genuine NOT_FOUND propagates: a resolvable-but-broken specifier is not
416
- * evidence of absence.
417
- */
418
- function resolveBackendRoot() {
419
- const self = createRequire(import.meta.url);
420
- const anchors = [self];
421
- try {
422
- anchors.push(createRequire(self.resolve(HOST_ANCHOR)));
423
- } catch (error) {
424
- if (!isNotFound(error)) throw error;
425
- }
426
- for (const anchor of anchors) try {
427
- return dirname(anchor.resolve(`${TERMINAL_BACKEND}/package.json`));
428
- } catch (error) {
429
- if (!isNotFound(error)) throw error;
430
- }
431
- }
432
- function isNotFound(error) {
433
- const code = error?.code;
434
- return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
435
- }
436
- /** Recursively collect `.js` sources under the package root, within budget. */
437
- function listSources(root) {
438
- const files = [];
439
- const visit = (dir) => {
440
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
441
- if (files.length >= MAX_SOURCE_FILES) return;
442
- const path = join(dir, entry.name);
443
- if (entry.isDirectory() && entry.name !== "node_modules") visit(path);
444
- else if (entry.isFile() && entry.name.endsWith(".js")) files.push(path);
445
- }
446
- };
447
- visit(root);
448
- return files;
449
- }
450
- const defaultIo = {
451
- resolveBackendRoot,
452
- listSources,
453
- read: (path) => readFileSync(path, "utf8")
454
- };
455
- /**
456
- * Judge the installed terminal backend against the marker contract.
457
- *
458
- * Never throws: every outcome — absent package, unreadable source, missing
459
- * marker — is folded into the verdict so the caller's reaction is a policy
460
- * decision in one place.
461
- * @param io - filesystem seams; tests inject them, production uses real fs.
462
- * @returns the verdict.
463
- */
464
- function agentPtyMarkerVerdict(io = defaultIo) {
465
- let root;
466
- try {
467
- root = io.resolveBackendRoot();
468
- } catch {
469
- return "unverifiable";
470
- }
471
- if (root === void 0) return "uninstalled";
472
- let files;
473
- try {
474
- files = io.listSources(root);
475
- } catch {
476
- return "unverifiable";
477
- }
478
- let remaining = MAX_TOTAL_BYTES;
479
- for (const file of files) {
480
- let content;
481
- try {
482
- content = io.read(file);
483
- } catch {
484
- return "unverifiable";
485
- }
486
- if (content.length > remaining) return "unverifiable";
487
- if (content.includes("DSH_PTY_SESSION_ID")) return "stamped";
488
- remaining -= content.length;
489
- }
490
- return "unverifiable";
491
- }
492
- /**
493
- * Enforce the contract: refuse when the marker cannot be confirmed.
494
- *
495
- * @param io - filesystem seams; tests inject them.
496
- * @throws {PtyMarkerContractError} on `unverifiable`.
497
- */
498
- function assertAgentPtyMarker(io = defaultIo) {
499
- if (agentPtyMarkerVerdict(io) !== "unverifiable") return;
500
- let backend;
501
- try {
502
- backend = io.resolveBackendRoot();
503
- } catch {}
504
- 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);
505
- }
506
- //#endregion
507
- //#region src/compat.ts
508
- /**
509
- * `multi-root-compat`: the startup gate that turns the DSH compatibility
510
- * contract from documentation into a precondition.
511
- *
512
- * Every widened-authority row of this bundle — `multi-root-providers`,
513
- * `multi-root-registry`, `multi-root-instructions`, `multi-root-lsp`,
514
- * `multi-root-workspace-files`, `multi-root-process-fence` — injects
515
- * `multiRootCompat`. Cordis will not start a plugin whose injected service is
516
- * absent, so this row is what decides whether they run at all:
517
- *
518
- * ```text
519
- * compat check passes → ctx.multiRootCompat exists → the gated rows start
520
- * compat check fails → the row throws → they never start
521
- * ```
522
- *
523
- * The two PROVIDER rows (`multi-root-fs`, `multi-root-sandbox`) deliberately
524
- * do NOT inject the gate: their widening is scope-driven, and the gated
525
- * registry can never populate the scope on a refused installation — so they
526
- * degenerate to the upstream providers. That is what keeps a refused gate a
527
- * loud degradation (the plugin's features absent, the profile intact) instead
528
- * of a dead profile: upstream `fs-sandbox`/`sandbox` are already disabled by
529
- * the patch, so holding `ctx.fs`/`ctx.sandbox` back would take `ctx.shell`,
530
- * `permissionPresets` and every confining tool down with them.
531
- *
532
- * That is why the gate is a plain `apply` and not a `Service` subclass mounted
533
- * directly: the check has to be able to fail BEFORE the service exists. A
534
- * `Service` constructor has already run `super(ctx, name)` — and therefore
535
- * already claimed the service key — by the time it could inspect anything.
536
- *
537
- * The verdict itself, and the allowlist it is judged against, live in
538
- * `./compat/dsh-version.ts`. This module owns only the reaction to it.
539
- *
540
- * @module @dsh-electron/dsh-plugin-multi-root-workspace/compat
541
- */
542
- /**
543
- * Raised when the installed DSH release is not one this plugin is verified
544
- * against. It is thrown from `apply`, so it surfaces as a plugin load failure
545
- * with the full diagnosis attached — never as a silently degraded sandbox.
546
- */
547
- var DshCompatUnsupportedError = class extends Error {
548
- /** The report that produced the refusal. */
549
- report;
550
- constructor(report) {
551
- super(report.message);
552
- this.name = "DshCompatUnsupportedError";
553
- this.report = report;
554
- }
555
- };
556
- /**
557
- * The `ctx.multiRootCompat` service: the resolved compatibility verdict, held
558
- * where the gated providers can read it.
559
- *
560
- * It intentionally exposes no per-version capability flags. The adapters under
561
- * `./compat/` detect the shape they need STRUCTURALLY — whether a return value
562
- * is a thenable, which export name a module carries — because a structural
563
- * probe also survives upstream reshaping something within one release, while a
564
- * version comparison does not. The release string here is for the gate and for
565
- * diagnostics.
566
- */
567
- var MultiRootCompatService = class extends Service {
568
- /** The verdict this composition started under. */
569
- report;
570
- constructor(ctx, report) {
571
- super(ctx, "multiRootCompat");
572
- this.report = report;
573
- }
574
- /** The single DSH release every inspected core package agreed on. */
575
- get release() {
576
- return this.report.release;
577
- }
578
- /** Whether the installation is on the allowlist. */
579
- get supported() {
580
- return this.report.verdict === "supported";
581
- }
582
- };
583
- /** This row needs nothing: it is the thing everything else waits for. */
584
- const inject = [];
585
- /**
586
- * Apply the gate policy to one report.
587
- *
588
- * Under `DSH_MULTI_ROOT_COMPAT=warn` exactly one verdict is relaxed: an
589
- * `unsupported` release that every inspected package agrees on. That is the
590
- * only shape the upgrade smoke probes — a coherent tree on a release the
591
- * allowlist does not name yet, which is how a release earns its place there.
592
- * `mixed` and `incomplete` describe an installation the contract cannot judge
593
- * at all, so they are refused in either mode; production deployments leave the
594
- * variable unset and get the refusal for everything.
595
- * @param report - the verdict to act on.
596
- * @param enforcement - how to react to a non-`supported` verdict.
597
- * @returns the warning to log, or `undefined` when there is nothing to say.
598
- * @throws {DshCompatUnsupportedError} when refusing to run.
599
- */
600
- function assertSupportedInstallation(report, enforcement) {
601
- if (report.verdict === "supported") return void 0;
602
- if (enforcement === "enforce" || report.verdict !== "unsupported") throw new DshCompatUnsupportedError(report);
603
- 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(", ")}.`;
604
- }
605
- /**
606
- * Judge the installation, then either refuse to provide the service or provide
607
- * it.
608
- * @param ctx - the row's context.
609
- */
610
- function apply(ctx) {
611
- const report = classifyInstallation();
612
- const warning = assertSupportedInstallation(report, compatEnforcement());
613
- if (warning !== void 0) ctx.logger.warn(warning);
614
- assertAgentPtyMarker();
615
- ctx.plugin(MultiRootCompatService, report);
616
- }
617
- //#endregion
618
- 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 };
356
+ export { REQUIRED_CORE_PACKAGES as a, SUPPORTED_DSH_RELEASES as c, readInstalledVersion as d, OPTIONAL_CORE_PACKAGES as i, classifyInstallation as l, CORE_PACKAGES as n, RUNNER_ANCHORED_PACKAGES as o, HOST_SERVICE_PACKAGES as r, RUNTIME_OPTIONAL_PACKAGES as s, COMPAT_ENFORCEMENT_ENV as t, compatEnforcement as u };