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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.en.md +56 -1
  2. package/CHANGELOG.md +56 -1
  3. package/README.en.md +157 -92
  4. package/README.md +158 -93
  5. package/cordis.patch.yml +61 -0
  6. package/lib/client.js +2819 -115
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-82Y27Iom.js → compat-DF_yL1bB.js} +180 -4
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-Cn1Jj5Xi.js → instructions-D19jRxN0.js} +109 -28
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +43 -0
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +240 -25
  28. package/lib/session-directory-BLOstut1.js +9 -0
  29. package/lib/tool-context-CpgIFcCB.js +6 -0
  30. package/lib/types/client/RootSwitcher.d.ts +63 -0
  31. package/lib/types/client/WorkspaceFoldersAction.d.ts +41 -0
  32. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  33. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  34. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  35. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  36. package/lib/types/client/files-tree/face.d.ts +107 -0
  37. package/lib/types/client/files-tree/locales.d.ts +10 -0
  38. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  39. package/lib/types/client/files-tree/store.d.ts +81 -0
  40. package/lib/types/client/index.d.ts +13 -2
  41. package/lib/types/client/locales.d.ts +60 -0
  42. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  43. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  44. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  45. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  46. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  47. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  48. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  49. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  50. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  51. package/lib/types/client/panel-client.d.ts +2 -1
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +16 -3
  54. package/lib/types/compat/client-icons.d.ts +18 -0
  55. package/lib/types/compat/client-session.d.ts +2 -0
  56. package/lib/types/compat/dsh-version.d.ts +5 -3
  57. package/lib/types/compat/instruction-overlap.d.ts +7 -0
  58. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  59. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  60. package/lib/types/compat/process-result.d.ts +14 -0
  61. package/lib/types/compat/pty-marker.d.ts +83 -0
  62. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  63. package/lib/types/compat/session-directory.d.ts +4 -0
  64. package/lib/types/compat/storage-zod.d.ts +65 -0
  65. package/lib/types/compat/tool-context.d.ts +9 -0
  66. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  67. package/lib/types/compat/windows-native.d.ts +68 -0
  68. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  69. package/lib/types/compat/workspace-files.d.ts +7 -0
  70. package/lib/types/contract.d.ts +35 -4
  71. package/lib/types/dialects.d.ts +99 -3
  72. package/lib/types/fs.d.ts +32 -10
  73. package/lib/types/index.d.ts +5 -0
  74. package/lib/types/lsp.d.ts +4 -0
  75. package/lib/types/method-wrapper.d.ts +2 -0
  76. package/lib/types/process-fence.d.ts +54 -0
  77. package/lib/types/profile-isolation.d.ts +178 -0
  78. package/lib/types/providers.d.ts +70 -0
  79. package/lib/types/registry.d.ts +132 -31
  80. package/lib/types/roots.d.ts +100 -13
  81. package/lib/types/sandbox.d.ts +59 -12
  82. package/lib/types/scope.d.ts +203 -14
  83. package/lib/types/windows-broker.d.ts +17 -0
  84. package/lib/types/windows-deny.d.ts +1 -0
  85. package/lib/types/windows-profile.d.ts +10 -0
  86. package/lib/types/windows-runner-contract.d.ts +97 -0
  87. package/lib/types/windows-runner.d.ts +1 -0
  88. package/lib/types/workspace-files.d.ts +4 -0
  89. package/lib/types/workspace-routing.d.ts +11 -0
  90. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  91. package/lib/windows-runner.js +1311 -0
  92. package/lib/workspace-files.js +203 -0
  93. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  94. package/package.json +116 -51
  95. package/scripts/windows-broker/install.ps1 +89 -0
  96. package/scripts/windows-broker/rights.cs +58 -0
  97. package/scripts/windows-broker/service.cs +323 -0
  98. package/scripts/windows-broker/verify-install.ps1 +80 -0
  99. package/lib/command-DyOWujPo.js +0 -690
  100. package/lib/sandbox-D8Fw95FM.js +0 -290
  101. 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 revealArgv, t as COMMAND_NAME } from "./command-DyOWujPo.js";
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 };
@@ -1,5 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Service } from "@deepseek-ai/cordis";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
3
5
  //#region src/compat/dsh-version.ts
4
6
  /**
5
7
  * The DSH compatibility contract: which upstream releases this plugin is
@@ -40,7 +42,14 @@ const SUPPORTED_DSH_RELEASES = [
40
42
  "0.1.7-rc.1",
41
43
  "0.1.7-rc.2",
42
44
  "0.2.0-rc.1",
43
- "0.2.0-rc.2"
45
+ "0.2.0-rc.2",
46
+ "0.2.1-alpha.1",
47
+ "0.2.1-alpha.2",
48
+ "0.2.1-alpha.3",
49
+ "0.2.1-alpha.4",
50
+ "0.2.1-alpha.5",
51
+ "0.2.1-rc.1",
52
+ "0.2.1-rc.2"
44
53
  ];
45
54
  /**
46
55
  * Packages whose absence means this plugin cannot function at all: every one of
@@ -61,6 +70,13 @@ const REQUIRED_CORE_PACKAGES = [
61
70
  * that is the mixed-installation failure this contract exists to catch.
62
71
  */
