@dsh-electron/dsh-plugin-multi-root-workspace 0.1.10 → 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 (60) hide show
  1. package/CHANGELOG.en.md +25 -1
  2. package/CHANGELOG.md +25 -1
  3. package/README.en.md +11 -9
  4. package/README.md +11 -9
  5. package/cordis.patch.yml +3 -2
  6. package/lib/auto-review.js +1 -1
  7. package/lib/client.js +252 -129
  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-D-MljsPV.js → compat-Bg2HFVMX.js} +6 -1
  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 +70 -10
  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/client/open-in-app/OpenInAppAction.d.ts +5 -3
  36. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +16 -10
  37. package/lib/types/client/open-in-app/adapter.d.ts +5 -1
  38. package/lib/types/client/open-in-app/controller.d.ts +5 -2
  39. package/lib/types/client/open-in-app/device.d.ts +11 -0
  40. package/lib/types/client/open-in-app/locales.d.ts +1 -1
  41. package/lib/types/client/open-in-app/open-path.d.ts +6 -5
  42. package/lib/types/client/open-in-app/open-toast.d.ts +39 -0
  43. package/lib/types/client/open-in-app/outcome.d.ts +23 -0
  44. package/lib/types/client/styles.d.ts +1 -1
  45. package/lib/types/compat/authority-storage.d.ts +6 -0
  46. package/lib/types/compat/dsh-version.d.ts +4 -4
  47. package/lib/types/compat/open-in-app-device.d.ts +23 -0
  48. package/lib/types/compat/process-entrypoints.d.ts +2 -0
  49. package/lib/types/compat/process-profile.d.ts +3 -0
  50. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  51. package/lib/types/fs.d.ts +4 -1
  52. package/lib/types/instructions.d.ts +0 -47
  53. package/lib/types/registry-operation.d.ts +31 -0
  54. package/lib/types/registry.d.ts +47 -6
  55. package/lib/types/scope.d.ts +12 -0
  56. package/lib/types/skills/catalog.d.ts +3 -0
  57. package/lib/types/skills/pool.d.ts +1 -1
  58. package/lib/workspace-files.js +63 -7
  59. package/package.json +87 -80
  60. package/lib/types/client/open-in-app/open-failure-toast.d.ts +0 -11
