@dsh-electron/dsh-plugin-multi-root-workspace 0.1.11 → 0.1.12

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 (49) hide show
  1. package/CHANGELOG.en.md +13 -1
  2. package/CHANGELOG.md +13 -1
  3. package/README.en.md +9 -8
  4. package/README.md +9 -8
  5. package/cordis.patch.yml +3 -2
  6. package/lib/auto-review.js +1 -1
  7. package/lib/client.js +96 -49
  8. package/lib/client.js.map +1 -1
  9. package/lib/{command-BGdz7VBH.js → command-CmLekG1M.js} +3 -2
  10. package/lib/command.js +1 -1
  11. package/lib/{compat-BiW-m_q0.js → compat-Bg2HFVMX.js} +3 -0
  12. package/lib/compat.js +1 -1
  13. package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
  14. package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
  15. package/lib/fs.js +1 -1
  16. package/lib/index.js +7 -6
  17. package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
  18. package/lib/instructions.js +1 -1
  19. package/lib/lsp.js +19 -16
  20. package/lib/native-win32-2waCRkPI.js +312 -0
  21. package/lib/process-fence.js +62 -12
  22. package/lib/protocol-4zdwV7u4.js +641 -0
  23. package/lib/{registry-KSsmU393.js → registry-YTUOjmVu.js} +368 -25
  24. package/lib/registry-operation-C2ZV3Jhw.js +116 -0
  25. package/lib/registry.js +3 -2
  26. package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
  27. package/lib/review-context.js +1 -1
  28. package/lib/sandbox-C-vCJCp3.js +278 -0
  29. package/lib/sandbox.js +1 -1
  30. package/lib/scope.js +65 -9
  31. package/lib/skills.js +48 -29
  32. package/lib/types/authority/native-win32.d.ts +7 -0
  33. package/lib/types/authority/native.d.ts +23 -0
  34. package/lib/types/authority/protocol.d.ts +70 -0
  35. package/lib/types/compat/authority-storage.d.ts +6 -0
  36. package/lib/types/compat/dsh-version.d.ts +4 -4
  37. package/lib/types/compat/open-in-app-device.d.ts +1 -1
  38. package/lib/types/compat/process-entrypoints.d.ts +2 -0
  39. package/lib/types/compat/process-profile.d.ts +3 -0
  40. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  41. package/lib/types/fs.d.ts +4 -1
  42. package/lib/types/instructions.d.ts +0 -47
  43. package/lib/types/registry-operation.d.ts +31 -0
  44. package/lib/types/registry.d.ts +47 -6
  45. package/lib/types/scope.d.ts +10 -0
  46. package/lib/types/skills/catalog.d.ts +3 -0
  47. package/lib/types/skills/pool.d.ts +1 -1
  48. package/lib/workspace-files.js +63 -7
  49. package/package.json +38 -31
@@ -2,6 +2,7 @@ import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
2
  import { r as canonicalPath$1 } from "./windows-runner-contract-B5R2K3mM.js";
3
3
  import { i as canonicalRoot, o as entryRootRef, p as resolveRootRef, r as availableRoots, t as RootValidationError } from "./roots-Ca_W1S9n.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-4zdwV7u4.js";