63
72
  const OPTIONAL_CORE_PACKAGES = [
73
+ "@deepseek-ai/dsh-tools",
74
+ "@deepseek-ai/dsh-working-directory",
75
+ "@deepseek-ai/dsh-lsp",
76
+ "@deepseek-ai/dsh-lsp-stdio",
77
+ "@deepseek-ai/dsh-api-workspace-files",
78
+ "@deepseek-ai/dsh-typert-protocol",
79
+ "@deepseek-ai/dsh-sandbox-windows-acl",
64
80
  "@deepseek-ai/dsh-agent",
65
81
  "@deepseek-ai/dsh-agent-instructions",
66
82
  "@deepseek-ai/dsh-client-connection",
@@ -72,6 +88,8 @@ const OPTIONAL_CORE_PACKAGES = [
72
88
  ];
73
89
  /** Every package the contract inspects, required first. */
74
90
  const CORE_PACKAGES = [...REQUIRED_CORE_PACKAGES, ...OPTIONAL_CORE_PACKAGES];
91
+ /** Services looked up structurally; older runtimes never install these modules. */
92
+ const HOST_RESOLVED_PACKAGES = ["@deepseek-ai/dsh-working-directory"];
75
93
  /** The environment variable that relaxes the gate. */
76
94
  const COMPAT_ENFORCEMENT_ENV = "DSH_MULTI_ROOT_COMPAT";
77
95
  /**
@@ -88,11 +106,18 @@ const COMPAT_ENFORCEMENT_ENV = "DSH_MULTI_ROOT_COMPAT";
88
106
  * manifest cannot be resolved.
89
107
  */
90
108
  function readInstalledVersion(packageName) {
109
+ const require = createRequire(import.meta.url);
91
110
  try {
92
- const manifest = createRequire(import.meta.url)(`${packageName}/package.json`);
111
+ const manifest = require(`${packageName}/package.json`);
93
112
  return typeof manifest.version === "string" ? manifest.version : void 0;
94
113
  } catch {
95
- return;
114
+ if (!HOST_RESOLVED_PACKAGES.includes(packageName)) return void 0;
115
+ try {
116
+ const manifest = createRequire(require.resolve("@deepseek-ai/dsh/package.json"))(`${packageName}/package.json`);
117
+ return typeof manifest.version === "string" ? manifest.version : void 0;
118
+ } catch {
119
+ return;
120
+ }
96
121
  }
97
122
  }
98
123
  /**
@@ -174,6 +199,156 @@ function report(verdict, release, packages, lines) {
174
199
  };
175
200
  }
176
201
  //#endregion
202
+ //#region src/compat/pty-marker.ts
203
+ /**
204
+ * Boot-time probe for the agent-PTY marker contract.
205
+ *
206
+ * `multi-root-process-fence` tells an agent terminal from the user's own by
207
+ * one env fact: every supported `dsh-terminal-bash` backend stamps
208
+ * `DSH_PTY_SESSION_ID` (alongside `DSH_SESSION_ID`) into the PTY spawn spec,
209
+ * while the panel's terminal controller stamps only `DSH_SESSION_ID`. That
210
+ * marker is the ONLY discriminator — a backend that ever stops stamping it
211
+ * would reclassify agent PTYs as user terminals and pass them through
212
+ * unconfined under a restricted `danger-full-access`, which is a silent
213
+ * fail-open on a security boundary.
214
+ *
215
+ * The marker is an env key inside upstream's emitted code, not an exported
216
+ * symbol, so the honest structural probe is content: the installed backend
217
+ * package must still write the marker. This module is called from the
218
+ * `multi-root-compat` gate (`src/compat.ts`) BEFORE the service registers —
219
+ * like the version verdict it is a precondition, not a diagnostic, and a
220
+ * breach refuses the composition the same way `mixed`/`incomplete` do: it is
221
+ * not relaxable by `DSH_MULTI_ROOT_COMPAT=warn`.
222
+ *
223
+ * Resolution follows the optional-peer precedent (`agent-instructions.ts`):
224
+ * the backend is not a peer of this plugin, so the plugin anchor is tried
225
+ * first (flat layouts) and the `@deepseek-ai/dsh` host anchor second — the
226
+ * same `HOST_RESOLVED_PACKAGES` pattern as `dsh-version.ts` — because the
227
+ * backend rides inside the harness meta package's own dependency tree.
228
+ *
229
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/pty-marker
230
+ */
231
+ /** The env key that identifies an agent-launched PTY spawn spec. */
232
+ const AGENT_PTY_MARKER = "DSH_PTY_SESSION_ID";
233
+ /** The upstream package whose backend owns the marker stamp. */
234
+ const TERMINAL_BACKEND = "@deepseek-ai/dsh-terminal-bash";
235
+ /** The harness meta package the backend rides inside (its dependency). */
236
+ const HOST_ANCHOR = "@deepseek-ai/dsh/package.json";
237
+ /** Scan budget: enough for any plausible backend layout, bounded forever. */
238
+ const MAX_SOURCE_FILES = 256;
239
+ const MAX_TOTAL_BYTES = 8388608;
240
+ /**
241
+ * Raised when the installed terminal backend does not uphold the marker
242
+ * contract. Thrown from `apply` like `DshCompatUnsupportedError`, so it
243
+ * surfaces as a plugin load failure with the diagnosis attached.
244
+ */
245
+ var PtyMarkerContractError = class extends Error {
246
+ /** The resolved backend path when one was found. */
247
+ backend;
248
+ constructor(message, backend) {
249
+ super(message);
250
+ this.name = "PtyMarkerContractError";
251
+ this.backend = backend;
252
+ }
253
+ };
254
+ /**
255
+ * Resolve the backend package root through the two anchors the contract can
256
+ * reach: this module first (flat layouts where the backend is a direct
257
+ * install), then the `@deepseek-ai/dsh` host manifest (pnpm-style layouts
258
+ * where it is the meta package's own dependency — the
259
+ * `HOST_RESOLVED_PACKAGES` precedent). A resolution failure that is not a
260
+ * genuine NOT_FOUND propagates: a resolvable-but-broken specifier is not
261
+ * evidence of absence.
262
+ */
263
+ function resolveBackendRoot() {
264
+ const self = createRequire(import.meta.url);
265
+ const anchors = [self];
266
+ try {
267
+ anchors.push(createRequire(self.resolve(HOST_ANCHOR)));
268
+ } catch (error) {
269
+ if (!isNotFound(error)) throw error;
270
+ }
271
+ for (const anchor of anchors) try {
272
+ return dirname(anchor.resolve(`${TERMINAL_BACKEND}/package.json`));
273
+ } catch (error) {
274
+ if (!isNotFound(error)) throw error;
275
+ }
276
+ }
277
+ function isNotFound(error) {
278
+ const code = error?.code;
279
+ return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
280
+ }
281
+ /** Recursively collect `.js` sources under the package root, within budget. */
282
+ function listSources(root) {
283
+ const files = [];
284
+ const visit = (dir) => {
285
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
286
+ if (files.length >= MAX_SOURCE_FILES) return;
287
+ const path = join(dir, entry.name);
288
+ if (entry.isDirectory() && entry.name !== "node_modules") visit(path);
289
+ else if (entry.isFile() && entry.name.endsWith(".js")) files.push(path);
290
+ }
291
+ };
292
+ visit(root);
293
+ return files;
294
+ }
295
+ const defaultIo = {
296
+ resolveBackendRoot,
297
+ listSources,
298
+ read: (path) => readFileSync(path, "utf8")
299
+ };
300
+ /**
301
+ * Judge the installed terminal backend against the marker contract.
302
+ *
303
+ * Never throws: every outcome — absent package, unreadable source, missing
304
+ * marker — is folded into the verdict so the caller's reaction is a policy
305
+ * decision in one place.
306
+ * @param io - filesystem seams; tests inject them, production uses real fs.
307
+ * @returns the verdict.
308
+ */
309
+ function agentPtyMarkerVerdict(io = defaultIo) {
310
+ let root;
311
+ try {
312
+ root = io.resolveBackendRoot();
313
+ } catch {
314
+ return "unverifiable";
315
+ }
316
+ if (root === void 0) return "uninstalled";
317
+ let files;
318
+ try {
319
+ files = io.listSources(root);
320
+ } catch {
321
+ return "unverifiable";
322
+ }
323
+ let remaining = MAX_TOTAL_BYTES;
324
+ for (const file of files) {
325
+ let content;
326
+ try {
327
+ content = io.read(file);
328
+ } catch {
329
+ return "unverifiable";
330
+ }
331
+ if (content.length > remaining) return "unverifiable";
332
+ if (content.includes("DSH_PTY_SESSION_ID")) return "stamped";
333
+ remaining -= content.length;
334
+ }
335
+ return "unverifiable";
336
+ }
337
+ /**
338
+ * Enforce the contract: refuse when the marker cannot be confirmed.
339
+ *
340
+ * @param io - filesystem seams; tests inject them.
341
+ * @throws {PtyMarkerContractError} on `unverifiable`.
342
+ */
343
+ function assertAgentPtyMarker(io = defaultIo) {
344
+ if (agentPtyMarkerVerdict(io) !== "unverifiable") return;
345
+ let backend;
346
+ try {
347
+ backend = io.resolveBackendRoot();
348
+ } catch {}
349
+ 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);
350
+ }
351
+ //#endregion
177
352
  //#region src/compat.ts
