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

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 (73) hide show
  1. package/CHANGELOG.en.md +32 -1
  2. package/CHANGELOG.md +32 -1
  3. package/README.en.md +25 -8
  4. package/README.md +25 -8
  5. package/cordis.patch.yml +5 -1
  6. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  7. package/lib/auto-review.js +2 -2
  8. package/lib/client.js +969 -100
  9. package/lib/client.js.map +1 -1
  10. package/lib/{command-Dcz6nULo.js → command-DaVJ-mK3.js} +76 -11
  11. package/lib/command.js +1 -1
  12. package/lib/compat-CTJbpCxu.js +267 -0
  13. package/lib/compat.js +1 -1
  14. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  15. package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
  16. package/lib/index.js +10 -8
  17. package/lib/{instructions-KsUwUFE0.js → instructions-Ca80SkZq.js} +696 -27
  18. package/lib/instructions.js +1 -1
  19. package/lib/llm-message-CepyzEBQ.js +84 -0
  20. package/lib/lsp.js +4 -4
  21. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  22. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  23. package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
  24. package/lib/ops-compatibility.js +58 -0
  25. package/lib/ops-settings-UN2oOKEp.js +40 -0
  26. package/lib/ops-tools-k4JYaOrM.js +152 -0
  27. package/lib/process-fence.js +193 -34
  28. package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
  29. package/lib/{registry-DR_sMuvS.js → registry-BibiukWk.js} +236 -80
  30. package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
  31. package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
  32. package/lib/registry.js +2 -2
  33. package/lib/{review-context-NDpKeMCc.js → review-context-OxO-fMWR.js} +24 -7
  34. package/lib/review-context.js +1 -1
  35. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  36. package/lib/{sandbox-C-vCJCp3.js → sandbox-aobdyQGi.js} +7 -4
  37. package/lib/sandbox.js +1 -1
  38. package/lib/scope.js +162 -12
  39. package/lib/skills-utyuvmwr.js +104 -0
  40. package/lib/skills.js +5 -106
  41. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  42. package/lib/types/authority/protocol.d.ts +5 -1
  43. package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
  44. package/lib/types/client/files-tree/adapter.d.ts +69 -16
  45. package/lib/types/client/files-tree/face.d.ts +31 -2
  46. package/lib/types/client/files-tree/locate.d.ts +74 -0
  47. package/lib/types/client/files-tree/navigator.d.ts +98 -0
  48. package/lib/types/client/index.d.ts +7 -0
  49. package/lib/types/client/locales.d.ts +30 -0
  50. package/lib/types/client/ops-settings.d.ts +6 -0
  51. package/lib/types/client/skills-settings.d.ts +3 -2
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +3 -3
  54. package/lib/types/compat/files-tree.d.ts +8 -1
  55. package/lib/types/compat/forge-entrypoints.d.ts +44 -0
  56. package/lib/types/compat/instruction-activity.d.ts +32 -1
  57. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  58. package/lib/types/compat/ops-settings.d.ts +11 -0
  59. package/lib/types/compat/ops-tools.d.ts +25 -0
  60. package/lib/types/compat/session-existence.d.ts +41 -0
  61. package/lib/types/compat/skills-client.d.ts +1 -0
  62. package/lib/types/compat/tool-context.d.ts +4 -0
  63. package/lib/types/compat.d.ts +2 -2
  64. package/lib/types/instructions.d.ts +6 -1
  65. package/lib/types/ops-compatibility.d.ts +34 -0
  66. package/lib/types/registry-operation.d.ts +7 -0
  67. package/lib/types/registry.d.ts +67 -8
  68. package/lib/types/review-context.d.ts +2 -0
  69. package/lib/types/roots.d.ts +116 -20
  70. package/lib/types/scope.d.ts +95 -3
  71. package/lib/workspace-files.js +4 -4
  72. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  73. package/package.json +5 -1
@@ -1,6 +1,7 @@
1
1
  import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
- import { s as touchedPathOfToolCall } from "./instructions-KsUwUFE0.js";
3
- import { l as isCanonicallyUnder } from "./roots-Ca_W1S9n.js";
2
+ import { c as stableOpsJson, l as touchedPathOfToolCall, s as pendingInstructionTargets } from "./instructions-Ca80SkZq.js";
3
+ import { r as opsCompatibilityToken } from "./ops-settings-UN2oOKEp.js";
4
+ import { l as isCanonicallyUnder } from "./roots-CmaWxRVy.js";
4
5
  import { createHash } from "node:crypto";
5
6
  import { resolve } from "node:path";
6
7
  import { Service } from "@deepseek-ai/cordis";