@@ -0,0 +1,116 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ //#region src/registry-operation.ts
4
+ /**
5
+ * Host-local operation context after ordered Registry authority admission.
6
+ * This is Host-only state, never a wire permit or a reusable cached grant.
7
+ */
8
+ const operations = new AsyncLocalStorage();
9
+ function currentRegistryOperation(proof) {
10
+ const frame = operations.getStore();
11
+ return frame?.active === true && frame.proof === proof ? frame.operation : void 0;
12
+ }
13
+ /** Capture by value: neither later publication nor a caller can mutate admission. */
14
+ function freezeOperationScope(scope) {
15
+ return Object.freeze({
16
+ ...scope,
17
+ additionalRoots: Object.freeze([...scope.additionalRoots]),
18
+ roots: Object.freeze(scope.roots.map((root) => Object.freeze({ ...root }))),
19
+ writableRoots: Object.freeze([...scope.writableRoots]),
20
+ deniedRoots: Object.freeze([...scope.deniedRoots])
21
+ });
22
+ }
23
+ /** Execute outside the authority queue; completion also expires async descendants. */
24
+ async function runRegistryOperation(scope, kind, authorityEpoch, revision, body, proof, mode) {
25
+ let active = true;
26
+ const operation = Object.freeze({
27
+ identity: Object.freeze({
28
+ operationId: randomUUID(),
29
+ kind,
30
+ authorityEpoch,
31
+ revision,
32
+ mode,
33
+ primaryRoot: scope.primaryRoot
34
+ }),
35
+ get scope() {
36
+ if (!active) throw new Error("multi-root registry: operation admission has expired");
37
+ return scope;
38
+ }
39
+ });
40
+ const frame = {
41
+ proof,
42
+ operation,
43
+ active: true,
44
+ profiles: []
45
+ };
46
+ try {
47
+ return await operations.run(frame, () => body(operation));
48
+ } finally {
49
+ active = false;
50
+ frame.active = false;
51
+ }
52
+ }
53
+ /** Optional Registry seam for public consumers; the legacy composition stays exact. */
54
+ async function withRegistryRead(ctx, primary, kind, body) {
55
+ const registry = ctx.get("multiRootRegistry");
56
+ return registry?.shared ? registry.withOperation({
57
+ workspaceRoot: primary,
58
+ mode: "read-only"
59
+ }, kind, () => body()) : body();
60
+ }
61
+ async function withRegistryAdmission(ctx, policy, kind, body) {
62
+ const registry = ctx.get("multiRootRegistry");
63
+ return registry?.shared ? registry.withOperation(policy, kind, () => body()) : body();
64
+ }
65
+ /** Preserve a legacy synchronous entry while granting only the safe primary-root fallback. */
66
+ function runPrimaryOperation(scope, mode, proof, body) {
67
+ let active = true;
68
+ const frame = {
69
+ proof,
70
+ operation: Object.freeze({
71
+ identity: Object.freeze({
72
+ operationId: randomUUID(),
73
+ kind: "process",
74
+ authorityEpoch: void 0,
75
+ revision: 0,
76
+ mode,
77
+ primaryRoot: scope.primaryRoot
78
+ }),
79
+ get scope() {
80
+ if (!active) throw new Error("operation admission has expired");
81
+ return scope;
82
+ }
83
+ }),
84
+ active: true,
85
+ profiles: []
86
+ };
87
+ const finish = () => {
88
+ active = false;
89
+ frame.active = false;
90
+ };
91
+ try {
92
+ const result = operations.run(frame, body);
93
+ if (typeof result?.then === "function") return Promise.resolve(result).finally(finish);
94
+ finish();
95
+ return result;
96
+ } catch (error) {
97
+ finish();
98
+ throw error;
99
+ }
100
+ }
101
+ /** Only profiles produced within this exact admission can reach a sync spawn. */
102
+ function rememberOperationProfile(proof, argv, capped) {
103
+ const frame = operations.getStore();
104
+ if (frame?.active !== true || frame.proof !== proof) return;
105
+ if (frame.profiles.length >= 128) throw new Error("too many confined profiles in one operation");
106
+ frame.profiles.push({
107
+ argv: [...argv],
108
+ capped
109
+ });
110
+ }
111
+ function operationPermitsProfile(proof, argv, capped) {
112
+ const frame = operations.getStore();
113
+ return frame?.active === true && frame.proof === proof && frame.profiles.some((profile) => profile.capped === capped && profile.argv.length <= argv.length && profile.argv.every((arg, i) => argv[i] === arg));
114
+ }
115
+ //#endregion
116
+ export { runPrimaryOperation as a, withRegistryRead as c, rememberOperationProfile as i, freezeOperationScope as n, runRegistryOperation as o, operationPermitsProfile as r, withRegistryAdmission as s, currentRegistryOperation as t };
package/lib/registry.js CHANGED
@@ -1,2 +1,3 @@
1
- import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-KSsmU393.js";
2
- export { DEFAULT_MAX_ADDITIONAL_ROOTS, DOMAIN_NAME, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec };
1
+ import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-YTUOjmVu.js";
2
+ import { o as nativeHostAvailability } from "./protocol-4zdwV7u4.js";
3
+ export { DEFAULT_MAX_ADDITIONAL_ROOTS, DOMAIN_NAME, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec, nativeHostAvailability };
@@ -1,5 +1,5 @@
1
1
  import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
- import { s as touchedPathOfToolCall } from "./instructions-MePoRigy.js";
2
+ import { s as touchedPathOfToolCall } from "./instructions-KsUwUFE0.js";
3
3
  import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
4
4
  import { createHash } from "node:crypto";
5
5
  import { resolve } from "node:path";