178
353
  /**
179
354
  * `multi-root-compat`: the startup gate that turns the DSH compatibility
@@ -271,7 +446,8 @@ function apply(ctx) {
271
446
  const report = classifyInstallation();
272
447
  const warning = assertSupportedInstallation(report, compatEnforcement());
273
448
  if (warning !== void 0) ctx.logger.warn(warning);
449
+ assertAgentPtyMarker();
274
450
  ctx.plugin(MultiRootCompatService, report);
275
451
  }
276
452
  //#endregion
277
- export { inject as a, REQUIRED_CORE_PACKAGES as c, compatEnforcement as d, readInstalledVersion as f, assertSupportedInstallation as i, SUPPORTED_DSH_RELEASES as l, MultiRootCompatService as n, CORE_PACKAGES as o, apply as r, OPTIONAL_CORE_PACKAGES as s, DshCompatUnsupportedError as t, classifyInstallation as u };
453
+ export { inject as a, agentPtyMarkerVerdict as c, OPTIONAL_CORE_PACKAGES as d, REQUIRED_CORE_PACKAGES as f, readInstalledVersion as g, compatEnforcement as h, assertSupportedInstallation as i, assertAgentPtyMarker as l, classifyInstallation as m, MultiRootCompatService as n, AGENT_PTY_MARKER as o, SUPPORTED_DSH_RELEASES as p, apply as r, PtyMarkerContractError as s, DshCompatUnsupportedError as t, CORE_PACKAGES as u };
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-82Y27Iom.js";
1
+ import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-DF_yL1bB.js";
2
2
  export { DshCompatUnsupportedError, MultiRootCompatService, apply, assertSupportedInstallation, inject };
@@ -0,0 +1,238 @@
1
+ import { z } from "zod";
2
+ //#region src/contract.ts
3
+ /**
4
+ * The wire contract between the browser panel and this plugin's host half.
5
+ *
6
+ * It is deliberately transport-agnostic plain data: the host registers one
7
+ * Connection RPC channel (see `command.ts`) and the client bundle inlines this
8
+ * module, so both sides agree on endpoint names and payload shapes without
9
+ * sharing any runtime identity. Error codes are the stable half of the
10
+ * contract; `message` is only a fallback the panel does not have to show.
11
+ *
12
+ * Two properties this module owns:
13
+ *
14
+ * - **One shape per endpoint.** {@link PanelResponseMap} is the single mapping
15
+ * from endpoint to response type, so the panel cannot silently decode one
16
+ * endpoint's answer as another's. `reveal` answers with
17
+ * {@link RevealedView}; root-management endpoints answer with
18
+ * {@link RootsView}.
19
+ * The host builds those answers in `command.ts` and the
20
+ * panel parses them here — one definition, two consumers.
21
+ * - **Runtime validation at the boundary.** The payload arrives from a browser
22
+ * and the answer arrives from the host; neither is trusted to match the
23
+ * TypeScript types. {@link parsePanelCall} and the `parse*View` functions
24
+ * check the shapes and report structural failures as a message, so a
25
+ * mismatch becomes a visible, localized error instead of a crash or a
26
+ * silently wrong render.
27
+ *
28
+ * Nothing here may import `node:*` or `@deepseek-ai/*`: this module is inlined
29
+ * into the browser bundle.
30
+ *
31
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/contract
32
+ */
33
+ /** The absolute logical channel both halves speak over. */
34
+ const PANEL_CHANNEL = "/multi-root-workspace";
35
+ /** Every endpoint name, for exhaustive iteration in tests and callers. */
36
+ const PANEL_ENDPOINTS = [
37
+ "list",
38
+ "add",
39
+ "remove",
40
+ "alias",
41
+ "move",
42
+ "setAccess",
43
+ "reveal"
44
+ ];
45
+ /** The shared nine-cell rule; tool dispatch may subsequently narrow writes. */
46
+ function effectiveRootAccess(access, mode) {
47
+ if (access !== "inherit") return access;
48
+ return mode === "danger-full-access" ? "full-access" : mode === "workspace-write" ? "read-write" : "read-only";
49
+ }
50
+ /**
51
+ * The request body on the wire. The endpoint travels as the RPC method, not as
52
+ * a field, so this schema covers the fields only; the host pairs it with the
53
+ * method the channel dispatched (and rejects the pair if the body contradicts
54
+ * it). Unknown keys are rejected rather than ignored: a client sending a field
55
+ * this host does not understand is out of contract, and silently dropping it
56
+ * would hide the drift.
57
+ */
58
+ const panelRequestSchema = z.object({
59
+ sessionId: z.string().min(1),
60
+ entry: z.object({
61
+ ordinal: z.number().int().positive(),
62
+ id: z.string(),
63
+ path: z.string(),
64
+ addedAt: z.string()
65
+ }).strict().optional(),
66
+ id: z.string().optional(),
67
+ path: z.string().optional(),
68
+ alias: z.string().optional(),
69
+ access: z.enum([
70
+ "inherit",
71
+ "read-write",
72
+ "read-only"
73
+ ]).optional(),
74
+ beforeEntry: z.object({
75
+ ordinal: z.number().int().positive(),
76
+ id: z.string(),
77
+ path: z.string(),
78
+ addedAt: z.string()
79
+ }).strict().optional(),
80
+ beforeId: z.string().optional()
81
+ }).strict();
82
+ /**
83
+ * One root as it travels over the channel. Deliberately NOT strict, in both
84
+ * directions: a newer host that adds a field must not brick an older panel, and
85
+ * the field that matters is the state it reports.
86
+ */
87
+ const rootViewSchema = z.object({
88
+ ordinal: z.number().int().positive(),
89
+ id: z.string(),
90
+ path: z.string(),
91
+ alias: z.string().optional(),
92
+ addedAt: z.string(),
93
+ state: z.enum([
94
+ "available",
95
+ "missing",
96
+ "redirected",
97
+ "invalid"
98
+ ]),
99
+ access: z.enum([
100
+ "inherit",
101
+ "read-write",
102
+ "read-only"
103
+ ]).optional(),
104
+ detail: z.string().optional(),
105
+ effectiveAccess: z.enum([
106
+ "read-only",
107
+ "read-write",
108
+ "full-access",
109
+ "unavailable"
110
+ ]).optional()
111
+ });
112
+ const rootsViewSchema = z.object({
113
+ primaryRoot: z.string(),
114
+ primaryName: z.string().optional(),
115
+ effectiveCwd: z.string().optional(),
116
+ mode: z.enum([
117
+ "read-only",
118
+ "workspace-write",
119
+ "danger-full-access"
120
+ ]).optional(),
121
+ cappedTools: z.array(z.string()).optional(),
122
+ toolAccessInvalid: z.boolean().optional(),
123
+ roots: z.array(rootViewSchema),
124
+ limit: z.number().int().nonnegative().optional(),
125
+ count: z.number().int().nonnegative().optional(),
126
+ unavailable: z.string().optional()
127
+ });
128
+ const revealedViewSchema = z.object({ revealed: z.string() });
129
+ z.object({
130
+ code: z.string(),
131
+ message: z.string(),
132
+ details: z.unknown().optional()
133
+ });
134
+ /**
135
+ * Validate the payload the host received for one endpoint.
136
+ * @param endpoint - the endpoint the channel dispatched (the RPC method).
137
+ * @param payload - the raw channel payload (the request body).
138
+ * @returns the call the host may act on, or the reason it is not one.
139
+ */
140
+ function parsePanelCall(endpoint, payload) {
141
+ if (!PANEL_ENDPOINTS.includes(endpoint)) return {
142
+ ok: false,
143
+ message: `unknown endpoint "${endpoint}"`
144
+ };
145
+ const parsed = panelRequestSchema.safeParse(payload ?? {});
146
+ if (!parsed.success) return {
147
+ ok: false,
148
+ message: `the request body is invalid: ${firstIssue(parsed.error)}`
149
+ };
150
+ return {
151
+ ok: true,
152
+ value: narrowCall(endpoint, parsed.data)
153
+ };
154
+ }
155
+ /**
156
+ * Validate a `RootsView` when it arrives.
157
+ * @param value - the raw endpoint value.
158
+ * @returns the view, or the reason it is not one.
159
+ */
160
+ function parseRootsView(value) {
161
+ const parsed = rootsViewSchema.safeParse(value);
162
+ if (!parsed.success) return {
163
+ ok: false,
164
+ message: `the answer is not a roots view: ${firstIssue(parsed.error)}`
165
+ };
166
+ const view = parsed.data;
167
+ return {
168
+ ok: true,
169
+ value: {
170
+ primaryRoot: view.primaryRoot,
171
+ ...view.primaryName === void 0 ? {} : { primaryName: view.primaryName },
172
+ ...view.effectiveCwd === void 0 ? {} : { effectiveCwd: view.effectiveCwd },
173
+ ...view.mode === void 0 ? {} : { mode: view.mode },
174
+ ...view.cappedTools === void 0 ? {} : { cappedTools: view.cappedTools },
175
+ ...view.toolAccessInvalid === void 0 ? {} : { toolAccessInvalid: view.toolAccessInvalid },
176
+ roots: view.roots.map(narrowRootView),
177
+ limit: view.limit ?? Number.MAX_SAFE_INTEGER,
178
+ count: view.count ?? view.roots.length,
179
+ ...view.unavailable === void 0 ? {} : { unavailable: view.unavailable }
180
+ }
181
+ };
182
+ }
183
+ /**
184
+ * Validate a `RevealedView` when it arrives.
185
+ * @param value - the raw endpoint value.
186
+ * @returns the view, or the reason it is not one.
187
+ */
188
+ function parseRevealedView(value) {
189
+ const parsed = revealedViewSchema.safeParse(value);
190
+ if (!parsed.success) return {
191
+ ok: false,
192
+ message: `the answer is not a reveal result: ${firstIssue(parsed.error)}`
193
+ };
194
+ return {
195
+ ok: true,
196
+ value: parsed.data
197
+ };
198
+ }
199
+ /**
200
+ * Rebuild a parsed call as the declared type, keeping absent optional fields
201
+ * ABSENT (`exactOptionalPropertyTypes` distinguishes "absent" from "undefined",
202
+ * and the host's own code reads the two the same way the panel sent them).
203
+ */
204
+ function narrowCall(endpoint, value) {
205
+ return {
206
+ endpoint,
207
+ sessionId: value.sessionId,
208
+ ...value.entry === void 0 ? {} : { entry: value.entry },
209
+ ...value.id === void 0 ? {} : { id: value.id },
210
+ ...value.path === void 0 ? {} : { path: value.path },
211
+ ...value.alias === void 0 ? {} : { alias: value.alias },
212
+ ...value.access === void 0 ? {} : { access: value.access },
213
+ ...value.beforeEntry === void 0 ? {} : { beforeEntry: value.beforeEntry },
214
+ ...value.beforeId === void 0 ? {} : { beforeId: value.beforeId }
215
+ };
216
+ }
217
+ /** Rebuild one parsed root as the declared `RootView`. */
218
+ function narrowRootView(value) {
219
+ return {
220
+ ordinal: value.ordinal,
221
+ id: value.id,
222
+ path: value.path,
223
+ addedAt: value.addedAt,
224
+ state: value.state,
225
+ access: value.access ?? "inherit",
226
+ ...value.effectiveAccess === void 0 ? {} : { effectiveAccess: value.effectiveAccess },
227
+ ...value.alias === void 0 ? {} : { alias: value.alias },
228
+ ...value.detail === void 0 ? {} : { detail: value.detail }
229
+ };
230
+ }
231
+ /** Render the first zod issue as one short sentence. */
232
+ function firstIssue(error) {
233
+ const issue = error.issues[0];
234
+ if (issue === void 0) return "invalid";
235
+ return `${issue.path.length === 0 ? "" : `${issue.path.join(".")}: `}${issue.message}`;
236
+ }
237
+ //#endregion
238
+ export { parseRevealedView as a, parsePanelCall as i, PANEL_ENDPOINTS as n, parseRootsView as o, effectiveRootAccess as r, PANEL_CHANNEL as t };
@@ -1,3 +1,4 @@
1
+ import { brandMultiRootProvider } from "./providers.js";
1
2
  import { dirname, sep } from "node:path";