@@ -50,7 +51,7 @@ var MultiRootReviewContext = class extends Service {
50
51
  const policy = this.ctx.sandboxPolicy.resolve({ session: agent.session });
51
52
  const primary = canonicalPath(policy.workspaceRoot);
52
53
  if (agent.session.header.cwd !== void 0 && canonicalPath(agent.session.header.cwd) !== primary) throw new Error("review-context: inconsistent session origin");
53
- await this.ctx.multiRootRegistry.refresh(primary);
54
+ await this.ctx.multiRootRegistry.refresh(policy.workspaceRoot);
54
55
  signal.throwIfAborted();
55
56
  const cwd = effectiveDirectory(this.ctx, agent.session);
56
57
  if (cwd === void 0) throw new Error("review-context: missing effective cwd");
@@ -73,10 +74,20 @@ var MultiRootReviewContext = class extends Service {
73
74
  primaryRootWritable: policy.mode !== "read-only" && !scope.deniedRoots.some((deny) => isCanonicallyUnder(scope.primaryRoot, deny)),
74
75
  writeBoundary: scope.authority === "unavailable" && policy.mode !== "read-only" ? "primary-root-only; no shared temp grants; explicit denies take precedence" : policy.mode === "danger-full-access" ? "ambient host writes minus explicit deny boundaries; membership is not an access allowlist" : policy.mode === "workspace-write" ? "primary root, effective writable additional roots and upstream host temp grants; explicit denies take precedence" : "only effective writable additional roots; explicit denies take precedence"
75
76
  };
76
- const path = touchedPathOfToolCall(exec.name, JSON.stringify(exec.arguments));
77
- const target = path === void 0 ? void 0 : this.ctx.fs.processPath(await this.ctx.fs.resolve(resolve(cwd, path), { signal }));
78
- const constraints = await this.ctx.multiRootInstructions.snapshot(agent, scope.authority === "available" ? scope.additionalRoots : [], target, signal);
77
+ const official = touchedPathOfToolCall(exec.name, JSON.stringify(exec.arguments));
78
+ const modeToken = opsCompatibilityToken(this.ctx);
79
+ const candidates = modeToken === void 0 ? [] : pendingInstructionTargets(exec.name, exec.arguments);
80
+ const targets = [];
81
+ for (const path of official === void 0 ? candidates : [official]) targets.push(this.ctx.fs.processPath(await this.ctx.fs.resolve(resolve(cwd, path), { signal })));
82
+ const pending = exec.callId === void 0 || modeToken === void 0 ? void 0 : {
83
+ callId: exec.callId,
84
+ name: exec.name,
85
+ arguments: exec.arguments,
86
+ parent: exec.parent
87
+ };
88
+ const constraints = await this.ctx.multiRootInstructions.snapshot(agent, scope.authority === "available" ? scope.additionalRoots : [], targets, signal, pending);
79
89
  signal.throwIfAborted();
90
+ if (opsCompatibilityToken(this.ctx) !== modeToken) throw new Error("review-context: OPS compatibility changed during the build; retry");
80
91
  const payload = {
81
92
  schemaVersion: 1,
82
93
  environment,
@@ -87,7 +98,13 @@ var MultiRootReviewContext = class extends Service {
87
98
  if (Buffer.byteLength(JSON.stringify(payload), "utf8") > 65536) throw new Error("review-context: context budget exceeded");
88
99
  return {
89
100
  ...payload,
90
- fingerprint: createHash("sha256").update(JSON.stringify([payload, nativeInstructionFingerprint(agent.session)])).digest("hex")
101
+ fingerprint: createHash("sha256").update(JSON.stringify(modeToken === void 0 ? [payload, nativeInstructionFingerprint(agent.session)] : [
102
+ payload,
103
+ nativeInstructionFingerprint(agent.session),
104
+ exec.name,
105
+ stableOpsJson(exec.arguments),
106
+ modeToken.id
107
+ ])).digest("hex")
91
108
  };
92
109
  }
93
110
  async assertCurrent(exec, snapshot, signal) {
@@ -1,2 +1,2 @@
1
- import { t as MultiRootReviewContext } from "./review-context-NDpKeMCc.js";
1
+ import { t as MultiRootReviewContext } from "./review-context-OxO-fMWR.js";
2
2
  export { MultiRootReviewContext, MultiRootReviewContext as default };
@@ -1,6 +1,6 @@
1
1
  import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-B5R2K3mM.js";
2
2
  import { statSync } from "node:fs";
3
- import { isAbsolute, join, sep } from "node:path";
3
+ import { isAbsolute, join, resolve, sep } from "node:path";
4
4
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
5
5
  import { homedir } from "node:os";
6
6
  //#region src/roots.ts
@@ -20,15 +20,21 @@ import { homedir } from "node:os";
20
20
  * misconfiguration, not a path to guess at.
21
21
  * 2. `missing` — the directory does not exist.
22
22
  * 3. `not-a-directory` — it exists but is not a directory.
23
- * 4. `equals-primary` — the canonical candidate IS the session's workspace
23
+ * 4. `protected-path` — the canonical candidate equals, lies inside, or
24
+ * contains a protected DSH directory (the DSH home, the registry's
25
+ * physical storage medium, or the lease directory, supplied through
26
+ * {@link RootCandidateContext.protectedPaths}): those hold the store,
27
+ * the lease, profiles and user credentials, so no access level may
28
+ * register them. Applies to every mode; no remote exemption exists.
29
+ * 5. `equals-primary` — the canonical candidate IS the session's workspace
24
30
  * root, which is already granted by the upstream policy.
25
- * 5. `primary-overlap` — the candidate lies under the workspace root, or
31
+ * 6. `primary-overlap` — the candidate lies under the workspace root, or
26
32
  * contains it. The union grants nothing new, and the displayed list would
27
33
  * stop matching the enforced root set.
28
- * 6. `duplicate` — another registered root canonicalizes to the same path.
29
- * 7. `nested` — the candidate lies under a registered root, or contains one.
30
- * Same reasoning as rule 5, applied to the additional roots.
31
- * 8. `unsupported-path` — on Windows, a network or device path (`\\server`,
34
+ * 7. `duplicate` — another registered root canonicalizes to the same path.
35
+ * 8. `nested` — the candidate lies under a registered root, or contains one.
36
+ * Same reasoning as rule 6, applied to the additional roots.
37
+ * 9. `unsupported-path` — on Windows, a network or device path (`\\server`,
32
38
  * `\\?\…`, `\\.\…`): the ACL confinement cannot be shown to cover it, so it
33
39
  * is rejected lexically before any filesystem touch.
34
40
  *
@@ -88,6 +94,22 @@ const CASE_SENSITIVE = process.platform !== "win32";
88
94
  function comparable(path) {
89
95
  return CASE_SENSITIVE ? path : path.toLowerCase();
90
96
  }
97
+ /**
98
+ * The Registry/scope lookup key of one primary root: the UPSTREAM spelling,
99
+ * normalized lexically by `path.resolve` and case-folded on Windows — never
100
+ * re-resolved. Upstream `dsh-sandbox-policy` sets `policy.workspaceRoot` to
101
+ * `session.header.cwd` verbatim, so the spelling IS the identity: a symlinked
102
+ * workspace and its realpath target are different sessions and different key
103
+ * spaces. Re-resolving here would let a replaced workspace directory
104
+ * silently re-key the record set onto another identity — the exact failure
105
+ * the redirect rules exist to prevent. This is the ONLY function allowed to
106
+ * turn a primary root into a Registry key; path geometry keeps using
107
+ * {@link canonicalRoot}.
108
+ */
109
+ function primaryKey(path, platform = process.platform) {
110
+ const resolved = resolve(path);
111
+ return platform === "win32" ? resolved.toLowerCase() : resolved;
112
+ }
91
113
  /** Whether two canonical paths name the same directory on this platform. */
92
114
  function samePath(left, right) {
93
115
  return comparable(left) === comparable(right);
@@ -131,6 +153,11 @@ function validateRootCandidate(raw, context) {
131
153
  const canonical = canonicalRoot(expanded);
132
154
  if (platform === "win32" && isWindowsNetworkOrDevicePath(canonical)) throw unsupportedPath(expanded);
133
155
  assertDirectory(canonical, expanded);
156
+ const protectedConflict = protectedOverlap(canonical, context.protectedPaths ?? []);
157
+ if (protectedConflict !== void 0) throw new RootValidationError("protected-path", `"${canonical}" overlaps the protected DSH directory "${protectedConflict.path}" (${protectedConflict.kind}) and cannot be registered`, {
158
+ conflict: protectedConflict.path,
159
+ reference: canonical
160
+ });
134
161
  const primaryRoot = canonicalRoot(context.primaryRoot);
135
162
  if (samePath(canonical, primaryRoot)) throw new RootValidationError("equals-primary", `"${canonical}" is this session's workspace root and is already writable`, {
136
163
  conflict: primaryRoot,
@@ -160,6 +187,18 @@ function validateRootCandidate(raw, context) {
160
187
  function unsupportedPath(asTyped) {
161
188
  return new RootValidationError("unsupported-path", `"${asTyped}" is a network or device path, which Windows ACL confinement cannot cover`, { reference: asTyped });
162
189
  }
190
+ /**
191
+ * The first protected directory `candidate` overlaps — equals, lies inside,
192
+ * or contains. Both sides must already be canonical (or absolute boundary
193
+ * spellings): the comparison is the same lexical three-way test the other
194
+ * boundary rules use, so a symlink alias can never route around it.
195
+ * @param candidate - the canonical candidate or recorded grant to test.
196
+ * @param protectedPaths - the protected DSH directories.
197
+ * @returns the overlapping protected path, or `undefined`.
198
+ */
199
+ function protectedOverlap(candidate, protectedPaths) {
200
+ return protectedPaths.find((boundary) => samePath(candidate, boundary.path) || isCanonicallyUnder(candidate, boundary.path) || isCanonicallyUnder(boundary.path, candidate));
201
+ }
163
202
  /** Assert that a canonical path is an existing directory. */
164
203
  function assertDirectory(canonical, asTyped) {
165
204
  let stats;
@@ -181,12 +220,15 @@ function assertDirectory(canonical, asTyped) {
181
220
  * @param primaryRoot - the session workspace root the records belong to.
182
221
  * @param records - the stored records, in registry order.
183
222
  * @param platform - the platform the rules run for; defaults to `process.platform`.
223
+ * @param protectedPaths - the protected DSH directories a stored grant may not
224
+ * overlap; a record that does is reported `invalid` with
225
+ * `invalidReason: 'protected-path'` — withheld, never deleted, never re-granted.
184
226
  * @returns one status per record, in the same order.
185
227
  */
186
- function classifyStoredRoots(primaryRoot, records, platform = process.platform) {
228
+ function classifyStoredRoots(primaryRoot, records, platform = process.platform, protectedPaths = []) {
187
229
  const primary = canonicalRoot(primaryRoot);
188
230
  const duplicated = duplicatedIds(records);
189
- return markMembershipConflicts(records.map((record, index) => classifyStoredRoot(record, primary, duplicated.has(record.id), index, platform)));
231
+ return markMembershipConflicts(records.map((record, index) => classifyStoredRoot(record, primary, duplicated.has(record.id), index, platform, protectedPaths)));
190
232
  }
191
233
  /**
192
234
  * Pass two of {@link classifyStoredRoots}: every pair of structurally sound
@@ -204,7 +246,7 @@ function classifyStoredRoots(primaryRoot, records, platform = process.platform)
204
246
  * when the pair found first was already doomed.
205
247
  */
206
248
  function markMembershipConflicts(statuses) {
207
- const boundaries = statuses.map((status) => status.state === "invalid" ? void 0 : singleGrantOf(status));
249
+ const boundaries = statuses.map((status) => status.state !== "invalid" || status.invalidReason === "protected-path" || status.invalidReason === "primary-overlap" ? singleGrantOf(status) : void 0);
208
250
  const conflicts = /* @__PURE__ */ new Map();
209
251
  for (let index = 0; index < boundaries.length; index += 1) {
210
252
  const boundary = boundaries[index];
@@ -221,7 +263,7 @@ function markMembershipConflicts(statuses) {
221
263
  if (conflicts.size === 0) return [...statuses];
222
264
  return statuses.map((status, index) => {
223
265
  const others = conflicts.get(index);
224
- if (others === void 0) return status;
266
+ if (others === void 0 || status.invalidReason === "protected-path" || status.invalidReason === "primary-overlap") return status;
225
267
  const boundary = boundaries[index];
226
268
  const partners = others.map((other) => `"${other}"`).join(", ");
227
269
  return {
@@ -271,7 +313,7 @@ function duplicatedIds(records) {
271
313
  * boundary — the directory the operator was granted — not what `path`
272
314
  * resolves to today.
273
315
  */
274
- function classifyStoredRoot(record, primary, duplicatedId, position, platform) {
316
+ function classifyStoredRoot(record, primary, duplicatedId, position, platform, protectedPaths) {
275
317
  const invalid = (detail) => ({
276
318
  ...record,
277
319
  state: "invalid",
@@ -291,8 +333,21 @@ function classifyStoredRoot(record, primary, duplicatedId, position, platform) {
291
333
  if (typeof recorded !== "string" || recorded === "") return invalid("the record does not say which directory it was granted for");
292
334
  if (!isAbsolute(recorded)) return invalid(`the recorded grant "${recorded}" is not an absolute path`);
293
335
  if (platform === "win32" && (isWindowsNetworkOrDevicePath(record.path) || isWindowsNetworkOrDevicePath(recorded))) return invalid(`"${record.path}" is a network or device path, which Windows ACL confinement cannot cover`);
294
- if (samePath(recorded, primary)) return invalid(`"${recorded}" is this session's workspace root`);
295
- if (isCanonicallyUnder(recorded, primary) || isCanonicallyUnder(primary, recorded)) return invalid(`"${recorded}" overlaps this session's workspace root`);
336
+ const primaryOverlap = (detail) => ({
337
+ ...record,
338
+ state: "invalid",
339
+ invalidReason: "primary-overlap",
340
+ detail: `${detail}; any recorded read-only restriction remains in force`
341
+ });
342
+ if (samePath(recorded, primary)) return primaryOverlap(`"${recorded}" is this session's workspace root`);
343
+ if (isCanonicallyUnder(recorded, primary) || isCanonicallyUnder(primary, recorded)) return primaryOverlap(`"${recorded}" overlaps this session's workspace root`);
344
+ const protectedConflict = protectedOverlap(recorded, protectedPaths);
345
+ if (protectedConflict !== void 0) return {
346
+ ...record,
347
+ state: "invalid",
348
+ invalidReason: "protected-path",
349
+ detail: protectedGrantDetail(recorded, protectedConflict)
350
+ };
296
351
  const canonical = canonicalRoot(record.path);
297
352
  if (!samePath(canonical, recorded)) return {
298
353
  ...record,
@@ -325,23 +380,58 @@ function classifyStoredRoot(record, primary, duplicatedId, position, platform) {
325
380
  function availableRoots(statuses) {
326
381
  return statuses.filter((status) => status.state === "available").map((status) => status.path);
327
382
  }
383
+ /** The detail text the protected-path verdict carries, shared by both marking paths. */
384
+ function protectedGrantDetail(grant, conflict) {
385
+ return `the recorded grant "${grant}" overlaps the protected DSH directory "${conflict.path}" (${conflict.kind}); it stays registered and withheld, and any recorded read-only restriction remains in force`;
386
+ }
387
+ /**
388
+ * Apply the protected-path verdict to an already-classified list: a status
389
+ * whose recorded grant overlaps a protected directory becomes `invalid` with
390
+ * `invalidReason: 'protected-path'` — the same verdict the pass-one stored
391
+ * check produces, here applied by a consumer that classified elsewhere (a
392
+ * shared-registry follower re-judging an owner snapshot against ITS OWN
393
+ * protected set, which may differ from the owner's). Statuses already
394
+ * `invalid` keep their own verdict — a structural failure is never healed
395
+ * into a trustworthy boundary.
396
+ * @param statuses - the classified statuses to re-mark.
397
+ * @param protectedPaths - the caller's own protected DSH directories.
398
+ * @returns a new list with the verdict applied.
399
+ */
400
+ function markProtectedPaths(statuses, protectedPaths) {
401
+ if (protectedPaths.length === 0) return [...statuses];
402
+ return statuses.map((status) => {
403
+ if (status.state === "invalid") return status;
404
+ const grant = singleGrantOf(status);
405
+ if (grant === void 0) return status;
406
+ const conflict = protectedOverlap(grant, protectedPaths);
407
+ if (conflict === void 0) return status;
408
+ return {
409
+ ...status,
410
+ state: "invalid",
411
+ invalidReason: "protected-path",
412
+ detail: protectedGrantDetail(grant, conflict)
413
+ };
414
+ });
415
+ }
328
416
  /**
329
417
  * The recorded boundary one status still RESERVES: the grant it was
330
418
  * registered for, which a new or revived registration may not overlap merely
331
419
  * because the root is currently unusable.
332
420
  *
333
- * `available`, `missing`, `redirected`, and membership-conflict statuses all
334
- * carry an intact, trustworthy recorded grant; every other `invalid` verdict
335
- * means the grant could not be parsed at all, so it reserves — and denies —
336
- * nothing. This is not an independent authorization table: it derives the
337
- * boundary from the status list {@link classifyStoredRoots} produced, with
338
- * the same grant-extraction semantics, so its answer can never disagree with
421
+ * `available`, `missing`, `redirected`, and the trustworthy-boundary
422
+ * `invalid` verdicts (`membership-conflict`, `protected-path`, and
423
+ * `primary-overlap`) all carry
424
+ * an intact recorded grant; every other `invalid` verdict means the grant
425
+ * could not be parsed at all, so it reserves — and denies — nothing. This is
426
+ * not an independent authorization table: it derives the boundary from the
427
+ * status list {@link classifyStoredRoots} produced, with the same
428
+ * grant-extraction semantics, so its answer can never disagree with
339
429
  * classification.
340
430
  * @param status - one classified registration.
341
431
  * @returns the absolute recorded boundary, or `undefined` when there is none.
342
432
  */
343
433
  function reservedRootBoundary(status) {
344
- if (status.state === "invalid" && status.invalidReason !== "membership-conflict") return void 0;
434
+ if (status.state === "invalid" && status.invalidReason === void 0) return void 0;
345
435
  const boundary = singleGrantOf(status);
346
436
  return boundary !== void 0 && isAbsolute(boundary) ? boundary : void 0;
347
437
  }
@@ -439,4 +529,4 @@ function describeRef(ref) {
439
529
  }
440
530
  }
441
531
  //#endregion
442
- export { classifyStoredRoots as a, indexedStatuses as c, reservedRootBoundary as d, resolveRootIndex as f, canonicalRoot as i, isCanonicallyUnder as l, validateRootCandidate as m, additionalRootId as n, entryRootRef as o, resolveRootRef as p, availableRoots as r, expandRootInput as s, RootValidationError as t, removeStatusAt as u };
532
+ export { samePath as _, classifyStoredRoots as a, indexedStatuses as c, primaryKey as d, protectedOverlap as f, resolveRootRef as g, resolveRootIndex as h, canonicalRoot as i, isCanonicallyUnder as l, reservedRootBoundary as m, additionalRootId as n, entryRootRef as o, removeStatusAt as p, availableRoots as r, expandRootInput as s, RootValidationError as t, markProtectedPaths as u, singleGrantOf as v, validateRootCandidate as y };
@@ -1,5 +1,5 @@
1
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";
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-CdlG3RF9.js";
3
3
  import { brandMultiRootProvider } from "./providers.js";
4
4
  import { SandboxUnavailableError, canonicalPath } from "@deepseek-ai/dsh-sandbox";
5
5
  import { fileURLToPath } from "node:url";
@@ -105,8 +105,9 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
105
105
  */
106
106
  confine(argv, policy, signal) {
107
107
  const scope = this.ctx.multiRootScope.resolve(policy);
108
+ const mint = this.ctx.multiRootScope.captureMint(scope.authority !== "unavailable" ? "ambient" : policy.mode === "read-only" ? "read-only" : policy.mode === "workspace-write" ? "clamped" : "ambient");
108
109
  const remember = (facts) => {
109
- this.ctx.multiRootScope.rememberProfile(facts.argv);
110
+ this.ctx.multiRootScope.rememberProfile(facts.argv, mint);
110
111
  return facts;
111
112
  };
112
113
  if (policy.mode === "workspace-write" && scope.authority === "unavailable") {
@@ -211,6 +212,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
211
212
  workspaceRoot: policy.workspaceRoot,
212
213
  ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
213
214
  };
215
+ const mint = this.ctx.multiRootScope.captureMint("ambient");
214
216
  const probe = super.confine.call(this, ["true"], probePolicy, signal);
215
217
  const restrict = (confined) => {
216
218
  try {
@@ -232,7 +234,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
232
234
  };
233
235
  return widenConfined(() => probe, (facts) => {
234
236
  const result = restrict(facts);
235
- this.ctx.multiRootScope.rememberProfile(result.argv);
237
+ this.ctx.multiRootScope.rememberProfile(result.argv, mint);
236
238
  return result;
237
239
  }, ["true"], probePolicy, signal);
238
240
  }
@@ -253,6 +255,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
253
255
  workspaceRoot: policy.workspaceRoot,
254
256
  ...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
255
257
  };
258
+ const mint = this.ctx.multiRootScope.captureMint("clamped");
256
259
  const probe = super.confine.call(this, ["true"], probePolicy, signal);
257
260
  const clamp = (confined) => {
258
261
  try {
@@ -269,7 +272,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
269
272
  };
270
273
  return widenConfined(() => probe, (facts) => {
271
274
  const result = clamp(facts);
272
- this.ctx.multiRootScope.rememberProfile(result.argv);
275
+ this.ctx.multiRootScope.rememberProfile(result.argv, mint);
273
276
  return result;
274
277
  }, ["true"], probePolicy, signal);
275
278
  }
package/lib/sandbox.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import "./providers.js";
2
- import { t as MultiRootSandboxProvider } from "./sandbox-C-vCJCp3.js";
2
+ import { t as MultiRootSandboxProvider } from "./sandbox-aobdyQGi.js";
3
3
  export { MultiRootSandboxProvider, MultiRootSandboxProvider as default };
package/lib/scope.js CHANGED
@@ -1,5 +1,9 @@
1
- import { i as rememberOperationProfile, r as operationPermitsProfile, t as currentRegistryOperation } from "./registry-operation-C2ZV3Jhw.js";
2
- import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
1
+ import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
2
+ import { a as rememberOperationProfile, i as operationPermitsProfile, r as operationFrameState, t as currentRegistryOperation } from "./registry-operation-CIlmt6_c.js";
3
+ import { t as aroundTool } from "./tool-context-CCeY3mrK.js";
4
+ import { r as opsCompatibilityToken } from "./ops-settings-UN2oOKEp.js";
5
+ import { d as primaryKey } from "./roots-CmaWxRVy.js";
6
+ import { a as createOpsProtectionState, l as opsDispatchBarrier, u as opsSpawnPermitted } from "./ops-tools-k4JYaOrM.js";
3
7
  import { r as effectiveRootAccess } from "./contract-CStSEOcK.js";
4
8
  import { statSync } from "node:fs";
5
9
  import { isAbsolute } from "node:path";
@@ -145,6 +149,8 @@ function renderWorkspaceRootsContext(options) {
145
149
  }
146
150
  /** The tool-dispatch cap carried across async boundaries by the waterfall. */
147
151
  const toolCapStore = new AsyncLocalStorage();
152
+ /** Bounded process-wide ledger of unadmitted mints; a burst cannot grow it without bound. */
153
+ const MAX_MINTED_PROFILES = 256;
148
154
  /**
149
155
  * The `ctx.multiRootScope` service. Providers ask it — never the filesystem —
150
156
  * what the current scope is, which is what keeps the fs fence and every kernel
@@ -156,10 +162,19 @@ var MultiRootScopeService = class extends Service {
156
162
  listeners = /* @__PURE__ */ new Set();
157
163
  /** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
158
164
  batchDepth = 0;
165
+ opsProtection = createOpsProtectionState();
159
166
  /** True when a batched write coalesced its notification into the flush. */
160
167
  batchDirty = false;
161
168
  admissionProof;
162
169
  /**
170
+ * The publication generation the mint ledger is keyed on: bumped when a
171
+ * root-table write or an authority transition changes the authorization
172
+ * basis (a repeat publish of identical content is not a publication), so a
173
+ * minted profile dies with the state it was computed from.
174
+ */
175
+ publicationVersion = 0;
176
+ mintedProfiles = [];
177
+ /**
163
178
  * The registry-authority fact the registry row publishes. Defaults to
164
179
  * `available`: a composition without a registry row can never have
165
180
  * registered roots, so nothing is unknown there. The registry sets it false
@@ -179,6 +194,13 @@ var MultiRootScopeService = class extends Service {
179
194
  * treated as evidence.
180
195
  */
181
196
  dshHome;
197
+ /**
198
+ * Whether the row carried a `dshHome` value at all — even an invalid one.
199
+ * The registry's protected-boundary set must tell "no home configured"
200
+ * (absent: nothing to protect) from "a home was supplied and rejected"
201
+ * (invalid: the boundary cannot be proven).
202
+ */
203
+ dshHomeSupplied;
182
204
  /** Observe registry publications; consumers still resolve authority from scopeOf(). */
183
205
  subscribe(listener) {
184
206
  this.listeners.add(listener);
@@ -216,6 +238,11 @@ var MultiRootScopeService = class extends Service {
216
238
  }
217
239
  for (const listener of this.listeners) listener();
218
240
  }
241
+ /** A publication superseded the authorization basis: every outstanding minted profile dies. */
242
+ bumpPublication() {
243
+ this.publicationVersion += 1;
244
+ this.mintedProfiles.length = 0;
245
+ }
219
246
  /**
220
247
  * The sorted names of the tools `toolAccess` caps — read-only for the
221
248
  * prompt contribution. An invalid config is not enumerated (it caps every
@@ -228,6 +255,20 @@ var MultiRootScopeService = class extends Service {
228
255
  get toolAccessInvalid() {
229
256
  return this.capsInvalid;
230
257
  }
258
+ /**
259
+ * The validated `dshHome` row config, classified for the registry's
260
+ * protected-boundary set: `absent` when the row carried no value (nothing
261
+ * to protect), `invalid` when it carried a value the constructor rejected
262
+ * (the boundary cannot be proven), `configured` with the accepted absolute
263
+ * path. Read-only — this changes no scope or `protectedHome` semantics.
264
+ */
265
+ get dshHomeConfig() {
266
+ if (this.dshHome !== void 0) return {
267
+ kind: "configured",
268
+ path: this.dshHome
269
+ };
270
+ return this.dshHomeSupplied ? { kind: "invalid" } : { kind: "absent" };
271
+ }
231
272
  constructor(ctx, config = {}) {
232
273
  super(ctx, "multiRootScope");
233
274
  const toolAccess = config.toolAccess;
@@ -243,10 +284,44 @@ var MultiRootScopeService = class extends Service {
243
284
  this.capsInvalid = false;
244
285
  }
245
286
  const dshHome = config.dshHome;
287
+ this.dshHomeSupplied = dshHome !== void 0;
246
288
  if (dshHome !== void 0 && (typeof dshHome !== "string" || dshHome.trim() === "" || !isAbsolute(dshHome))) ctx.logger.error(`multi-root-workspace: invalid dshHome config (expected an absolute path, got ${JSON.stringify(dshHome)}); the DSH-home deny boundary is disabled`);
247
289
  else this.dshHome = dshHome;
248
290
  aroundTool(ctx, (exec, next) => {
249
- return toolCapStore.run({ capped: this.capForTool(exec.name) }, next);
291
+ return this.withToolCap(exec.name, async () => {
292
+ const denial = opsDispatchBarrier(ctx, this, exec);
293
+ if (denial !== void 0) throw new Error(denial);
294
+ return await next();
295
+ });
296
+ });
297
+ ctx.inject([
298
+ "tools",
299
+ "sandboxPolicy",
300
+ "multiRootCompat",
301
+ "multiRootProviders"
302
+ ], (scoped) => {
303
+ if (typeof scoped.tools.guard !== "function") return;
304
+ scoped.effect(() => scoped.tools.guard((exec) => opsDispatchBarrier(ctx, this, exec)));
305
+ });
306
+ ctx.inject([
307
+ "subprocess",
308
+ "multiRootCompat",
309
+ "multiRootProviders"
310
+ ], (scoped) => {
311
+ scoped.effect(() => {
312
+ let active = true;
313
+ const restore = wrapMethod(scoped.get("subprocess"), "spawn", (original) => {
314
+ const scope = this;
315
+ return function(spec) {
316
+ if (active && opsCompatibilityToken(scoped) !== void 0 && scope.hasActiveDenyRoots() && !opsSpawnPermitted(scope.opsProtection) && !(scope.admissionRequired && scope.admissionPathsValid() && scope.permitsOperationProfile(spec.argv))) throw new Error("multi-root workspace: an unattributed subprocess has no protected execution or live profile admission; refusing launch while OPS compatibility write boundaries apply");
317
+ return original.call(this, spec);
318
+ };
319
+ });
320
+ return () => {
321
+ active = false;
322
+ restore();
323
+ };
324
+ });
250
325
  });
251
326
  ctx.inject(["systemPrompt", "sandboxPolicy"], (scope) => {
252
327
  scope.systemPrompt.context({
@@ -280,12 +355,74 @@ var MultiRootScopeService = class extends Service {
280
355
  requireAdmission(proof) {
281
356
  this.admissionProof = proof;
282
357
  }
283
- rememberProfile(argv) {
284
- if (this.admissionProof !== void 0) rememberOperationProfile(this.admissionProof, argv, this.capped());
358
+ /**
359
+ * The stamp an asynchronously completing mint must carry: the publication
360
+ * generation and tool-cap basis at the mint's synchronous entry, before any
361
+ * provider probe could run. An in-flight mint that crosses a real
362
+ * publication keeps its entry generation, so the ledger refuses it rather
363
+ * than re-stamping stale authorization under the current publication.
364
+ * @param form - the profile form the mint will produce (`clamped` and
365
+ * `read-only` are the only ledger-eligible forms).
366
+ */
367
+ captureMint(form) {
368
+ return {
369
+ version: this.publicationVersion,
370
+ capped: this.capped(),
371
+ form
372
+ };
373
+ }
374
+ /**
375
+ * Record a confined profile this host's provider just minted. Only
376
+ * meaningful under the shared Registry (`admissionProof` set; without it
377
+ * `permitsProfile` never refuses): a live admission binds the profile to
378
+ * the operation and it dies with it; an expired admission never bequeaths
379
+ * one — a mint that outlived its operation is dropped, not demoted into
380
+ * the ledger; only with NO admission frame at all does the profile join
381
+ * the publication-keyed ledger stamped with `mint`, and only when the
382
+ * mint's form is primary-bounded — an `ambient` mint that arrives without
383
+ * a frame is dropped the same way.
384
+ * @param argv - the confined argv just minted.
385
+ * @param mint - the {@link captureMint} stamp taken at the mint's
386
+ * synchronous entry.
387
+ */
388
+ rememberProfile(argv, mint) {
389
+ if (this.admissionProof === void 0) return;
390
+ const frame = operationFrameState(this.admissionProof);
391
+ if (frame === "expired") return;
392
+ if (frame === "active") {
393
+ rememberOperationProfile(this.admissionProof, argv, mint.capped);
394
+ return;
395
+ }
396
+ if (mint.form === "ambient") return;
397
+ this.mintedProfiles.push({
398
+ argv: [...argv],
399
+ capped: mint.capped,
400
+ version: mint.version
401
+ });
402
+ if (this.mintedProfiles.length > MAX_MINTED_PROFILES) this.mintedProfiles.shift();
285
403
  }
404
+ /**
405
+ * Whether a confined argv may launch now: a profile minted inside this live
406
+ * admission, or — for a `confine`→`spawn` composition with no wrapped entry
407
+ * to admit it — a profile this host minted while its authorization
408
+ * publication is still current. Cached argv, argv minted under a superseded
409
+ * publication, and argv minted elsewhere stay refused.
410
+ */
286
411
  permitsProfile(argv) {
412
+ return this.admissionProof === void 0 || this.permitsOperationProfile(argv) || this.permitsMintedProfile(argv);
413
+ }
414
+ /**
415
+ * The strict form: only profiles minted inside this live admission. The OPS
416
+ * execution domain owns its profile exclusively and never honors the
417
+ * unadmitted mint ledger.
418
+ */
419
+ permitsOperationProfile(argv) {
287
420
  return this.admissionProof === void 0 || operationPermitsProfile(this.admissionProof, argv, this.capped());
288
421
  }
422
+ permitsMintedProfile(argv) {
423
+ const capped = this.capped(), version = this.publicationVersion;
424
+ return this.mintedProfiles.some((entry) => entry.version === version && entry.capped === capped && entry.argv.length > 0 && entry.argv.length <= argv.length && entry.argv.every((arg, index) => argv[index] === arg));
425
+ }
289
426
  get admissionRequired() {
290
427
  return this.admissionProof !== void 0;
291
428
  }
@@ -327,16 +464,19 @@ var MultiRootScopeService = class extends Service {
327
464
  previewForTool(policy, toolName) {
328
465
  return this.project(policy, this.capForTool(toolName));
329
466
  }
467
+ withToolCap(name, fn) {
468
+ return toolCapStore.run({ capped: this.capForTool(name) }, fn);
469
+ }
330
470
  /** Execution and review use exactly the same parent-narrowing rule. */
331
471
  capForTool(toolName) {
332
472
  return this.capsInvalid || toolCapStore.getStore()?.capped === true || this.caps[toolName] === "read-only";
333
473
  }
334
474
  project(policy, capped, recorded, authority = this.registryAuthority) {
335
475
  const primaryRoot = canonicalPath(policy.workspaceRoot);
336
- const registered = recorded ?? this.rootsByPrimary.get(primaryRoot) ?? [];
476
+ const registered = recorded ?? this.rootsByPrimary.get(primaryKey(policy.workspaceRoot)) ?? [];
337
477
  const roots = authority === "unavailable" ? [] : sanitizeMembers(primaryRoot, registered);
338
478
  const writable = capped || authority === "unavailable" ? [] : roots.filter((root) => effectivelyWritable(policy.mode, root.access)).map((root) => root.path);
339
- const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && root.recordedPath !== primaryRoot && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
479
+ const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
340
480
  const deniedRoots = [...new Set(denied)];
341
481
  let protectedHome;
342
482
  if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || authority === "unavailable")) {
@@ -419,10 +559,9 @@ var MultiRootScopeService = class extends Service {
419
559
  }
420
560
  /** The sanitized members of one primary root, in registry order. */
421
561
  membersOf(primaryRoot) {
422
- const key = canonicalPath(primaryRoot);
423
- const registered = this.rootsByPrimary.get(key);
562
+ const registered = this.rootsByPrimary.get(primaryKey(primaryRoot));
424
563
  if (registered === void 0) return [];
425
- return sanitizeMembers(key, registered);
564
+ return sanitizeMembers(canonicalPath(primaryRoot), registered);
426
565
  }
427
566
  /**
428
567
  * Replace the roots registered for one primary root.
@@ -430,11 +569,13 @@ var MultiRootScopeService = class extends Service {
430
569
  * M1 uses this from tests and smoke scripts only; M3 feeds it from plugin
431
570
  * storage. It is a plain setter so the transition swaps the data source
432
571
  * without touching any provider.
433
- * @param primaryRoot - canonical primary root to register roots under.
572
+ * @param primaryRoot - primary root the roots belong to; the Registry key
573
+ * is its upstream spelling via {@link primaryKey}, never re-resolved.
434
574
  * @param roots - the additional roots, in display order.
435
575
  */
436
576
  setAdditionalRoots(primaryRoot, roots) {
437
- const key = canonicalPath(primaryRoot);
577
+ const key = primaryKey(primaryRoot);
578
+ if (!sameAdditionalRoots(this.rootsByPrimary.get(key) ?? [], roots)) this.bumpPublication();
438
579
  if (roots.length === 0) this.rootsByPrimary.delete(key);
439
580
  else this.rootsByPrimary.set(key, [...roots]);
440
581
  this.notifySubscribers();
@@ -454,8 +595,17 @@ var MultiRootScopeService = class extends Service {
454
595
  const next = available ? "available" : "unavailable";
455
596
  if (this.registryAuthority === next) return;
456
597
  this.registryAuthority = next;
598
+ this.bumpPublication();
457
599
  this.notifySubscribers();
458
600
  }
459
601
  };
602
+ /** Whether two persisted member lists carry the same authorization basis, in registry order. */
603
+ function sameAdditionalRoots(a, b) {
604
+ if (a.length !== b.length) return false;
605
+ return a.every((root, index) => {
606
+ const next = b[index];
607
+ return next !== void 0 && root.id === next.id && root.path === next.path && root.recordedPath === next.recordedPath && root.access === next.access && root.withheld === next.withheld && root.alias === next.alias;
608
+ });
609
+ }
460
610
  //#endregion
461
611
  export { MULTI_ROOT_CONTEXT_NAME, MultiRootScopeService, MultiRootScopeService as default, REGISTRY_UNAVAILABLE_NOTE, renderWorkspaceRootsContext, sanitizeAdditionalRoots };