@@ -1,2 +1,2 @@
1
- import { t as MultiRootReviewContext } from "./review-context-AH-SXHmj.js";
1
+ import { t as MultiRootReviewContext } from "./review-context-NDpKeMCc.js";
2
2
  export { MultiRootReviewContext, MultiRootReviewContext as default };
@@ -0,0 +1,278 @@
1
+ import { a as parseWindowsProfile, i as isWindowsNetworkOrDevicePath, n as assertWindowsDenyCapacity, t as DialectUnrecognizedError } from "./windows-runner-contract-B5R2K3mM.js";
2
+ import { a as splitConfined, c as mapConfined, i as restrictProfileArgs, l as widenConfined, o as widenProfileArgs, r as graftProfileArgs, t as denyProfileArgs } from "./dialects-Dr4rDR-H.js";
3
+ import { brandMultiRootProvider } from "./providers.js";
4
+ import { SandboxUnavailableError, canonicalPath } from "@deepseek-ai/dsh-sandbox";
5
+ import { fileURLToPath } from "node:url";
6
+ import { LocalSandboxProvider } from "@deepseek-ai/dsh-sandbox-local";
7
+ //#region src/windows-profile.ts
8
+ /** Windows runner argv and capability identity. No Win32 calls at module load. */
9
+ /** Recognize the upstream runner, then select our ambient deny-token path.
10
+ *
11
+ * `protectedHome` is a diagnostics label only: when it names one of the deny
12
+ * roots it is forwarded as `--protected-home` so the runner can word its
13
+ * hardlink/scan remediation for the protected `$DSH_HOME`. It never changes
14
+ * which roots are denied or how.
15
+ */
16
+ function restrictWindowsProfile(profile, deniedRoots, protectedHome) {
17
+ assertWindowsDenyCapacity(deniedRoots.length);
18
+ if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
19
+ const parsed = parseWindowsProfile(profile.slice(2));
20
+ if (deniedRoots.length === 0) throw new DialectUnrecognizedError("Windows ambient restriction requires at least one deny root");
21
+ for (const root of [parsed.workspace, ...deniedRoots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
22
+ const label = protectedHome !== void 0 && deniedRoots.some((root) => root.toLowerCase() === protectedHome.toLowerCase()) ? ["--protected-home", protectedHome] : [];
23
+ return [
24
+ profile[0],
25
+ fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
26
+ "--upstream-runner",
27
+ profile[1],
28
+ "--workspace",
29
+ parsed.workspace,
30
+ "--temp",
31
+ parsed.temp,
32
+ "--mode",
33
+ "danger-full-access",
34
+ ...deniedRoots.flatMap((root) => ["--deny-root", root]),
35
+ ...label
36
+ ];
37
+ }
38
+ function widenWindowsProfile(profile, roots, deniedRoots, primaryRoot) {
39
+ if (profile.length < 2 || profile[0] === void 0 || profile[1]?.startsWith("--")) throw new DialectUnrecognizedError("Windows ACL runner invocation is not [node, runner, ...flags]");
40
+ const parsed = parseWindowsProfile(profile.slice(2));
41
+ for (const root of [parsed.workspace, ...roots]) if (isWindowsNetworkOrDevicePath(root)) throw new DialectUnrecognizedError(`Windows ACL confinement cannot cover network or device paths: ${root}`);
42
+ if (primaryRoot !== void 0 && canonicalPath(parsed.workspace) !== primaryRoot) throw new DialectUnrecognizedError("Windows ACL profile workspace does not match the policy root");
43
+ {
44
+ const granted = parsed.mode === "workspace-write" ? [
45
+ parsed.workspace,
46
+ parsed.temp,
47
+ ...roots
48
+ ] : roots;
49
+ const covered = deniedRoots.filter((root) => granted.some((target) => windowsContained(root, target) || windowsContained(target, root)));
50
+ if (covered.length > 0) throw new DialectUnrecognizedError(`Windows ACL cannot deny [${covered.join(", ")}] inside a writable grant — the restricted-token model only grants`);
51
+ }
52
+ if (roots.length === 0) return [...profile];
53
+ return [
54
+ profile[0],
55
+ fileURLToPath(new URL("./windows-runner.js", import.meta.url)),
56
+ "--upstream-runner",
57
+ profile[1],
58
+ ...profile.slice(2),
59
+ ...roots.flatMap((root) => ["--additional-root", root])
60
+ ];
61
+ }
62
+ /**
63
+ * Containment over Windows SPELLINGS, not the host OS: the profile this
64
+ * function rewrites may be produced for a Windows host while the check runs
65
+ * in tests on POSIX, so the comparison cannot borrow the host's separator or
66
+ * case rules.
67
+ */
68
+ function windowsContained(candidate, root) {
69
+ const normalize = (path) => path.replaceAll("/", "\\").replace(/\\+$/, "").toLowerCase();
70
+ const target = normalize(candidate);
71
+ const base = normalize(root);
72
+ return target === base || target.startsWith(`${base}\\`);
73
+ }
74
+ //#endregion
75
+ //#region src/sandbox.ts
76
+ /**
77
+ * Local process-sandbox provider over the multi-root scope. Registers as
78
+ * `ctx.sandbox`; the runner chain, probing, denial dialects, and Windows ACL
79
+ * grant mechanics stay the upstream implementation's.
80
+ */
81
+ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
82
+ static inject = ["sandboxPolicy", "multiRootScope"];
83
+ constructor(ctx, config) {
84
+ super(ctx, config);
85
+ brandMultiRootProvider(this, "sandbox");
86
+ }
87
+ /**
88
+ * Wrap `argv` so it executes confined under `policy` on this host, granting
89
+ * the scope's additional roots inside the SAME dialect profile.
90
+ *
91
+ * Under `read-only` nothing is added (the mode denies every write, additional
92
+ * roots included), and `danger-full-access` never reaches here at all: the
93
+ * bash executor and the PTY backend both hand their argv straight to the local
94
+ * runtime in that mode. Widening therefore only ever ADDS the scope's
95
+ * additional roots to an upstream `workspace-write` profile.
96
+ *
97
+ * The return shape is the INSTALLED upstream provider's: synchronous on a
98
+ * release with a synchronous `confine`, a promise on a release with an
99
+ * asynchronous one. `widenConfined` preserves whichever it is — see
100
+ * `./compat/sandbox-confine.ts`.
101
+ * @param argv - the exact argv the caller is about to spawn.
102
+ * @param policy - the file-effect policy this execution runs under.
103
+ * @param signal - cancellation, forwarded to releases that accept it.
104
+ * @returns the argv to spawn instead, plus the selected backend's facts.
105
+ */
106
+ confine(argv, policy, signal) {
107
+ const scope = this.ctx.multiRootScope.resolve(policy);
108
+ const remember = (facts) => {
109
+ this.ctx.multiRootScope.rememberProfile(facts.argv);
110
+ return facts;
111
+ };
112
+ if (policy.mode === "workspace-write" && scope.authority === "unavailable") {
113
+ const prefix = this.clampedExecution(policy, scope.deniedRoots, signal);
114
+ return widenConfined(() => prefix, (facts) => remember({
115
+ ...facts,
116
+ argv: [...facts.argv, ...argv]
117
+ }), argv, policy, signal);
118
+ }
119
+ const base = super.confine;
120
+ return widenConfined(base.bind(this), (confined) => remember(this.widenForScope(confined, argv, policy, scope)), argv, policy, signal);
121
+ }
122
+ /**
123
+ * Graft the scope's effective grants and denies onto one upstream `confine`
124
+ * result.
125
+ *
126
+ * Pure with respect to the upstream call: it only ever reads the scope and
127
+ * rewrites `argv`. `enforcement`, `denialSignatures` and `runnerFailureRules`
128
+ * are passed through untouched, because the bash executor derives its denial
129
+ * and enforcement reporting from them.
130
+ *
131
+ * Mode behavior (B9, R2): `workspace-write` widens the grant with the
132
+ * effective writable roots AND denies any restricted root the upstream
133
+ * grants could reach (a `read-only` root registered under the platform
134
+ * temp). `read-only` grafts the fixed `read-write` roots back onto the
135
+ * deny-all baseline — the three-state matrix's standout cell — and then
136
+ * applies the SAME deny pass: a grafted writable parent must not silently
137
+ * swallow a denied child (Seatbelt appends the deny after the allow, bwrap
138
+ * adds a mandatory `--ro-bind`, Landlock / windows-acl refuse the
139
+ * unrepresentable subtraction). `danger-full-access` never reaches
140
+ * here (consumers bypass `confine`); its deny semantics live in
141
+ * {@link restrictedArgvPrefix}.
142
+ * @param confined - the upstream result for this call.
143
+ * @param argv - the caller's argv, needed to re-split the wrapped form.
144
+ * @param policy - the per-call policy.
145
+ * @param scope - the authorization snapshot taken at dispatch.
146
+ * @returns the upstream result element for element, or one with a rewritten argv.
147
+ */
148
+ widenForScope(confined, argv, policy, scope) {
149
+ if (scope.authority === "unavailable" || policy.mode !== "workspace-write" && policy.mode !== "read-only") return confined;
150
+ const writable = scope.writableRoots;
151
+ const denied = scope.deniedRoots;
152
+ if (writable.length === 0 && (denied.length === 0 || policy.mode === "read-only")) return confined;
153
+ try {
154
+ const shape = splitConfined(confined.argv, argv);
155
+ if (shape.dialect === "windows-acl") return {
156
+ ...confined,
157
+ argv: [
158
+ ...widenWindowsProfile(shape.profileArgs, writable, denied, scope.primaryRoot),
159
+ "--",
160
+ ...argv
161
+ ]
162
+ };
163
+ const widened = policy.mode === "workspace-write" ? widenProfileArgs(shape.dialect, shape.profileArgs, policy, writable) : graftProfileArgs(shape.dialect, shape.profileArgs, writable);
164
+ const profileArgs = denyProfileArgs(shape.dialect, widened, denied);
165
+ return {
166
+ ...confined,
167
+ argv: [
168
+ ...profileArgs,
169
+ "--",
170
+ ...argv
171
+ ]
172
+ };
173
+ } catch (error) {
174
+ if (!(error instanceof DialectUnrecognizedError)) throw error;
175
+ 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}`);
176
+ }
177
+ }
178
+ /**
179
+ * Build the runner-prefix that confines an ambient-writable execution to
180
+ * everything EXCEPT `deniedRoots` — the `danger-full-access` + explicit
181
+ * `read-only` cell, which the shell, PTY and PTC consumers never route
182
+ * through `confine()` at all.
183
+ *
184
+ * The restricted profile does not depend on the callee's argv, so the prefix
185
+ * is computed once per call here and the synchronous `ctx.subprocess.spawn`
186
+ * seam only has to prepend it. The upstream profile builders stay private:
187
+ * this probes `confine` under `read-only` for the runner's argument skeleton
188
+ * and rewrites the file-effect part per dialect. Windows selects the
189
+ * standalone ambient deny-token runner. Landlock cannot express
190
+ * "everything except", so this combination selects bubblewrap; unusable
191
+ * runners fail without spawning an unconfined command.
192
+ *
193
+ * The return preserves the installed `confine` shape: a synchronous result
194
+ * on a synchronous release, a promise on an asynchronous one.
195
+ * @param policy - the caller's policy (mode must be `danger-full-access`).
196
+ * @param deniedRoots - canonical roots this execution must not write.
197
+ * @param signal - cancellation, forwarded to releases that accept it.
198
+ * @param protectedHome - diagnostics label: which deny root is the
199
+ * protected `$DSH_HOME`. Forwarded to the Windows runner only when it is
200
+ * a member of `deniedRoots`; it never changes authorization.
201
+ * @returns the argv prefix, ending at the `--` separator.
202
+ */
203
+ restrictedArgvPrefix(policy, deniedRoots, signal, protectedHome) {
204
+ const result = this.restrictedExecution(policy, deniedRoots, signal, protectedHome);
205
+ return mapConfined(result, (confined) => confined.argv);
206
+ }
207
+ /** Full-access restriction and the actual selected backend's diagnostics. */
208
+ restrictedExecution(policy, deniedRoots, signal, protectedHome) {
209
+ const probePolicy = {
210
+ mode: "read-only",
211
+ workspaceRoot: policy.workspaceRoot,
212
+ ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
213
+ };
214
+ const probe = super.confine.call(this, ["true"], probePolicy, signal);
215
+ const restrict = (confined) => {
216
+ try {
217
+ const shape = splitConfined(confined.argv, ["true"]);
218
+ const argv = [...shape.dialect === "windows-acl" ? restrictWindowsProfile(shape.profileArgs, deniedRoots, protectedHome) : restrictProfileArgs(shape.dialect, shape.profileArgs, deniedRoots), "--"];
219
+ return shape.dialect === "landlock" ? {
220
+ argv,
221
+ enforcement: "full",
222
+ denialSignatures: ["read-only file system"],
223
+ runnerFailureRules: [{ fatalSignatures: ["bwrap: "] }]
224
+ } : {
225
+ ...confined,
226
+ argv
227
+ };
228
+ } catch (error) {
229
+ if (!(error instanceof DialectUnrecognizedError)) throw error;
230
+ throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot confine an ambient execution away from the denied roots [${deniedRoots.join(", ")}] — ${error.message}`);
231
+ }
232
+ };
233
+ return widenConfined(() => probe, (facts) => {
234
+ const result = restrict(facts);
235
+ this.ctx.multiRootScope.rememberProfile(result.argv);
236
+ return result;
237
+ }, ["true"], probePolicy, signal);
238
+ }
239
+ /**
240
+ * Limit an execution with unknown registry authority to the primary root.
241
+ * Shared temp grants are unsafe: another process may have registered a
242
+ * read-only root beneath them. Harvest the upstream read-only skeleton and
243
+ * graft only the primary root, retaining diagnostics and the sync/async shape.
244
+ * Explicit denies still win; an unrepresentable overlap refuses launch.
245
+ * @param policy - the caller's execution policy.
246
+ * @param deniedRoots - known negative boundaries, including protected home.
247
+ * @param signal - cancellation forwarded to upstream.
248
+ * @returns the confined prefix ending at `--`, without the caller's command.
249
+ */
250
+ clampedExecution(policy, deniedRoots, signal) {
251
+ const probePolicy = {
252
+ mode: "read-only",
253
+ workspaceRoot: policy.workspaceRoot,
254
+ ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
255
+ };
256
+ const probe = super.confine.call(this, ["true"], probePolicy, signal);
257
+ const clamp = (confined) => {
258
+ try {
259
+ const shape = splitConfined(confined.argv, ["true"]);
260
+ 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);
261
+ return {
262
+ ...confined,
263
+ argv: [...profileArgs, "--"]
264
+ };
265
+ } catch (error) {
266
+ if (!(error instanceof DialectUnrecognizedError)) throw error;
267
+ throw new SandboxUnavailableError(policy.mode, `multi-root workspace: cannot limit an execution to the primary root while the registry authority is unavailable — ${error.message}`);
268
+ }
269
+ };
270
+ return widenConfined(() => probe, (facts) => {
271
+ const result = clamp(facts);
272
+ this.ctx.multiRootScope.rememberProfile(result.argv);
273
+ return result;
274
+ }, ["true"], probePolicy, signal);
275
+ }
276
+ };
277
+ //#endregion
278
+ export { MultiRootSandboxProvider as t };
package/lib/sandbox.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as MultiRootSandboxProvider } from "./sandbox-CXk5h-ct.js";
2
1
  import "./providers.js";
