@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
@@ -1,8 +1,11 @@
1
- import { l as isCanonicallyUnder } from "./roots-DasWEBJk.js";
1
+ import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
+ import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
3
+ import { l as isCanonicallyUnder } from "./roots-B-0l8mNG.js";
2
4
  import { createRequire } from "node:module";
3
5
  import "@deepseek-ai/cordis";
4
- import { createHash } from "node:crypto";
5
6
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
7
+ import { createHash } from "node:crypto";
8
+ import { AsyncLocalStorage } from "node:async_hooks";
6
9
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
7
10
  //#region src/compat/agent-instructions.ts
8
11
  /**
@@ -221,6 +224,51 @@ function toolResultFailed(message) {
221
224
  return block.isError === true;
222
225
  }
223
226
  //#endregion
227
+ //#region src/compat/instruction-overlap.ts
228
+ /** Recognize files already represented by the native instruction baseline in this step. */
229
+ function nativeInstructionCopies(messages, cwd) {
230
+ const represented = /* @__PURE__ */ new Map();
231
+ const removed = /* @__PURE__ */ new Set();
232
+ let replacesBaseline = false;
233
+ for (const message of messages) {
234
+ const source = message.source;
235
+ if (source.kind !== "agent-instructions" || source.changes === void 0) continue;
236
+ if (source.baseline === true) {
237
+ replacesBaseline = true;
238
+ represented.clear();
239
+ }
240
+ for (const change of source.changes) {
241
+ if (change.action !== "remove") continue;
242
+ removed.add(change.path);
243
+ for (const [path, copy] of represented) if (copy.displayPath === change.path) represented.delete(path);
244
+ }
245
+ if (cwd === void 0 || source.baselineIdentity === void 0) continue;
246
+ let identity;
247
+ try {
248
+ identity = JSON.parse(source.baselineIdentity);
249
+ } catch {
250
+ continue;
251
+ }
252
+ if (typeof identity.projectRoot !== "string") continue;
253
+ const project = resolve(cwd, identity.projectRoot);
254
+ for (const change of source.changes) {
255
+ if (change.action === "remove" || typeof change.digest !== "string") continue;
256
+ if (/^(?:~|\$|user-global[\\/]|agents-global[\\/])/.test(change.path)) continue;
257
+ represented.set(resolve(project, change.path), {
258
+ digest: change.digest,
259
+ displayPath: change.path
260
+ });
261
+ }
262
+ }
263
+ return {
264
+ pathOf: (file) => {
265
+ const copy = represented.get(file.absolutePath);
266
+ return copy?.digest === createHash("sha1").update(file.content).digest("hex") ? copy.displayPath : void 0;
267
+ },
268
+ withdraws: (displayPath) => replacesBaseline || removed.has(displayPath)
269
+ };
270
+ }
271
+ //#endregion
224
272
  //#region src/instructions.ts
225
273
  /**
226
274
  * `multi-root-instructions`: the additional roots' own instruction files, made
@@ -342,16 +390,13 @@ function touchedPathOfToolCall(name, argumentsJson) {
342
390
  * The directory a touched path's instructions would live in.
343
391
  *
344
392
  * Tool arguments are the model's raw input, so a relative `file_path` is
345
- * resolved against the session cwd — the base the tool layer itself uses —
346
- * rather than against this process's cwd. Mac's `realpath` spelling is applied
347
- * here so the result compares against the canonical roots.
348
- * @param path - a touched path, absolute or relative to the session cwd.
349
- * @param session - the session the touch belongs to.
393
+ * captured as an absolute path at the operation boundary. Mac's `realpath`
394
+ * spelling is applied here so it compares against the canonical roots.
395
+ * @param path - the absolute touched file path.
350
396
  * @returns the canonical parent directory, or `undefined` when it cannot be named.
351
397
  */