2
3
  import { writableRoots } from "@deepseek-ai/dsh-sandbox";
3
4
  import { FsError } from "@deepseek-ai/dsh-fs";
@@ -72,6 +73,7 @@ var MultiRootFileSystem = class extends LocalFileSystem {
72
73
  defaultMode;
73
74
  constructor(ctx, config) {
74
75
  super(ctx, config);
76
+ brandMultiRootProvider(this, "fs");
75
77
  this.defaultMode = ctx.sandboxPolicy.defaultMode;
76
78
  }
77
79
  /** The deployment default mode — the capability fact the tool layer reads to advertise escalation. */
@@ -107,47 +109,81 @@ var MultiRootFileSystem = class extends LocalFileSystem {
107
109
  return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
108
110
  }
109
111
  /**
110
- * The writable roots of one workspace-write call: the upstream derivation
111
- * (primary root plus the platform temp areas, canonical and deduplicated)
112
- * widened by the scope's additional roots, in scope order.
112
+ * The writable roots of one confined call: the upstream derivation (primary
113
+ * root plus the platform temp areas, canonical and deduplicated — empty
114
+ * under `read-only`) widened by the scope's EFFECTIVE writable members.
115
+ * Under `read-only` that is exactly the fixed `read-write` roots (the tool
116
+ * cap may already have emptied the scope's list), so an explicit grant
117
+ * survives a globally read-only session. An unavailable registry authority
118
+ * never contributes members: a process that cannot read the store grants
119
+ * only the primary root (and no shared temp grant).
113
120
  * @param policy - the per-call policy.
114
121
  * @param scope - the scope resolved once for this call; both the fence and
115
122
  * the denial message below share it instead of re-resolving.
116
- * @returns the canonical writable roots; empty under `read-only`.
123
+ * @returns the canonical writable roots; possibly empty.
117
124
  */
118
125
  rootsFor(policy, scope) {
126
+ if (scope.authority === "unavailable") return policy.mode === "read-only" ? [] : [scope.primaryRoot];
119
127
  const roots = writableRoots(policy);
120
- if (roots.length === 0) return roots;
121
- for (const root of scope.additionalRoots) if (!roots.includes(root)) roots.push(root);
128
+ for (const root of scope.writableRoots) if (!roots.includes(root)) roots.push(root);
122
129
  return roots;
123
130
  }
124
131
  /**
125
- * Enforce the per-call policy against `target` and return the EXACT target the
126
- * mutation must use, so the checked identity is the mutated one (no
127
- * check-here-write-there TOCTOU). `read-only` denies; `workspace-write`
128
- * re-canonicalizes NOW (reflecting a concurrently swapped symlink ancestor),
129
- * requires containment under one of the scope's writable roots, and returns
130
- * THAT fresh target; `danger-full-access` returns the caller's target unfenced.
132
+ * Enforce the per-call policy against `target` and return the EXACT target
133
+ * the mutation must use, so the checked identity is the mutated one (no
134
+ * check-here-write-there TOCTOU). Deny precedes containment: a root in
135
+ * `scope.deniedRoots` — an explicit `read-only` root, or any member when the
136
+ * call is tool-capped — refuses writes under EVERY mode, including
137
+ * `danger-full-access` and where a broader grant (the platform temp area)
138
+ * would otherwise cover the path. `read-only` then grants only the fixed
139
+ * `read-write` members; `workspace-write` requires containment under the
140
+ * widened writable set.
141
+ *
142
+ * `danger-full-access` is the only mode with a fast path, and only while
143
+ * the deny set is empty: it then returns the caller's target untouched,
144
+ * upstream-exact. Once ANY deny boundary exists the caller's `FsTarget` may
145
+ * be stale — the directory it resolved through can since have been replaced
146
+ * by a symlink into a denied root — so the fence re-resolves
147
+ * `target.displayPath`, checks the FRESH target key, and mutates that fresh
148
+ * target rather than the stale one the caller resolved earlier.
149
+ *
150
+ * One more Full-access rule: while the registry authority is `unavailable`
151
+ * the empty deny set is not trustworthy — the store this process cannot
152
+ * read may hold explicit `read-only` registrations — so the mode degrades
153
+ * to primary-root-only writes (no shared temp or additional-root grants)
154
+ * until the authority returns.
131
155
  * @param target - the resolved target about to be mutated.
132
156
  * @param sandboxPolicy - the per-call policy; omit for the deployment fallback.
133
157
  * @returns the fresh canonical target the mutation must use.
134
158
  */
135
159
  async checkedTarget(target, sandboxPolicy) {
136
160
  const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
137
- const { mode } = policy;
138
- if (mode === "danger-full-access") return target;
139
- if (mode === "read-only") throw new FsError(`cannot write "${target.displayPath}": file access denied under read-only mode`, "FS_SANDBOX_DENIED");
140
- const fresh = await this.resolve(target.displayPath);
141
161
  const scope = this.ctx.multiRootScope.resolve(policy);
142
- const roots = this.rootsFor(policy, scope);
162
+ const effectiveMode = scope.authority === "unavailable" && policy.mode === "danger-full-access" ? "workspace-write" : policy.mode;
163
+ if (effectiveMode === "danger-full-access" && scope.deniedRoots.length === 0) return target;
164
+ const fresh = await this.resolve(target.displayPath);
165
+ if (scope.deniedRoots.length > 0) {
166
+ for (const path of scope.deniedRoots) if (await isPathUnder(fresh.targetKey, path)) {
167
+ const root = scope.roots.find((root) => root.path === path);
168
+ const reason = path === scope.protectedHome ? `${path} is the DSH home directory, which is protected under danger-full-access while workspace restrictions apply` : root?.access === "read-only" || root === void 0 ? `${path} is a read-only additional root` : `${path} is an additional root this call may not write`;
169
+ throw new FsError(`cannot write "${target.displayPath}": ${reason}`, "FS_SANDBOX_DENIED");
170
+ }
171
+ }
172
+ if (effectiveMode === "danger-full-access") return fresh;
173
+ const effectivePolicy = {
174
+ ...policy,
175
+ mode: effectiveMode
176
+ };
177
+ const roots = this.rootsFor(effectivePolicy, scope);
143
178
  let contained = false;
144
179
  for (const root of roots) if (await isPathUnder(fresh.targetKey, root)) {
145
180
  contained = true;
146
181
  break;
147
182
  }
148
183
  if (!contained) {
149
- const denial = `cannot write "${target.displayPath}": file access denied under workspace-write mode`;
150
- throw new FsError(scope.additionalRoots.length === 0 ? denial : `${denial}; allowed roots: ${roots.join(", ")}`, "FS_SANDBOX_DENIED");
184
+ let denial = `cannot write "${target.displayPath}": file access denied under ${effectiveMode} mode`;
185
+ if (scope.additionalRoots.length > 0) denial += `; allowed roots: ${roots.join(", ")}`;
186
+ throw new FsError(denial, "FS_SANDBOX_DENIED");
151
187
  }
152
188
  return fresh;
153
189
  }
package/lib/fs.js CHANGED
@@ -1,2 +1,3 @@
1
- import { t as MultiRootFileSystem } from "./fs-D_Sw3o5z.js";
1
+ import "./providers.js";
2
+ import { t as MultiRootFileSystem } from "./fs-9JIzGaf3.js";
2
3
  export { MultiRootFileSystem, MultiRootFileSystem as default };
package/lib/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { c as REQUIRED_CORE_PACKAGES, d as compatEnforcement, f as readInstalledVersion, i as assertSupportedInstallation, l as SUPPORTED_DSH_RELEASES, n as MultiRootCompatService, o as CORE_PACKAGES, s as OPTIONAL_CORE_PACKAGES, t as DshCompatUnsupportedError, u as classifyInstallation } from "./compat-82Y27Iom.js";
2
- import { a as composeInstructionMessage, c as createInstructionMessage, l as instructionsApi, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES, u as renderInstructions } from "./instructions-Cn1Jj5Xi.js";
3
- import { n as widenConfined, t as MultiRootSandboxProvider } from "./sandbox-D8Fw95FM.js";
4
- import { a as classifyStoredRoots, c as indexedStatuses, f as resolveRootRef, i as canonicalRoot, l as isCanonicallyUnder, n as additionalRootId, p as validateRootCandidate, r as availableRoots, s as expandRootInput, t as RootValidationError, u as removeStatusAt } from "./roots-DasWEBJk.js";
5
- import { t as MultiRootFileSystem } from "./fs-D_Sw3o5z.js";
1
+ import { c as agentPtyMarkerVerdict, d as OPTIONAL_CORE_PACKAGES, f as REQUIRED_CORE_PACKAGES, g as readInstalledVersion, h as compatEnforcement, i as assertSupportedInstallation, l as assertAgentPtyMarker, m as classifyInstallation, n as MultiRootCompatService, o as AGENT_PTY_MARKER, p as SUPPORTED_DSH_RELEASES, s as PtyMarkerContractError, t as DshCompatUnsupportedError, u as CORE_PACKAGES } from "./compat-DF_yL1bB.js";
2
+ import { a as composeInstructionMessage, c as createInstructionMessage, l as instructionsApi, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES, u as renderInstructions } from "./instructions-D19jRxN0.js";
3
+ import { n as widenConfined, t as MultiRootSandboxProvider } from "./sandbox-Kup4_ZIb.js";
4
+ import { a as classifyStoredRoots, c as indexedStatuses, i as canonicalRoot, l as isCanonicallyUnder, m as validateRootCandidate, n as additionalRootId, p as resolveRootRef, r as availableRoots, s as expandRootInput, t as RootValidationError, u as removeStatusAt } from "./roots-B-0l8mNG.js";
5
+ import { MULTI_ROOT_PROVIDER_BRAND, MultiRootProviders, brandMultiRootProvider, isMultiRootProvider } from "./providers.js";
6
+ import { t as MultiRootFileSystem } from "./fs-9JIzGaf3.js";
7
+ import { a as parseRevealedView, i as parsePanelCall, n as PANEL_ENDPOINTS, o as parseRootsView, t as PANEL_CHANNEL } from "./contract-DZ4zczDT.js";
6
8
  import { MultiRootScopeService, renderWorkspaceRootsContext, sanitizeAdditionalRoots } from "./scope.js";
7
- import { c as RegistryLeaseContendedError, i as REGISTRY_CONTENDED_MESSAGE, n as MAX_ALIAS_LENGTH, o as multiRootDomainSpec, r as MultiRootRegistry, s as RegistryAuthorityLease, t as DOMAIN_NAME } from "./registry-DySMbjgW.js";
8
- import { a as renderRootsReport, c as PANEL_CHANNEL, d as parseRevealedView, f as parseRootsView, i as parseFoldersCommand, l as PANEL_ENDPOINTS, o as resolvePanelPrimaryRoot, s as revealArgv, t as COMMAND_NAME, u as parsePanelCall } from "./command-DyOWujPo.js";
9
+ import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, l as RegistryAuthorityLease, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, t as DEFAULT_MAX_ADDITIONAL_ROOTS, u as RegistryLeaseContendedError } from "./registry-DCxC5Lay.js";
10
+ import { a as renderRootsReport, c as revealArgv, i as parseFoldersCommand, o as resolvePanelPrimaryRoot, t as COMMAND_NAME } from "./command-CESuwlVt.js";
9
11
  //#region src/index.ts