2
+ import { t as MultiRootSandboxProvider } from "./sandbox-C-vCJCp3.js";
3
3
  export { MultiRootSandboxProvider, MultiRootSandboxProvider as default };
package/lib/scope.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { i as rememberOperationProfile, r as operationPermitsProfile, t as currentRegistryOperation } from "./registry-operation-C2ZV3Jhw.js";
1
2
  import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
2
3
  import { r as effectiveRootAccess } from "./contract-CStSEOcK.js";
3
4
  import { statSync } from "node:fs";
@@ -153,6 +154,7 @@ var MultiRootScopeService = class extends Service {
153
154
  /** Additional roots keyed by canonical primary root; empty until roots are registered. */
154
155
  rootsByPrimary = /* @__PURE__ */ new Map();
155
156
  listeners = /* @__PURE__ */ new Set();
157
+ admissionProof;
156
158
  /**
157
159
  * The registry-authority fact the registry row publishes. Defaults to
158
160
  * `available`: a composition without a registry row can never have
@@ -220,7 +222,7 @@ var MultiRootScopeService = class extends Service {
220
222
  const session = assembly.agent?.session;
221
223
  if (session === void 0) return "";
222
224
  const policy = scope.sandboxPolicy.resolve({ session });
223
- const resolved = this.resolve(policy);
225
+ const resolved = this.snapshot(policy);
224
226
  if (resolved.roots.length === 0 && resolved.authority !== "unavailable" && resolved.protectedHome === void 0) return "";
225
227
  return renderWorkspaceRootsContext({
226
228
  platform: process.platform,
@@ -241,9 +243,52 @@ var MultiRootScopeService = class extends Service {
241
243
  * @param policy - the upstream policy whose workspace root is the primary root.
242
244
  * @returns the canonical scope; `additionalRoots` is empty when none are registered.
243
245
  */
244
- resolve(policy) {
246
+ requireAdmission(proof) {
247
+ this.admissionProof = proof;
248
+ }
249
+ rememberProfile(argv) {
250
+ if (this.admissionProof !== void 0) rememberOperationProfile(this.admissionProof, argv, this.capped());
251
+ }
252
+ permitsProfile(argv) {
253
+ return this.admissionProof === void 0 || operationPermitsProfile(this.admissionProof, argv, this.capped());
254
+ }
255
+ get admissionRequired() {
256
+ return this.admissionProof !== void 0;
257
+ }
258
+ admissionPathsValid() {
259
+ if (this.admissionProof === void 0) return true;
260
+ const operation = currentRegistryOperation(this.admissionProof);
261
+ return operation !== void 0 && operation.scope.additionalRoots.every((root) => {
262
+ try {
263
+ return canonicalPath(root) === root && statSync(root).isDirectory();
264
+ } catch {
265
+ return false;
266
+ }
267
+ });
268
+ }
269
+ hasAdmission() {
270
+ return this.admissionProof === void 0 || currentRegistryOperation(this.admissionProof) !== void 0;
271
+ }
272
+ /** Trusted display/review projection, never an execution permit. */
273
+ snapshot(policy) {
245
274
  return this.project(policy, this.capped());
246
275
  }
276
+ resolveSnapshot(policy, roots, available) {
277
+ return this.project(policy, this.capped(), roots, available ? "available" : "unavailable");
278
+ }
279
+ resolve(policy) {
280
+ if (this.admissionProof === void 0) return this.project(policy, this.capped());
281
+ const operation = currentRegistryOperation(this.admissionProof);
282
+ if (operation !== void 0 && operation.identity.mode === policy.mode && operation.scope.primaryRoot === canonicalPath(policy.workspaceRoot)) {
283
+ const snapshot = operation.scope;
284
+ return this.capped() ? {
285
+ ...snapshot,
286
+ writableRoots: [],
287
+ deniedRoots: [.../* @__PURE__ */ new Set([...snapshot.deniedRoots, ...snapshot.additionalRoots])]
288
+ } : snapshot;
289
+ }
290
+ return this.project(policy, true, void 0, "unavailable");
291
+ }
247
292
  /** Read-only pending-dispatch projection. Does not mint an ALS execution grant. */
248
293
  previewForTool(policy, toolName) {
249
294
  return this.project(policy, this.capForTool(toolName));
@@ -252,14 +297,15 @@ var MultiRootScopeService = class extends Service {
252
297
  capForTool(toolName) {
253
298
  return this.capsInvalid || toolCapStore.getStore()?.capped === true || this.caps[toolName] === "read-only";
254
299
  }
255
- project(policy, capped) {
300
+ project(policy, capped, recorded, authority = this.registryAuthority) {
256
301
  const primaryRoot = canonicalPath(policy.workspaceRoot);
257
- const roots = this.membersOf(primaryRoot);
258
- const writable = capped || this.registryAuthority === "unavailable" ? [] : roots.filter((root) => effectivelyWritable(policy.mode, root.access)).map((root) => root.path);
259
- const denied = (this.rootsByPrimary.get(primaryRoot) ?? []).filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && root.recordedPath !== primaryRoot && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
302
+ const registered = recorded ?? this.rootsByPrimary.get(primaryRoot) ?? [];
303
+ const roots = authority === "unavailable" ? [] : sanitizeMembers(primaryRoot, registered);
304
+ const writable = capped || authority === "unavailable" ? [] : roots.filter((root) => effectivelyWritable(policy.mode, root.access)).map((root) => root.path);
305
+ const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && root.recordedPath !== primaryRoot && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
260
306
  const deniedRoots = [...new Set(denied)];
261
307
  let protectedHome;
262
- if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || this.registryAuthority === "unavailable")) {
308
+ if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || authority === "unavailable")) {
263
309
  const home = canonicalPath(this.dshHome);
264
310
  if (!deniedRoots.includes(home)) {
265
311
  deniedRoots.push(home);
@@ -272,7 +318,7 @@ var MultiRootScopeService = class extends Service {
272
318
  roots,
273
319
  writableRoots: writable,
274
320
  deniedRoots,
275
- authority: this.registryAuthority,
321
+ authority,
276
322
  ...protectedHome === void 0 ? {} : { protectedHome }
277
323
  };
278
324
  }
@@ -298,6 +344,16 @@ var MultiRootScopeService = class extends Service {
298
344
  * @returns the canonical additional roots; empty when none are registered.
299
345
  */
300
346
  scopeOf(primaryRoot) {
347
+ if (this.admissionProof !== void 0) {
348
+ const operation = currentRegistryOperation(this.admissionProof);
349
+ return operation?.scope.primaryRoot === canonicalPath(primaryRoot) ? operation.scope.additionalRoots.filter((root) => {
350
+ try {
351
+ return canonicalPath(root) === root && statSync(root).isDirectory();
352
+ } catch {
353
+ return false;
354
+ }
355
+ }) : [];
356
+ }
301
357
  return this.membersOf(primaryRoot).map((root) => root.path);
302
358
  }
303
359
  /**
@@ -323,7 +379,7 @@ var MultiRootScopeService = class extends Service {
323
379
  * registry cannot prove none does.
324
380
  */
325
381
  hasActiveDenyRoots() {
326
- if (this.registryAuthority === "unavailable") return true;
382
+ if (this.registryAuthority === "unavailable" || !this.hasAdmission()) return true;
327
383
  for (const members of this.rootsByPrimary.values()) if (members.some((root) => root.access === "read-only") || members.length > 0 && this.capped()) return true;
328
384
  return false;
329
385
  }
@@ -356,10 +412,14 @@ var MultiRootScopeService = class extends Service {
356
412
  * (before acquisition, on contention or storage failure, at teardown) and
357
413
  * `available` only once its stored records have been published. Nothing
358
414
  * else may call it, and it never touches the root table.
415
+ * Only transitions notify: a failed refresh may republish `unavailable`,
416
+ * and notifying on that no-op would re-arm Skills clients' refresh loop.
359
417
  * @param available - whether this process holds the registry authority.
360
418
  */
361
419
  setRegistryAuthority(available) {
362
- this.registryAuthority = available ? "available" : "unavailable";
420
+ const next = available ? "available" : "unavailable";
421
+ if (this.registryAuthority === next) return;
422
+ this.registryAuthority = next;
363
423
  for (const listener of this.listeners) listener();
364
424
  }
365
425
  };