352
- function parentDirectoryOf(path, session) {
353
- const cwd = session.header.cwd;
354
- const absolute = isAbsolute(path) ? path : cwd === void 0 ? void 0 : resolve(cwd, path);
398
+ function parentDirectoryOf(path) {
399
+ const absolute = isAbsolute(path) ? path : void 0;
355
400
  if (absolute === void 0) return void 0;
356
401
  try {
357
402
  return canonicalPath(dirname(absolute));
@@ -385,6 +430,7 @@ function composeInstructionMessage(rendered, revoked, removed) {
385
430
  /** This row is gated on the compatibility contract like every other provider. */
386
431
  const inject = [
387
432
  "multiRootCompat",
433
+ "multiRootProviders",
388
434
  "multiRootScope",
389
435
  "sandboxPolicy"
390
436
  ];
@@ -407,6 +453,8 @@ function apply(ctx, config = {}) {
407
453
  if (maxBytes <= 0 || !Number.isFinite(maxBytes)) return;
408
454
  const states = /* @__PURE__ */ new WeakMap();
409
455
  const callsBySession = /* @__PURE__ */ new WeakMap();
456
+ const executions = new AsyncLocalStorage();
457
+ aroundTool(ctx, (exec, next) => executions.run(exec, next));
410
458
  const stateOf = (session) => {
411
459
  const existing = states.get(session);
412
460
  if (existing !== void 0) return existing;
@@ -427,7 +475,8 @@ function apply(ctx, config = {}) {
427
475
  }
428
476
  pendingCalls.set(event.data.callId, {
429
477
  name: event.data.name,
430
- arguments: event.data.arguments
478
+ arguments: event.data.arguments,
479
+ cwd: effectiveDirectory(ctx, session)
431
480
  });
432
481
  return;
433
482
  }
@@ -437,17 +486,24 @@ function apply(ctx, config = {}) {
437
486
  pendingCalls.delete(callId);
438
487
  if (call === void 0) return;
439
488
  if (event.data.error !== void 0 || toolResultFailed(event.data.message)) return;
440
- const path = touchedPathOfToolCall(call.name, call.arguments);
489
+ const path = call.observedPath ?? touchedPathOfToolCall(call.name, call.arguments);
441
490
  if (path === void 0) return;
442
491
  const state = stateOf(session);
443
492
  if (state.touched.size >= MAX_TOUCHED_PATHS) return;
444
- state.touched.add(path);
493
+ if (isAbsolute(path)) state.touched.add(path);
494
+ else if (call.cwd !== void 0) state.touched.add(resolve(call.cwd, path));
445
495
  });
446
496
  ctx.inject(["fs"], (scope) => {
497
+ scope.on("fs/observed", (target, _observation, actor) => {
498
+ const exec = executions.getStore();
499
+ if (exec?.agent === void 0 || FILE_TOUCH_TOOL_NAMES[exec.name] !== true || actor !== exec) return;
500
+ const call = callsBySession.get(exec.agent.session)?.get(exec.callId);
501
+ if (call !== void 0) call.observedPath = scope.fs.processPath(target);
502
+ });
447
503
  scope.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
448
504
  const decision = await next();
449
505
  if (decision.kind !== "enter") return decision;
450
- const seeded = await pending(scope, states, agent, config, maxBytes, signal);
506
+ const seeded = await pending(scope, states, agent, config, maxBytes, signal, decision.messages);
451
507
  if (seeded === void 0) return decision;
452
508
  signal.throwIfAborted();
453
509
  const lastClaimed = decision.messages.findLastIndex((message) => messages.includes(message));
@@ -455,7 +511,7 @@ function apply(ctx, config = {}) {
455
511
  ...decision,
456
512
  messages: decision.messages.toSpliced(lastClaimed + 1, 0, seeded)
457
513
  };
458
- });
514
+ }, { prepend: true });
459
515
  });
460
516
  }
461
517
  /**
@@ -471,7 +527,7 @@ function apply(ctx, config = {}) {
471
527
  * @param signal - the turn's cancellation signal.
472
528
  * @returns the message to inject, or `undefined`.
473
529
  */
474
- async function pending(ctx, states, agent, config, maxBytes, signal) {
530
+ async function pending(ctx, states, agent, config, maxBytes, signal, messages) {
475
531
  const session = agent.session;
476
532
  const state = states.get(session) ?? {
477
533
  scopes: /* @__PURE__ */ new Map(),
@@ -481,9 +537,12 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
481
537
  const evaluatedTouches = new Set(state.touched);
482
538
  let retryTouches = false;
483
539
  const scope = ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session }));
484
- const granted = new Set(scope.additionalRoots);
485
- const revoked = [...new Set([...state.scopes.values()].map((entry) => entry.root))].filter((root) => !granted.has(root));
486
540
  const rendered = [];