5
6
  import { closeSync, lstatSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
6
7
  import { isAbsolute, join } from "node:path";
7
8
  import "@deepseek-ai/cordis";
@@ -878,7 +879,7 @@ async function report(registry, primaryRoot, mode) {
878
879
  }
879
880
  /** Render a failure the way the composer displays it. */
880
881
  function failureText(error) {
881
- if (error instanceof RootValidationError) return `/${COMMAND_NAME}: ${error.code}: ${error.message}`;
882
+ if (error instanceof RootValidationError || error instanceof AuthorityRpcError) return `/${COMMAND_NAME}: ${error.code}: ${error.message}`;
882
883
  return `/${COMMAND_NAME}: ${error instanceof Error ? error.message : String(error)}`;
883
884
  }
884
885
  /**
@@ -1268,7 +1269,7 @@ async function dispatchPanelRequest(ctx, endpoint, payload, signal) {
1268
1269
  }
1269
1270
  }
1270
1271
  } catch (error) {
1271
- if (error instanceof RootValidationError) return {
1272
+ if (error instanceof RootValidationError || error instanceof AuthorityRpcError) return {
1272
1273
  ok: false,
1273
1274
  error: {
1274
1275
  code: error.code,
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-BGdz7VBH.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-CmLekG1M.js";
2
2
  export { COMMAND_NAME, apply, inject, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealAccepted, revealArgv };
@@ -53,6 +53,7 @@ const SUPPORTED_DSH_RELEASES = [
53
53
  "0.2.1-rc.2",
54
54
  "0.2.1-rc.3",
55
55
  "0.2.1-rc.4",
56
+ "0.2.1-rc.5",
56
57
  "0.2.2-alpha.2"
57
58
  ];
58
59
  /**
@@ -74,6 +75,7 @@ const REQUIRED_CORE_PACKAGES = [
74
75
  * that is the mixed-installation failure this contract exists to catch.
75
76
  */
76
77
  const OPTIONAL_CORE_PACKAGES = [
78
+ "@deepseek-ai/dsh-storage-json",
77
79
  "@deepseek-ai/dsh-skill",
78
80
  "@deepseek-ai/dsh-skill-filesystem",
79
81
  "@deepseek-ai/dsh-settings",
@@ -103,6 +105,7 @@ const OPTIONAL_CORE_PACKAGES = [
103
105
  * that disagrees with the host is a real mixed installation, not a phantom.
104
106
  */
105
107
  const RUNTIME_OPTIONAL_PACKAGES = [
108
+ "@deepseek-ai/dsh-storage-json",
106
109
  "@deepseek-ai/dsh-skill",
107
110
  "@deepseek-ai/dsh-skill-filesystem",
108
111
  "@deepseek-ai/dsh-agent-instructions",
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-BiW-m_q0.js";
1
+ import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-Bg2HFVMX.js";
2
2
  export { DshCompatUnsupportedError, MultiRootCompatService, apply, assertSupportedInstallation, inject };
@@ -1,10 +1,6 @@
1
- import { a as parseWindowsProfile, i as isWindowsNetworkOrDevicePath, n as assertWindowsDenyCapacity, t as DialectUnrecognizedError } from "./windows-runner-contract-B5R2K3mM.js";
1
+ import { t as DialectUnrecognizedError } from "./windows-runner-contract-B5R2K3mM.js";
2
2
  import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
3
- import { brandMultiRootProvider } from "./providers.js";
4
3
  import { posix } from "node:path";
5
- import { SandboxUnavailableError, canonicalPath } from "@deepseek-ai/dsh-sandbox";
6
- import { fileURLToPath } from "node:url";
7
- import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
8
4
  //#region src/compat/sandbox-confine.ts
9
5
  /** Whether a value is a thenable, i.e. whether the installed `confine` is async. */
10
6
  function isThenable(value) {
@@ -30,6 +26,10 @@ function widenConfined(base, widen, argv, policy, signal) {
30
26
  const confined = base(argv, policy, signal);
31
27
  return isThenable(confined) ? confined.then(widen) : widen(confined);
32
28
  }
29
+ /** Public consumer call with the installed provider's arity and return shape. */
30
+ function confineInstalled(provider, argv, policy, signal) {
31
+ return provider.confine.call(provider, argv, policy, signal);
32
+ }
33
33
  //#endregion
34
34
  //#region src/dialects.ts
35
35
  /**
@@ -482,262 +482,4 @@ function restrictProfileArgs(dialect, profileArgs, deniedRoots) {
482
482
  }
483
483
  }
484
484
  //#endregion
485
- //#region src/windows-profile.ts
486
- /** Windows runner argv and capability identity. No Win32 calls at module load. */
487
- /** Recognize the upstream runner, then select our ambient deny-token path.
488
- *
489
- * `protectedHome` is a diagnostics label only: when it names one of the deny
490
- * roots it is forwarded as `--protected-home` so the runner can word its
491
- * hardlink/scan remediation for the protected `$DSH_HOME`. It never changes
492
- * which roots are denied or how.
493
- */
494
- function restrictWindowsProfile(profile, deniedRoots, protectedHome) {
495
- assertWindowsDenyCapacity(deniedRoots.length);
496
- if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
497
- const parsed = parseWindowsProfile(profile.slice(2));
498
- if (deniedRoots.length === 0) throw new DialectUnrecognizedError("Windows ambient restriction requires at least one deny root");
499
- for (const root of [parsed.workspace, ...deniedRoots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
500
- const label = protectedHome !== void 0 && deniedRoots.some((root) => root.toLowerCase() === protectedHome.toLowerCase()) ? ["--protected-home", protectedHome] : [];
501
- return [
502
- profile[0],
503
- fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
504
- "--upstream-runner",
505
- profile[1],
506
- "--workspace",
507
- parsed.workspace,
508
- "--temp",
509
- parsed.temp,
510
- "--mode",
511
- "danger-full-access",
512
- ...deniedRoots.flatMap((root) => ["--deny-root", root]),
513
- ...label
514
- ];
515
- }
516
- function widenWindowsProfile(profile, roots, deniedRoots, primaryRoot) {
517
- if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
518
- const parsed = parseWindowsProfile(profile.slice(2));
519
- for (const root of [parsed.workspace, ...roots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
520
- if (primaryRoot !== void 0 && canonicalPath(parsed.workspace) !== primaryRoot) throw new DialectUnrecognizedError("Windows ACL profile workspace does not match the policy root");
521
- {
522
- const granted = parsed.mode === "workspace-write" ? [
523
- parsed.workspace,
524
- parsed.temp,
525
- ...roots
526
- ] : roots;
527
- const covered = deniedRoots.filter((root) => granted.some((target) => windowsContained(root, target) || windowsContained(target, root)));
528
- if (covered.length > 0) throw new DialectUnrecognizedError(`Windows ACL cannot deny [${covered.join(", ")}] inside a writable grant — the restricted-token model only grants`);
529
- }
530
- if (roots.length === 0) return [...profile];
531
- return [
532
- profile[0],
533
- fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
534
- "--upstream-runner",
535
- profile[1],
536
- ...profile.slice(2),
537
- ...roots.flatMap((root) => ["--additional-root", root])
538
- ];
539
- }
540
- /**
541
- * Containment over Windows SPELLINGS, not the host OS: the profile this
542
- * function rewrites may be produced for a Windows host while the check runs
543
- * in tests on POSIX, so the comparison cannot borrow the host's separator or
544
- * case rules.
545
- */
546
- function windowsContained(candidate, root) {
547
- const normalize = (path) => path.replaceAll("/", "\\").replace(/\\+$/, "").toLowerCase();
548
- const target = normalize(candidate);
549
- const base = normalize(root);
550
- return target === base || target.startsWith(`${base}\\`);
551
- }
552
- //#endregion
553
- //#region src/sandbox.ts
554
- /**
555
- * Local process-sandbox provider over the multi-root scope. Registers as
556
- * `ctx.sandbox`; the runner chain, probing, denial dialects, and Windows ACL
557
- * grant mechanics stay the upstream implementation's.
558
- */
559
- var MultiRootSandboxProvider = class extends LocalSandboxProvider {
560
- static inject = ["sandboxPolicy", "multiRootScope"];
561
- constructor(ctx, config) {
562
- super(ctx, config);
563
- brandMultiRootProvider(this, "sandbox");
564
- }
565
- /**
566
- * Wrap `argv` so it executes confined under `policy` on this host, granting
567
- * the scope's additional roots inside the SAME dialect profile.
568
- *
569
- * Under `read-only` nothing is added (the mode denies every write, additional
570
- * roots included), and `danger-full-access` never reaches here at all: the
571
- * bash executor and the PTY backend both hand their argv straight to the local
572
- * runtime in that mode. Widening therefore only ever ADDS the scope's
573
- * additional roots to an upstream `workspace-write` profile.
574
- *
575
- * The return shape is the INSTALLED upstream provider's: synchronous on a
576
- * release with a synchronous `confine`, a promise on a release with an
577
- * asynchronous one. `widenConfined` preserves whichever it is — see
578
- * `./compat/sandbox-confine.ts`.
579
- * @param argv - the exact argv the caller is about to spawn.
580
- * @param policy - the file-effect policy this execution runs under.
581
- * @param signal - cancellation, forwarded to releases that accept it.
582
- * @returns the argv to spawn instead, plus the selected backend's facts.
583
- */
584
- confine(argv, policy, signal) {
585
- const scope = this.ctx.multiRootScope.resolve(policy);
586
- if (policy.mode === "workspace-write" && scope.authority === "unavailable") {
587
- const prefix = this.clampedExecution(policy, scope.deniedRoots, signal);
588
- return widenConfined(() => prefix, (facts) => ({
589
- ...facts,
590
- argv: [...facts.argv, ...argv]
591
- }), argv, policy, signal);
592
- }
593
- const base = super.confine;
594
- return widenConfined(base.bind(this), (confined) => this.widenForScope(confined, argv, policy, scope), argv, policy, signal);
595
- }
596
- /**
597
- * Graft the scope's effective grants and denies onto one upstream `confine`
598
- * result.
599
- *
600
- * Pure with respect to the upstream call: it only ever reads the scope and
601
- * rewrites `argv`. `enforcement`, `denialSignatures` and `runnerFailureRules`
602
- * are passed through untouched, because the bash executor derives its denial
603
- * and enforcement reporting from them.
604
- *
605
- * Mode behavior (B9, R2): `workspace-write` widens the grant with the
606
- * effective writable roots AND denies any restricted root the upstream
607
- * grants could reach (a `read-only` root registered under the platform
608
- * temp). `read-only` grafts the fixed `read-write` roots back onto the
609
- * deny-all baseline — the three-state matrix's standout cell — and then
610
- * applies the SAME deny pass: a grafted writable parent must not silently
611
- * swallow a denied child (Seatbelt appends the deny after the allow, bwrap
612
- * adds a mandatory `--ro-bind`, Landlock / windows-acl refuse the
613
- * unrepresentable subtraction). `danger-full-access` never reaches
614
- * here (consumers bypass `confine`); its deny semantics live in
615
- * {@link restrictedArgvPrefix}.
616
- * @param confined - the upstream result for this call.
617
- * @param argv - the caller's argv, needed to re-split the wrapped form.
618
- * @param policy - the per-call policy.
619
- * @param scope - the authorization snapshot taken at dispatch.
620
- * @returns the upstream result element for element, or one with a rewritten argv.
621
- */
622
- widenForScope(confined, argv, policy, scope) {
623
- if (scope.authority === "unavailable" || policy.mode !== "workspace-write" && policy.mode !== "read-only") return confined;
624
- const writable = scope.writableRoots;
625
- const denied = scope.deniedRoots;
626
- if (writable.length === 0 && (denied.length === 0 || policy.mode === "read-only")) return confined;
627
- try {
628
- const shape = splitConfined(confined.argv, argv);
629
- if (shape.dialect === "windows-acl") return {
630
- ...confined,
631
- argv: [
632
- ...widenWindowsProfile(shape.profileArgs, writable, denied, scope.primaryRoot),
633
- "--",
634
- ...argv
635
- ]
636
- };
637
- const widened = policy.mode === "workspace-write" ? widenProfileArgs(shape.dialect, shape.profileArgs, policy, writable) : graftProfileArgs(shape.dialect, shape.profileArgs, writable);
638
- const profileArgs = denyProfileArgs(shape.dialect, widened, denied);
639
- return {
640
- ...confined,
641
- argv: [
642
- ...profileArgs,
643
- "--",
644
- ...argv
645
- ]
646
- };
647
- } catch (error) {
648
- if (!(error instanceof DialectUnrecognizedError)) throw error;
649
- throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot apply the additional workspace roots (writable: [${writable.join(", ")}], denied: [${denied.join(", ")}]) under the primary root ${scope.primaryRoot} — ${error.message}`);
650
- }
651
- }
652
- /**
653
- * Build the runner-prefix that confines an ambient-writable execution to
654
- * everything EXCEPT `deniedRoots` — the `danger-full-access` + explicit
655
- * `read-only` cell, which the shell, PTY and PTC consumers never route
656
- * through `confine()` at all.
657
- *
658
- * The restricted profile does not depend on the callee's argv, so the prefix
659
- * is computed once per call here and the synchronous `ctx.subprocess.spawn`
660
- * seam only has to prepend it. The upstream profile builders stay private:
661
- * this probes `confine` under `read-only` for the runner's argument skeleton
662
- * and rewrites the file-effect part per dialect. Windows selects the
663
- * standalone ambient deny-token runner. Landlock cannot express
664
- * "everything except", so this combination selects bubblewrap; unusable
665
- * runners fail without spawning an unconfined command.
666
- *
667
- * The return preserves the installed `confine` shape: a synchronous result
668
- * on a synchronous release, a promise on an asynchronous one.
669
- * @param policy - the caller's policy (mode must be `danger-full-access`).
670
- * @param deniedRoots - canonical roots this execution must not write.
671
- * @param signal - cancellation, forwarded to releases that accept it.
672
- * @param protectedHome - diagnostics label: which deny root is the
673
- * protected `$DSH_HOME`. Forwarded to the Windows runner only when it is
674
- * a member of `deniedRoots`; it never changes authorization.
675
- * @returns the argv prefix, ending at the `--` separator.
676
- */
677
- restrictedArgvPrefix(policy, deniedRoots, signal, protectedHome) {
678
- return mapConfined(this.restrictedExecution(policy, deniedRoots, signal, protectedHome), (confined) => confined.argv);
679
- }
680
- /** Full-access restriction and the actual selected backend's diagnostics. */
681
- restrictedExecution(policy, deniedRoots, signal, protectedHome) {
682
- const probePolicy = {
683
- mode: "read-only",
684
- workspaceRoot: policy.workspaceRoot,
685
- ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
686
- };
687
- const probe = super.confine.call(this, ["true"], probePolicy, signal);
688
- const restrict = (confined) => {
689
- try {
690
- const shape = splitConfined(confined.argv, ["true"]);
691
- const argv = [...shape.dialect === "windows-acl" ? restrictWindowsProfile(shape.profileArgs, deniedRoots, protectedHome) : restrictProfileArgs(shape.dialect, shape.profileArgs, deniedRoots), "--"];
692
- return shape.dialect === "landlock" ? {
693
- argv,
694
- enforcement: "full",
695
- denialSignatures: ["read-only file system"],
696
- runnerFailureRules: [{ fatalSignatures: ["bwrap: "] }]
697
- } : {
698
- ...confined,
699
- argv
700
- };
701
- } catch (error) {
702
- if (!(error instanceof DialectUnrecognizedError)) throw error;
703
- throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot confine an ambient execution away from the denied roots [${deniedRoots.join(", ")}] — ${error.message}`);
704
- }
705
- };
706
- return widenConfined(() => probe, restrict, ["true"], probePolicy, signal);
707
- }
708
- /**
709
- * Limit an execution with unknown registry authority to the primary root.
710
- * Shared temp grants are unsafe: another process may have registered a
711
- * read-only root beneath them. Harvest the upstream read-only skeleton and
712
- * graft only the primary root, retaining diagnostics and the sync/async shape.
713
- * Explicit denies still win; an unrepresentable overlap refuses launch.
714
- * @param policy - the caller's execution policy.
715
- * @param deniedRoots - known negative boundaries, including protected home.
716
- * @param signal - cancellation forwarded to upstream.
717
- * @returns the confined prefix ending at `--`, without the caller's command.
718
- */
719
- clampedExecution(policy, deniedRoots, signal) {
720
- const probePolicy = {
721
- mode: "read-only",
722
- workspaceRoot: policy.workspaceRoot,
723
- ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
724
- };
725
- const probe = super.confine.call(this, ["true"], probePolicy, signal);
726
- const clamp = (confined) => {
727
- try {
728
- const shape = splitConfined(confined.argv, ["true"]);
729
- const profileArgs = shape.dialect === "windows-acl" ? widenWindowsProfile(shape.profileArgs, [canonicalPath(policy.workspaceRoot)], deniedRoots, canonicalPath(policy.workspaceRoot)) : denyProfileArgs(shape.dialect, graftProfileArgs(shape.dialect, shape.profileArgs, [canonicalPath(policy.workspaceRoot)]), deniedRoots);
730
- return {
731
- ...confined,
732
- argv: [...profileArgs, "--"]
733
- };
734
- } catch (error) {
735
- if (!(error instanceof DialectUnrecognizedError)) throw error;
736
- throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot limit an execution to the primary root while the registry authority is unavailable — ${error.message}`);
737
- }
738
- };
739
- return widenConfined(() => probe, clamp, ["true"], probePolicy, signal);
740
- }
741
- };
742
- //#endregion
743
- export { widenConfined as n, MultiRootSandboxProvider as t };
485
+ export { splitConfined as a, mapConfined as c, restrictProfileArgs as i, widenConfined as l, detectDialect as n, widenProfileArgs as o, graftProfileArgs as r, confineInstalled as s, denyProfileArgs as t };
@@ -1,6 +1,7 @@
1
1
  import { brandMultiRootProvider } from "./providers.js";
2
+ import { statSync } from "node:fs";
2
3
  import { dirname, sep } from "node:path";
3
- import { writableRoots } from "@deepseek-ai/dsh-sandbox";
4
+ import { canonicalPath, writableRoots } from "@deepseek-ai/dsh-sandbox";
4
5
  import { stat } from "node:fs/promises";
5
6
  import { FsError } from "@deepseek-ai/dsh-fs";
6
7
  import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
@@ -88,7 +89,8 @@ var MultiRootFileSystem = class extends LocalFileSystem {
88
89
  * @returns the write outcome from the inherited backend.
89
90
  */
90
91
  async writeText(target, content, expected, signal, sandboxPolicy) {
91
- return super.writeText(await this.checkedTarget(target, sandboxPolicy), content, expected, signal);
92
+ const policy = structuredClone(sandboxPolicy ?? this.ctx.sandboxPolicy.resolve());
93
+ return this.withOperation(policy, "fs-write", async (scope) => super.writeText(await this.checkedTarget(target, policy, scope), content, expected, signal));
92
94
  }
93
95
  /**
94
96
  * Fence the edit by the per-call policy, then delegate to the inherited
@@ -102,7 +104,14 @@ var MultiRootFileSystem = class extends LocalFileSystem {
102
104
  * @returns the edit outcome from the inherited backend.
103
105
  */
104
106
  async editText(target, edit, expected, signal, sandboxPolicy) {
105
- return super.editText(await this.checkedTarget(target, sandboxPolicy), edit, expected, signal);
107
+ const policy = structuredClone(sandboxPolicy ?? this.ctx.sandboxPolicy.resolve());
108
+ return this.withOperation(policy, "fs-edit", async (scope) => super.editText(await this.checkedTarget(target, policy, scope), edit, expected, signal));
109
+ }
110
+ /** The provider must also work without the gated registry (ADR-0009). */
111
+ async withOperation(policy, kind, body) {
112
+ const registry = this.ctx.get("multiRootRegistry");
113
+ if (registry !== void 0) return registry.withOperation(policy, kind, (operation) => body(operation.scope));
114
+ return body(this.ctx.multiRootScope.resolve(policy));
106
115
  }
107
116
  /**
108
117
  * The writable roots of one confined call: the upstream derivation (primary
@@ -121,7 +130,14 @@ var MultiRootFileSystem = class extends LocalFileSystem {
121
130
  rootsFor(policy, scope) {
122
131
  if (scope.authority === "unavailable") return policy.mode === "read-only" ? [] : [scope.primaryRoot];
123
132
  const roots = writableRoots(policy);
124
- for (const root of scope.writableRoots) if (!roots.includes(root)) roots.push(root);
133
+ for (const root of scope.writableRoots) {
134
+ try {
135
+ if (canonicalPath(root) !== root || !statSync(root).isDirectory()) continue;
136
+ } catch {
137
+ continue;
138
+ }
139
+ if (!roots.includes(root)) roots.push(root);
140
+ }
125
141
  return roots;
126
142
  }
127
143
  /**
@@ -149,12 +165,11 @@ var MultiRootFileSystem = class extends LocalFileSystem {
149
165
  * to primary-root-only writes (no shared temp or additional-root grants)
150
166
  * until the authority returns.
151
167
  * @param target - the resolved target about to be mutated.
152
- * @param sandboxPolicy - the per-call policy; omit for the deployment fallback.
168
+ * @param policy - the per-call policy captured before admission.
169
+ * @param scope - the admission snapshot shared by every fence check.
153
170
  * @returns the fresh canonical target the mutation must use.
154
171
  */
155
- async checkedTarget(target, sandboxPolicy) {
156
- const policy = sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
157
- const scope = this.ctx.multiRootScope.resolve(policy);
172
+ async checkedTarget(target, policy, scope) {
158
173
  const effectiveMode = scope.authority === "unavailable" && policy.mode === "danger-full-access" ? "workspace-write" : policy.mode;
159
174
  if (effectiveMode === "danger-full-access" && scope.deniedRoots.length === 0) return target;
160
175
  const fresh = await this.resolve(target.displayPath);
package/lib/fs.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import "./providers.js";
2
- import { t as MultiRootFileSystem } from "./fs-BTXySwSG.js";
2
+ import { t as MultiRootFileSystem } from "./fs-CfzJq2wd.js";
3
3
  export { MultiRootFileSystem, MultiRootFileSystem as default };
package/lib/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { i as renderInstructions, n as instructionsApi, t as createInstructionMessage } from "./llm-message-fYORacvh.js";
2
- import { a as composeInstructionMessage, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES } from "./instructions-MePoRigy.js";
2
+ import { a as composeInstructionMessage, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES } from "./instructions-KsUwUFE0.js";
3
3
  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-Ca_W1S9n.js";
4
- import { _ as classifyInstallation, c as agentPtyMarkerVerdict, d as HOST_SERVICE_PACKAGES, f as OPTIONAL_CORE_PACKAGES, g as SUPPORTED_DSH_RELEASES, h as RUNTIME_OPTIONAL_PACKAGES, i as assertSupportedInstallation, l as assertAgentPtyMarker, m as RUNNER_ANCHORED_PACKAGES, n as MultiRootCompatService, o as AGENT_PTY_MARKER, p as REQUIRED_CORE_PACKAGES, s as PtyMarkerContractError, t as DshCompatUnsupportedError, u as CORE_PACKAGES, v as compatEnforcement, y as readInstalledVersion } from "./compat-BiW-m_q0.js";
5
- import { n as widenConfined, t as MultiRootSandboxProvider } from "./sandbox-CXk5h-ct.js";
4
+ import { _ as classifyInstallation, c as agentPtyMarkerVerdict, d as HOST_SERVICE_PACKAGES, f as OPTIONAL_CORE_PACKAGES, g as SUPPORTED_DSH_RELEASES, h as RUNTIME_OPTIONAL_PACKAGES, i as assertSupportedInstallation, l as assertAgentPtyMarker, m as RUNNER_ANCHORED_PACKAGES, n as MultiRootCompatService, o as AGENT_PTY_MARKER, p as REQUIRED_CORE_PACKAGES, s as PtyMarkerContractError, t as DshCompatUnsupportedError, u as CORE_PACKAGES, v as compatEnforcement, y as readInstalledVersion } from "./compat-Bg2HFVMX.js";
5
+ import { l as widenConfined } from "./dialects-Dr4rDR-H.js";
6
6
  import { MULTI_ROOT_PROVIDER_BRAND, MultiRootProviders, brandMultiRootProvider, isMultiRootProvider } from "./providers.js";
7
- import { t as MultiRootFileSystem } from "./fs-BTXySwSG.js";
7
+ import { t as MultiRootFileSystem } from "./fs-CfzJq2wd.js";
8
+ import { t as MultiRootSandboxProvider } from "./sandbox-C-vCJCp3.js";
8
9
  import { a as parseRevealedView, i as parsePanelCall, n as PANEL_ENDPOINTS, o as parseRootsView, t as PANEL_CHANNEL } from "./contract-CStSEOcK.js";
9
10
  import { MultiRootScopeService, renderWorkspaceRootsContext, sanitizeAdditionalRoots } from "./scope.js";
10
- 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-KSsmU393.js";
11
- import { a as renderRootsReport, c as revealArgv, i as parseFoldersCommand, o as resolvePanelPrimaryRoot, t as COMMAND_NAME } from "./command-BGdz7VBH.js";
11
+ 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-YTUOjmVu.js";
12
+ import { a as renderRootsReport, c as revealArgv, i as parseFoldersCommand, o as resolvePanelPrimaryRoot, t as COMMAND_NAME } from "./command-CmLekG1M.js";
12
13
  //#region src/index.ts
13
14
  /**
14
15
  * Carrier plugin face for the bare-package-name loader row (see the module
@@ -1,4 +1,5 @@
1
1
  import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
+ import { c as withRegistryRead, s as withRegistryAdmission } from "./registry-operation-C2ZV3Jhw.js";
2
3
  import { n as instructionsApi, t as createInstructionMessage } from "./llm-message-fYORacvh.js";
3
4
  import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
4
5
  import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
@@ -6,8 +7,8 @@ import { createHash } from "node:crypto";
6
7
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
7
8
  import "@deepseek-ai/cordis";
8
9
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
9
- import { stat } from "node:fs/promises";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { stat } from "node:fs/promises";
11
12
  //#region src/compat/tool-result.ts
12
13
  /**
13
14
  * Whether a tool-result message reports failure, on every supported release.
@@ -367,39 +368,43 @@ function apply(ctx, config = {}) {
367
368
  return [...states.get(session)?.provenanceRoots ?? []];
368
369
  },
369
370
  async snapshot(agent, roots, target, signal) {
370
- signal.throwIfAborted();
371
- if (roots.length === 0) return [];
372
- const api = await instructionsApi();
373
- if (api === void 0) throw new Error("review-context: instruction discovery unavailable");
374
- const existing = states.get(agent.session);
375
- if (existing?.overflow) throw new Error("review-context: successful work index exceeded");
376
- const state = {
377
- scopes: new Map([...existing?.scopes ?? []].map(([key, value]) => [key, {
378
- ...value,
379
- digest: void 0
380
- }])),
381
- provenanceRoots: new Set(existing?.provenanceRoots),
382
- touched: /* @__PURE__ */ new Set([...existing?.touched ?? [], ...existing?.worked ?? []]),
383
- worked: /* @__PURE__ */ new Set()
384
- };
385
- for (const path of replayedInstructionTargets(agent.session.snapshotEvents(), agent.session.header.cwd)) state.touched.add(path);
386
- if (target !== void 0) state.touched.add(target);
387
- const cwd = effectiveDirectory(scope, agent.session);
388
- const reviewCwd = cwd === void 0 ? void 0 : canonicalPath(cwd);
389
- const result = [];
390
- for (const root of roots) {
391
- const plan = await planRoot(scope, api, root, config, state, signal, true, reviewCwd);
392
- for (const { file } of plan.deliveries) result.push({
393
- root,
394
- path: file.absolutePath,
395
- relativeDirectory: relativeDirectoryOf(root, dirname(file.absolutePath)),
396
- digest: digestOf(file.content),
397
- text: file.content
398
- });
399
- }
400
- signal.throwIfAborted();
401
- if (Buffer.byteLength(JSON.stringify(result), "utf8") > maxBytes) throw new Error("review-context: instruction budget exceeded");
402
- return result;
371
+ return withRegistryRead(scope, scope.sandboxPolicy.resolve({ session: agent.session }).workspaceRoot, "instructions", async () => {
372
+ const allowed = new Set(scope.multiRootScope.scopeOf(scope.sandboxPolicy.resolve({ session: agent.session }).workspaceRoot));
373
+ roots = roots.filter((root) => allowed.has(root));
374
+ signal.throwIfAborted();
375
+ if (roots.length === 0) return [];
376
+ const api = await instructionsApi();
377
+ if (api === void 0) throw new Error("review-context: instruction discovery unavailable");
378
+ const existing = states.get(agent.session);
379
+ if (existing?.overflow) throw new Error("review-context: successful work index exceeded");
380
+ const state = {
381
+ scopes: new Map([...existing?.scopes ?? []].map(([key, value]) => [key, {
382
+ ...value,
383
+ digest: void 0
384
+ }])),
385
+ provenanceRoots: new Set(existing?.provenanceRoots),
386
+ touched: /* @__PURE__ */ new Set([...existing?.touched ?? [], ...existing?.worked ?? []]),
387
+ worked: /* @__PURE__ */ new Set()
388
+ };
389
+ for (const path of replayedInstructionTargets(agent.session.snapshotEvents(), agent.session.header.cwd)) state.touched.add(path);
390
+ if (target !== void 0) state.touched.add(target);
391
+ const cwd = effectiveDirectory(scope, agent.session);
392
+ const reviewCwd = cwd === void 0 ? void 0 : canonicalPath(cwd);
393
+ const result = [];
394
+ for (const root of roots) {
395
+ const plan = await planRoot(scope, api, root, config, state, signal, true, reviewCwd);
396
+ for (const { file } of plan.deliveries) result.push({
397
+ root,
398
+ path: file.absolutePath,
399
+ relativeDirectory: relativeDirectoryOf(root, dirname(file.absolutePath)),
400
+ digest: digestOf(file.content),
401
+ text: file.content
402
+ });
403
+ }
404
+ signal.throwIfAborted();
405
+ if (Buffer.byteLength(JSON.stringify(result), "utf8") > maxBytes) throw new Error("review-context: instruction budget exceeded");
406
+ return result;
407
+ });
403
408
  }
404
409
  });
405
410
  scope.on("fs/observed", (target, _observation, actor) => {
@@ -412,7 +417,7 @@ function apply(ctx, config = {}) {
412
417
  scope.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
413
418
  const decision = await next();
414
419
  if (decision.kind !== "enter") return decision;
415
- const seeded = await pending(scope, states, agent, config, maxBytes, signal, decision.messages);
420
+ const seeded = await withRegistryAdmission(scope, scope.sandboxPolicy.resolve({ session: agent.session }), "instructions", () => pending(scope, states, agent, config, maxBytes, signal, decision.messages));
416
421
  if (seeded === void 0) return decision;
417
422
  signal.throwIfAborted();
418
423
  const lastClaimed = decision.messages.findLastIndex((message) => messages.includes(message));
@@ -436,8 +441,9 @@ function apply(ctx, config = {}) {
436
441
  * @param signal - the turn's cancellation signal.
437
442
  * @returns the message to inject, or `undefined`.
438
443
  */
439
- async function pending(ctx, states, agent, config, maxBytes, signal, messages) {
444
+ async function pending(ctx, states, agent, config, maxBytes, signal, messages, retried = false) {
440
445
  const session = agent.session;
446
+ const sourceEpoch = ctx.get("multiRootRegistry")?.epoch;
441
447
  const state = states.get(session) ?? {
442
448
  scopes: /* @__PURE__ */ new Map(),
443
449
  provenanceRoots: /* @__PURE__ */ new Set(),
@@ -508,7 +514,7 @@ async function pending(ctx, states, agent, config, maxBytes, signal, messages) {
508
514
  }
509
515
  }
510
516
  }
511
- const current = new Set(ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session })).additionalRoots);
517
+ const current = new Set(await withRegistryAdmission(ctx, ctx.sandboxPolicy.resolve({ session }), "instructions", () => ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session })).additionalRoots));
512
518
  const revoked = [...new Set([...state.scopes.values()].map((entry) => entry.root))].filter((root) => !current.has(root));
513
519
  const liveUpdates = updates.filter((entry) => current.has(entry.scope.root));
514
520
  const text = composeInstructionMessage(rendered.filter((entry) => current.has(entry.root)), revoked, gone.filter((entry) => current.has(entry.scope.root)).map((entry) => entry.scope));
@@ -517,6 +523,16 @@ async function pending(ctx, states, agent, config, maxBytes, signal, messages) {
517
523
  plugin: PLUGIN_SOURCE
518
524
  });
519
525
  signal.throwIfAborted();
526
+ if (ctx.get("multiRootRegistry")?.shared) {
527
+ const final = await withRegistryAdmission(ctx, ctx.sandboxPolicy.resolve({ session }), "instructions", () => ({
528
+ roots: ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session })).additionalRoots,
529
+ epoch: ctx.get("multiRootRegistry")?.epoch
530
+ }));
531
+ if (sourceEpoch !== final.epoch || final.roots.length !== current.size || final.roots.some((root) => !current.has(root))) {
532
+ if (retried) return void 0;
533
+ return withRegistryAdmission(ctx, ctx.sandboxPolicy.resolve({ session }), "instructions", () => pending(ctx, states, agent, config, maxBytes, signal, messages, true));
534
+ }
535
+ }
520
536
  if (!retryTouches) for (const touch of evaluatedTouches) state.touched.delete(touch);
521
537
  if (text !== void 0 || liveUpdates.length > 0) {
522
538
  for (const root of revoked) for (const [key, entry] of state.scopes) if (entry.root === root) state.scopes.delete(key);
@@ -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-MePoRigy.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-KsUwUFE0.js";
2
2
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_SOURCE_BYTES, PLUGIN_SOURCE, apply, composeInstructionMessage, inject, touchedPathOfToolCall };
package/lib/lsp.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
2
+ import { c as withRegistryRead } from "./registry-operation-C2ZV3Jhw.js";
2
3
  import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
3
4
  import { t as routeWorkspacePath } from "./workspace-routing-YPxaQ4V3.js";
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -17,25 +18,27 @@ function apply(ctx) {
17
18
  const session = callers.getStore();
18
19
  const primary = session === void 0 ? request.workspaceRoot : session.header.cwd ?? scope.get("sandboxPolicy")?.resolve({ session }).workspaceRoot;
19
20
  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
- });
21
+ return await withRegistryRead(scope, primary, "lsp", async () => {
22
+ if (scope.multiRootScope.scopeOf(primary).length === 0 && session === void 0) return original.call(this, request, signal);
23
+ const route = await routeWorkspacePath(scope, primary, request.filePath, signal, request.workspaceRoot);
24
+ if (route === void 0) {
25
+ const target = await scope.fs.resolve(request.filePath, {
26
+ cwd: request.workspaceRoot,
27
+ ...signal === void 0 ? {} : { signal }
28
+ });
29
+ return original.call(this, {
30
+ ...request,
31
+ workspaceRoot: primary,
32
+ filePath: scope.fs.processPath(target)
33
+ }, signal);
34
+ }
35
+ if (session === void 0 && route.workspaceRoot === route.primaryRoot) return original.call(this, request, signal);
27
36
  return original.call(this, {
28
37
  ...request,
29
- workspaceRoot: primary,
30
- filePath: scope.fs.processPath(target)
38
+ workspaceRoot: route.workspaceRoot,
39
+ filePath: route.absolutePath
31
40
  }, 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);
41
+ });
39
42
  }), "multi-root:lsp routing");
40
43
  });
41
44
  }