10
12
  /**
11
13
  * Carrier plugin face for the bare-package-name loader row (see the module
@@ -16,4 +18,4 @@ import { a as renderRootsReport, c as PANEL_CHANNEL, d as parseRevealedView, f a
16
18
  const inject = [];
17
19
  function apply() {}
18
20
  //#endregion
19
- export { COMMAND_NAME, CORE_PACKAGES, DEFAULT_MAX_BYTES, DOMAIN_NAME, DshCompatUnsupportedError, MAX_ALIAS_LENGTH, MultiRootCompatService, MultiRootFileSystem, MultiRootRegistry, MultiRootSandboxProvider, MultiRootScopeService, OPTIONAL_CORE_PACKAGES, PANEL_CHANNEL, PANEL_ENDPOINTS, PLUGIN_SOURCE, REGISTRY_CONTENDED_MESSAGE, REQUIRED_CORE_PACKAGES, RegistryAuthorityLease, RegistryLeaseContendedError, RootValidationError, SUPPORTED_DSH_RELEASES, additionalRootId, apply, assertSupportedInstallation, availableRoots, canonicalRoot, classifyInstallation, classifyStoredRoots, compatEnforcement, composeInstructionMessage, createInstructionMessage, expandRootInput, indexedStatuses, inject, instructionsApi, isCanonicallyUnder, multiRootDomainSpec, parseFoldersCommand, parsePanelCall, parseRevealedView, parseRootsView, readInstalledVersion, removeStatusAt, renderInstructions, renderRootsReport, renderWorkspaceRootsContext, resolvePanelPrimaryRoot, resolveRootRef, revealArgv, sanitizeAdditionalRoots, validateRootCandidate, widenConfined };
21
+ export { AGENT_PTY_MARKER, COMMAND_NAME, CORE_PACKAGES, DEFAULT_MAX_ADDITIONAL_ROOTS, DEFAULT_MAX_BYTES, DOMAIN_NAME, DshCompatUnsupportedError, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MULTI_ROOT_PROVIDER_BRAND, MultiRootCompatService, MultiRootFileSystem, MultiRootProviders, MultiRootRegistry, MultiRootSandboxProvider, MultiRootScopeService, OPTIONAL_CORE_PACKAGES, PANEL_CHANNEL, PANEL_ENDPOINTS, PLUGIN_SOURCE, PtyMarkerContractError, REGISTRY_CONTENDED_MESSAGE, REQUIRED_CORE_PACKAGES, RegistryAuthorityLease, RegistryLeaseContendedError, RootValidationError, SUPPORTED_DSH_RELEASES, additionalRootId, agentPtyMarkerVerdict, apply, assertAgentPtyMarker, assertSupportedInstallation, availableRoots, brandMultiRootProvider, canonicalRoot, classifyInstallation, classifyStoredRoots, compatEnforcement, composeInstructionMessage, createInstructionMessage, expandRootInput, indexedStatuses, inject, instructionsApi, isCanonicallyUnder, isMultiRootProvider, multiRootDomainSpec, parseFoldersCommand, parsePanelCall, parseRevealedView, parseRootsView, readInstalledVersion, removeStatusAt, renderInstructions, renderRootsReport, renderWorkspaceRootsContext, resolvePanelPrimaryRoot, resolveRootRef, revealArgv, sanitizeAdditionalRoots, validateRootCandidate, widenConfined };