541
+ const nativeCopy = nativeInstructionCopies(messages, effectiveDirectory(ctx, session));
542
+ for (const [key, entry] of state.scopes) if (entry.nativePath !== void 0 && nativeCopy.withdraws(entry.nativePath)) state.scopes.set(key, {
543
+ ...entry,
544
+ digest: void 0
545
+ });
487
546
  const gone = [];
488
547
  const updates = [];
489
548
  if (scope.additionalRoots.length > 0) {
@@ -492,14 +551,29 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
492
551
  let remaining = maxBytes;
493
552
  for (const root of scope.additionalRoots) {
494
553
  signal.throwIfAborted();
495
- const plan = await planRoot(ctx, api, root, config, state, session, signal);
554
+ const plan = await planRoot(ctx, api, root, config, state, signal);
496
555
  gone.push(...plan.gone);
497
556
  if (plan.deliveries.length === 0) continue;
498
557
  if (remaining <= 0) {
499
558
  retryTouches = true;
500
559
  continue;
501
560
  }
502
- const { text, omitted, truncated } = api.render(plan.deliveries.map((entry) => entry.file), { maxBytes: remaining });
561
+ const deliveries = plan.deliveries.filter((entry) => {
562
+ const nativePath = nativeCopy.pathOf(entry.file);
563
+ if (nativePath === void 0) return true;
564
+ updates.push({
565
+ key: entry.key,
566
+ scope: {
567
+ root,
568
+ path: entry.file.absolutePath,
569
+ digest: digestOf(entry.file.content),
570
+ nativePath
571
+ }
572
+ });
573
+ return false;
574
+ });
575
+ if (deliveries.length === 0) continue;
576
+ const { text, omitted, truncated } = api.render(deliveries.map((entry) => entry.file), { maxBytes: remaining });
503
577
  const omittedPaths = new Set(omitted.map((file) => file.absolutePath));
504
578
  const truncatedPaths = new Set(truncated.map((file) => file.displayPath));
505
579
  if (omitted.length > 0 || truncated.length > 0 || text === "") retryTouches = true;
@@ -509,7 +583,7 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
509
583
  root,
510
584
  text
511
585
  });
512
- for (const entry of plan.deliveries) {
586
+ for (const entry of deliveries) {
513
587
  if (omittedPaths.has(entry.file.absolutePath)) continue;
514
588
  updates.push({
515
589
  key: entry.key,
@@ -523,17 +597,20 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
523
597
  }
524
598
  }
525
599
  }
526
- const text = composeInstructionMessage(rendered, revoked, gone.map((entry) => entry.scope));
600
+ const current = new Set(ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session })).additionalRoots);
601
+ const revoked = [...new Set([...state.scopes.values()].map((entry) => entry.root))].filter((root) => !current.has(root));
602
+ const liveUpdates = updates.filter((entry) => current.has(entry.scope.root));
603
+ const text = composeInstructionMessage(rendered.filter((entry) => current.has(entry.root)), revoked, gone.filter((entry) => current.has(entry.scope.root)).map((entry) => entry.scope));
527
604
  const message = text === void 0 ? void 0 : await createInstructionMessage({
528
605
  text,
529
606
  plugin: PLUGIN_SOURCE
530
607
  });
531
608
  signal.throwIfAborted();
532
609
  if (!retryTouches) for (const touch of evaluatedTouches) state.touched.delete(touch);
533
- if (text !== void 0) {
610
+ if (text !== void 0 || liveUpdates.length > 0) {
534
611
  for (const root of revoked) for (const [key, entry] of state.scopes) if (entry.root === root) state.scopes.delete(key);
535
612
  for (const entry of gone) state.scopes.delete(entry.key);
536
- for (const entry of updates) state.scopes.set(entry.key, entry.scope);
613
+ for (const entry of liveUpdates) state.scopes.set(entry.key, entry.scope);
537
614
  }
538
615
  return message;
539
616
  }
@@ -554,11 +631,10 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
554
631
  * @param root - the canonical additional root.
555
632
  * @param config - the row's configuration.
556
633
  * @param state - the session's delivery state.
557
- * @param session - the session, for the cwd a relative touch resolves against.
558
634
  * @param signal - the turn's cancellation signal.
559
635
  * @returns the files to send and the deliveries to withdraw.
560
636
  */
561
- async function planRoot(ctx, api, root, config, state, session, signal) {
637
+ async function planRoot(ctx, api, root, config, state, signal) {
562
638
  const delivered = /* @__PURE__ */ new Map();
563
639
  for (const [key, entry] of state.scopes) if (entry.root === root) delivered.set(key, entry);
564
640
  const dirs = /* @__PURE__ */ new Set([root]);
@@ -567,7 +643,7 @@ async function planRoot(ctx, api, root, config, state, session, signal) {
567
643
  if (dir !== root) dirs.add(dir);
568
644
  }
569
645
  for (const touched of state.touched) {
570
- const parent = parentDirectoryOf(touched, session);
646
+ const parent = parentDirectoryOf(touched);
571
647
  if (parent === void 0 || !isCanonicallyUnder(parent, root)) continue;
572
648
  let dir = parent;
573
649
  for (;;) {
@@ -678,16 +754,21 @@ async function loadDirectory(fs, api, root, config, signal, cwd) {
678
754
  }
679
755
  const files = [];
680
756
  for (const candidate of candidates) {
757
+ if (isAbsolute(candidate.displayPath) || resolve(root, candidate.displayPath) !== resolve(candidate.absolutePath)) continue;
681
758
  if (!isCanonicallyUnder(canonicalPath(candidate.absolutePath), root)) continue;
682
759
  try {
683
760
  const target = await fs.resolve(candidate.absolutePath);
761
+ const path = fs.processPath(target);
762
+ if (!isCanonicallyUnder(path, root)) continue;
684
763
  const info = await fs.stat(target, signal);
685
764
  if (info === void 0 || info.type !== "file") continue;
686
765
  if (info.size !== void 0 && info.size > maxSourceBytes) continue;
766
+ const content = await fs.readText(target, signal);
767
+ if (canonicalPath(candidate.absolutePath) !== path || !isCanonicallyUnder(path, root)) continue;
687
768
  files.push({
688
769
  ...candidate,
689
770
  displayPath: candidate.absolutePath,
690
- content: await fs.readText(target, signal)
771
+ content
691
772
  });
692
773
  } catch {
693
774
  continue;
@@ -1,2 +1,2 @@
1
- import { a as composeInstructionMessage, i as apply, n as DEFAULT_MAX_SOURCE_BYTES, o as inject, r as PLUGIN_SOURCE, s as touchedPathOfToolCall, t as DEFAULT_MAX_BYTES } from "./instructions-Cn1Jj5Xi.js";
1
+ import { a as composeInstructionMessage, i as apply, n as DEFAULT_MAX_SOURCE_BYTES, o as inject, r as PLUGIN_SOURCE, s as touchedPathOfToolCall, t as DEFAULT_MAX_BYTES } from "./instructions-D19jRxN0.js";
2
2
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_SOURCE_BYTES, PLUGIN_SOURCE, apply, composeInstructionMessage, inject, touchedPathOfToolCall };
package/lib/lsp.js ADDED
@@ -0,0 +1,43 @@
1
+ import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
2
+ import { t as routeWorkspacePath } from "./workspace-routing-YPxaQ4V3.js";
3
+ import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
4
+ import { AsyncLocalStorage } from "node:async_hooks";
5
+ //#region src/lsp.ts
6
+ const inject = [
7
+ "multiRootCompat",
8
+ "multiRootProviders",
9
+ "multiRootScope"
10
+ ];
11
+ function apply(ctx) {
12
+ const callers = new AsyncLocalStorage();
13
+ aroundTool(ctx, (exec, next) => exec.agent === void 0 ? next() : callers.run(exec.agent.session, next));
14
+ ctx.inject(["lsp", "fs"], (scope) => {
15
+ const service = scope.get("lsp");
16
+ scope.effect(() => wrapMethod(service, "query", (original) => async function(request, signal) {
17
+ const session = callers.getStore();
18
+ const primary = session === void 0 ? request.workspaceRoot : session.header.cwd ?? scope.get("sandboxPolicy")?.resolve({ session }).workspaceRoot;
19
+ if (primary === void 0) throw new Error("multi-root workspace: LSP caller has no session origin or deployment policy");
20
+ if (scope.multiRootScope.scopeOf(primary).length === 0 && session === void 0) return original.call(this, request, signal);
21
+ const route = await routeWorkspacePath(scope, primary, request.filePath, signal, request.workspaceRoot);
22
+ if (route === void 0) {
23
+ const target = await scope.fs.resolve(request.filePath, {
24
+ cwd: request.workspaceRoot,
25
+ ...signal === void 0 ? {} : { signal }
26
+ });
27
+ return original.call(this, {
28
+ ...request,
29
+ workspaceRoot: primary,
30
+ filePath: scope.fs.processPath(target)
31
+ }, signal);
32
+ }
33
+ if (session === void 0 && route.workspaceRoot === route.primaryRoot) return original.call(this, request, signal);
34
+ return original.call(this, {
35
+ ...request,
36
+ workspaceRoot: route.workspaceRoot,
37
+ filePath: route.absolutePath
38
+ }, signal);
39
+ }), "multi-root:lsp routing");
40
+ });
41
+ }
42
+ //#endregion
43
+ export { apply, inject };
@@ -0,0 +1,26 @@
1
+ //#region src/method-wrapper.ts
2
+ /** Wrap a public method on a Cordis service without capturing a caller-context proxy. */
3
+ function wrapMethod(target, key, wrap) {
4
+ const own = Object.getOwnPropertyDescriptor(target, key);
5
+ let prototype = target;
6
+ let descriptor;
7
+ while (prototype !== null && descriptor === void 0) {
8
+ descriptor = Object.getOwnPropertyDescriptor(prototype, key);
9
+ prototype = Object.getPrototypeOf(prototype);
10
+ }
11
+ if (typeof descriptor?.value !== "function") throw new Error(`multi-root: ${String(key)} is not a public method`);
12
+ const value = wrap(descriptor.value);
13
+ Object.defineProperty(target, key, {
14
+ configurable: true,
15
+ writable: true,
16
+ enumerable: own?.enumerable ?? false,
17
+ value
18
+ });
19
+ return () => {
20
+ if (Object.getOwnPropertyDescriptor(target, key)?.value !== value) return;
21
+ if (own === void 0) Reflect.deleteProperty(target, key);
22
+ else Object.defineProperty(target, key, own);
23
+ };
24
+ }
25
+ //#endregion
26
+ export { wrapMethod as t };
@@ -0,0 +1,280 @@
1
+ import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ //#region src/compat/process-result.ts
4
+ const object$1 = (value) => typeof value === "object" && value !== null;
5
+ const thenable = (value) => typeof value?.then === "function";
6
+ const textOf = (value) => typeof value === "string" ? value : object$1(value) && typeof value.text === "string" ? value.text : "";
7
+ const stderrOf = (value) => textOf(value.stderr) + (object$1(value.error) ? textOf(value.error.message) : "") + (object$1(value.failure) ? textOf(value.failure.message) : "");
8
+ const exitCodeOf = (value) => typeof value.exitCode === "number" || value.exitCode === null ? value.exitCode : value.ok === false || object$1(value.error) || object$1(value.failure) ? 1 : 0;
9
+ /** Denial and runner-failure verdicts for one settled process — the same
10
+ * non-zero-exit + stderr-signature rules the sandbox executors apply: a fatal
11
+ * runner line is an infrastructure failure, never a policy denial. */
12
+ const classifyOutcome = (exitCode, stderr, facts) => {
13
+ const nonzero = exitCode !== null && exitCode !== 0;
14
+ const lines = stderr.toLowerCase().split(/\r?\n/);
15
+ const runnerFailed = nonzero && facts.runnerFailureRules.some((rule) => (rule.allowedExitCodes === void 0 || rule.allowedExitCodes.includes(exitCode)) && lines.some((line) => !rule.informationalLines?.some((info) => info.toLowerCase() === line) && rule.fatalSignatures.some((signature) => line.includes(signature.toLowerCase()))));
16
+ return {
17
+ denied: nonzero && !runnerFailed && facts.denialSignatures.some((signature) => stderr.toLowerCase().includes(signature.toLowerCase())),
18
+ runnerFailed
19
+ };
20
+ };
21
+ /** The `sandbox` stamp one settled outcome carries, merged over any stamp the
22
+ * confining executor already installed. */
23
+ const sandboxStamp = (value, facts, policy, stderr) => {
24
+ const { denied, runnerFailed } = classifyOutcome(exitCodeOf(value), stderr ?? stderrOf(value), facts);
25
+ return {
26
+ ...object$1(value.sandbox) ? value.sandbox : {},
27
+ mode: policy.mode,
28
+ enforcement: facts.enforcement,
29
+ denied,
30
+ ...runnerFailed ? { runnerFailed: true } : {}
31
+ };
32
+ };
33
+ function stampProcessResult(value, facts, policy) {
34
+ if (!object$1(value)) return value;
35
+ return {
36
+ ...value,
37
+ sandbox: sandboxStamp(value, facts, policy)
38
+ };
39
+ }
40
+ /** Whether a non-thenable, non-`result()` value is a live process handle
41
+ * rather than settled result data: it owns a `done` settle hook or callable
42
+ * members, or carries a shaped prototype. Copying it would freeze
43
+ * `status`/`exitCode` where consumers poll them and sever `readOutput`/`kill`
44
+ * identity — the model-facing `proc.sandbox` read happens on this very
45
+ * object, so the stamp must land in place.
46
+ */
47
+ const liveHandle = (value) => thenable(value.done) || typeof value.done === "function" || Object.values(value).some((member) => typeof member === "function") || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null;
48
+ function decorateProcessOutcome(value, facts, policy, diagnostics) {
49
+ if (object$1(value) && typeof value.then === "function") return value.then((result) => decorateProcessOutcome(result, facts, policy, diagnostics));
50
+ if (object$1(value) && typeof value.result === "function") {
51
+ let cached;
52
+ let settled = false;
53
+ wrapMethod(value, "result", (original) => function(...args) {
54
+ if (!settled) {
55
+ cached = decorateProcessOutcome(original.apply(this, args), facts, policy, diagnostics);
56
+ settled = true;
57
+ }
58
+ return cached;
59
+ });
60
+ return value;
61
+ }
62
+ if (object$1(value) && liveHandle(value)) {
63
+ const settle = () => {
64
+ try {
65
+ value.sandbox = sandboxStamp(value, facts, policy, diagnostics?.stderr?.());
66
+ } catch {}
67
+ };
68
+ if (thenable(value.done)) {
69
+ value.done = value.done.then((outcome) => {
70
+ settle();
71
+ return decorateProcessOutcome(outcome, facts, policy, diagnostics);
72
+ }, (error) => {
73
+ settle();
74
+ throw error;
75
+ });
76
+ return value;
77
+ }
78
+ if (typeof value.done === "function") {
79
+ wrapMethod(value, "done", (original) => function(...args) {
80
+ const outcome = original.apply(this, args);
81
+ return thenable(outcome) ? outcome.then((resolved) => {
82
+ settle();
83
+ return decorateProcessOutcome(resolved, facts, policy, diagnostics);
84
+ }, (error) => {
85
+ settle();
86
+ throw error;
87
+ }) : (settle(), decorateProcessOutcome(outcome, facts, policy, diagnostics));
88
+ });
89
+ return value;
90
+ }
91
+ settle();
92
+ return value;
93
+ }
94
+ return stampProcessResult(value, facts, policy);
95
+ }
96
+ //#endregion
97
+ //#region src/compat/process-entrypoints.ts
98
+ /**
99
+ * The public shell executor entries present on the mounted service, in this
100
+ * order: `execute`, `run`, `start` (`start` existed through 0.1.6).
101
+ * @param shell - the mounted `ctx.shell` service.
102
+ * @returns the present entries — a structural probe only.
103
+ */
104
+ function shellEntrypoints(shell) {
105
+ const record = shell;
106
+ return [
107
+ "execute",
108
+ "run",
109
+ "start"
110
+ ].filter((key) => typeof record[key] === "function");
111
+ }
112
+ /**
113
+ * The PTC runtime's public entry: `['run']` when it is a function; never the
114
+ * concrete runtime's private `execute` helper.
115
+ * @param runtime - the mounted `ctx.ptcRuntime` service.
116
+ * @returns the present entries — a structural probe only.
117
+ */
118
+ function ptcEntrypoints(runtime) {
119
+ return typeof runtime.run === "function" ? ["run"] : [];
120
+ }
121
+ //#endregion
122
+ //#region src/process-fence.ts
123
+ /**
124
+ * `multi-root-process-fence`: the B9 seam for the one cell `ctx.sandbox.confine`
125
+ * never sees — `danger-full-access` with explicit `read-only` additional roots.
126
+ *
127
+ * Upstream, every confined consumer routes through `ctx.sandbox.confine`, which
128
+ * this bundle's provider already widens (grants, the read-only fixed-write
129
+ * graft, and the workspace-write deny edge). But the shell executor, the PTC
130
+ * runtime and the PTY backend all short-circuit that mode: `policy.mode ===
131
+ * 'danger-full-access'` returns the bare argv, so an explicit `read-only` root
132
+ * would silently stay writable. This row closes the gap without touching any
133
+ * upstream executor:
134
+ *
135
+ * - `ctx.subprocess.spawn` is wrapped to prepend a precomputed restricted
136
+ * profile whenever the AsyncLocalStorage below carries one. `spawn` is
137
+ * synchronous, so the profile — which never depends on the callee's argv —
138
+ * is built by the wrapping consumer, not here.
139
+ * - `ctx.shell.execute` (bash and pwsh alike, and `start` where an older
140
+ * release still exposes it — `compat/process-entrypoints.ts` owns which
141
+ * entries a mounted executor actually carries) resolves the per-call policy
142
+ * exactly the way the executor itself does, and — under Full-access with
143
+ * deny roots — asks the sandbox provider for the restricted prefix and runs
144
+ * the call inside the store, so every subprocess the executor launches is
145
+ * confined.
146
+ * - `ctx.ptcRuntime.run` does the same for `run_code`.
147
+ * - `ctx.subprocess.spawnTerminal` keys on the `DSH_PTY_SESSION_ID` marker:
148
+ * every supported `dsh-terminal-bash` backend stamps it (alongside
149
+ * `DSH_SESSION_ID`) when it launches an agent's PTY, and no other caller —
150
+ * above all the panel's terminal controller — ever sets it. A spec without
151
+ * the marker is a USER terminal, passed through untouched in every mode:
152
+ * the sandbox protects agent behavior, and upstream never confines the
153
+ * user's own terminal. Because that marker is the ONLY discriminator, the
154
+ * `multi-root-compat` gate asserts it at boot (`compat/pty-marker.ts`):
155
+ * an installed backend whose emitted code no longer stamps it fails the
156
+ * plugin load rather than silently reclassifying agent PTYs as the user's.
157
+ * `ctx.terminals` itself is deliberately NOT wrapped:
158
+ * upstream mounts it inside `optional-persistent-terminals` with
159
+ * `isolate: { terminals: true }`, so the registry entry is unreachable from
160
+ * this row — the shared `spawnTerminal` seam plus the env discriminator
161
+ * covers both the un-isolated SDK mount and the isolated web mount.
162
+ *
163
+ * The same seam carries the unknown-authority downgrade: while the scope
164
+ * reports `authority: 'unavailable'` — a contended, storage-failed or
165
+ * config-invalid registry publishes an empty deny set that is NOT evidence —
166
+ * `danger-full-access` executions run inside a primary-root-only
167
+ * profile (with the deny set applied) instead of ambient.
168
+ *
169
+ * Windows uses the standalone ambient deny-token runner. Landlock cannot subtract a subtree, so that combination selects bubblewrap.
170
+ * An unusable mount runner fails without starting an unconfined process.
171
+ *
172
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/process-fence
173
+ */
174
+ const inject = [
175
+ "multiRootCompat",
176
+ "multiRootProviders",
177
+ "multiRootScope",
178
+ "sandbox",
179
+ "sandboxPolicy"
180
+ ];
181
+ const processDenyStore = new AsyncLocalStorage();
182
+ /** Whether a value is a thenable, i.e. whether the installed `confine` is async. */
183
+ function isThenable(value) {
184
+ return typeof value?.then === "function";
185
+ }
186
+ const object = (value) => typeof value === "object" && value !== null;
187
+ function apply(ctx) {
188
+ const policyOf = (spec) => spec?.sandboxPolicy ?? ctx.sandboxPolicy.resolve();
189
+ /**
190
+ * The confined facts for one Full-access call, or `undefined` when the call
191
+ * needs no confinement (not full-access, and nothing to deny while the
192
+ * registry is the store's authority). An `unavailable` scope authority
193
+ * clamps the call to a primary-root-only profile: the empty deny
194
+ * set a contended or failed registry publishes is not evidence that nothing
195
+ * is restricted. May throw synchronously — `SandboxUnavailableError` from a
196
+ * dialect that cannot express the confinement, which must propagate to the
197
+ * caller, never be swallowed.
198
+ */
199
+ const prepare = (policy, signal) => {
200
+ if (policy.mode !== "danger-full-access") return void 0;
201
+ const scope = ctx.multiRootScope.resolve(policy);
202
+ if (scope.authority === "unavailable") return ctx.sandbox.clampedExecution(policy, scope.deniedRoots, signal);
203
+ if (scope.deniedRoots.length === 0) return void 0;
204
+ return ctx.sandbox.restrictedExecution(policy, scope.deniedRoots, signal, scope.protectedHome);
205
+ };
206
+ /**
207
+ * Run `body` under the restricted deny context when `prepared` is a prefix —
208
+ * preserving the sync/async shape of the prepared value itself.
209
+ */
210
+ const runDenied = (prepared, policy, body) => {
211
+ const run = (facts) => {
212
+ const store = { argvPrefix: facts.argv };
213
+ return processDenyStore.run(store, () => decorateProcessOutcome(body(), facts, policy, { stderr: () => store.stderr?.() }));
214
+ };
215
+ return isThenable(prepared) ? prepared.then(run) : run(prepared);
216
+ };
217
+ ctx.inject(["subprocess"], (scope) => {
218
+ scope.effect(() => wrapMethod(scope.subprocess, "spawn", (original) => function(spec) {
219
+ const store = processDenyStore.getStore();
220
+ if (store === void 0) return original.call(this, spec);
221
+ const handle = original.call(this, {
222
+ ...spec,
223
+ argv: [...store.argvPrefix, ...spec.argv]
224
+ });
225
+ const stderr = object(handle) && object(handle.collected) && object(handle.collected.stderr) && typeof handle.collected.stderr.readFrom === "function" ? handle.collected.stderr : void 0;
226
+ if (stderr !== void 0) store.stderr = () => {
227
+ try {
228
+ return stderr.readFrom(0).text;
229
+ } catch {
230
+ return;
231
+ }
232
+ };
233
+ return handle;
234
+ }));
235
+ scope.effect(() => wrapMethod(scope.subprocess, "spawnTerminal", (original) => async function(spec) {
236
+ const ptySessionId = spec.env?.DSH_PTY_SESSION_ID;
237
+ if (ptySessionId === void 0 || ptySessionId === "") return await original.call(this, spec);
238
+ const sessionId = spec.env?.DSH_SESSION_ID;
239
+ const session = sessionId === void 0 || sessionId === "" ? void 0 : ctx.get("sessions")?.get(sessionId);
240
+ if (session === void 0) {
241
+ if (!ctx.multiRootScope.hasActiveDenyRoots()) return await original.call(this, spec);
242
+ throw new Error(`multi-root workspace: agent terminal spawn carries a session marker that does not resolve to a live session (${JSON.stringify(sessionId ?? ptySessionId)}), and explicit read-only roots are or may be registered — refusing to spawn an unconfined terminal`);
243
+ }
244
+ const policy = ctx.sandboxPolicy.resolve({ session });
245
+ const prepared = prepare(policy, spec.signal);
246
+ if (prepared === void 0) return await original.call(this, spec);
247
+ const argvPrefix = (await prepared).argv;
248
+ return await original.call(this, {
249
+ ...spec,
250
+ argv: [...argvPrefix, ...spec.argv]
251
+ });
252
+ }));
253
+ });
254
+ /**
255
+ * The wrapper every execute-style consumer entry shares: resolve the policy
256
+ * the way the upstream consumer does, and when the call runs ambient under
257
+ * Full-access with roots to deny, execute it inside the restricted store so
258
+ * every subprocess it launches is confined.
259
+ */
260
+ const deniedEntry = (scope, target, key) => {
261
+ scope.effect(() => wrapMethod(target, key, (original) => function(spec, ...rest) {
262
+ const policy = policyOf(spec);
263
+ const prepared = prepare(policy, spec?.signal);
264
+ if (prepared === void 0) return original.call(this, spec, ...rest);
265
+ return runDenied(prepared, policy, () => original.call(this, spec, ...rest));
266
+ }));
267
+ };
268
+ ctx.inject(["shell"], (scope) => {
269
+ const shell = scope.get("shell");
270
+ if (shell === void 0) return;
271
+ for (const key of shellEntrypoints(shell)) deniedEntry(scope, shell, key);
272
+ });
273
+ ctx.inject(["ptcRuntime"], (scope) => {
274
+ const runtime = scope.get("ptcRuntime");
275
+ if (runtime === void 0) return;
276
+ for (const key of ptcEntrypoints(runtime)) deniedEntry(scope, runtime, key);
277
+ });
278
+ }
279
+ //#endregion
280
+ export { apply, inject };