@dsh-electron/dsh-plugin-multi-root-workspace 0.1.13 → 0.1.15

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 (84) hide show
  1. package/CHANGELOG.en.md +36 -1
  2. package/CHANGELOG.md +36 -1
  3. package/README.en.md +27 -10
  4. package/README.md +27 -10
  5. package/cordis.patch.yml +5 -1
  6. package/detail/DETAIL.en.md +1 -0
  7. package/detail/DETAIL.md +1 -0
  8. package/detail/preview.svg +1 -0
  9. package/detail/preview.zh.svg +1 -0
  10. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  11. package/lib/auto-review.js +2 -2
  12. package/lib/client.js +584 -73
  13. package/lib/client.js.map +1 -1
  14. package/lib/{command-B9855H7y.js → command-y5tfcAeC.js} +45 -12
  15. package/lib/command.js +1 -1
  16. package/lib/compat-CTJbpCxu.js +267 -0
  17. package/lib/compat.js +1 -1
  18. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  19. package/lib/{compat-Bg2HFVMX.js → dsh-version-DCwF7REk.js} +5 -265
  20. package/lib/{fs-CfzJq2wd.js → fs-Uws0ZpmV.js} +38 -0
  21. package/lib/fs.js +1 -1
  22. package/lib/index.js +11 -9
  23. package/lib/{instructions-KsUwUFE0.js → instructions-Ck9wp4J4.js} +695 -26
  24. package/lib/instructions.js +1 -1
  25. package/lib/llm-message-CepyzEBQ.js +84 -0
  26. package/lib/lsp.js +3 -3
  27. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  28. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  29. package/lib/{native-win32-2waCRkPI.js → native-win32-C5oMtt7f.js} +2 -1
  30. package/lib/ops-compatibility.js +58 -0
  31. package/lib/ops-settings-UN2oOKEp.js +40 -0
  32. package/lib/ops-tools-k4JYaOrM.js +152 -0
  33. package/lib/process-fence.js +100 -24
  34. package/lib/{protocol-CJOvAvOS.js → protocol-CwDnFEyK.js} +54 -8
  35. package/lib/{registry-Bv431TfO.js → registry-DyDNDSJZ.js} +235 -79
  36. package/lib/{registry-lease-win32-DVTq9d4D.js → registry-lease-win32-r4puac9m.js} +2 -1
  37. package/lib/registry.js +2 -2
  38. package/lib/{review-context-NDpKeMCc.js → review-context-B-oHkU6a.js} +24 -7
  39. package/lib/review-context.js +1 -1
  40. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  41. package/lib/{sandbox-C-vCJCp3.js → sandbox-CZElTNPz.js} +1 -1
  42. package/lib/sandbox.js +1 -1
  43. package/lib/scope.js +72 -9
  44. package/lib/skills-utyuvmwr.js +104 -0
  45. package/lib/skills.js +4 -105
  46. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  47. package/lib/types/authority/protocol.d.ts +5 -1
  48. package/lib/types/client/RootSwitcher.d.ts +2 -0
  49. package/lib/types/client/auto-review-settings.d.ts +18 -0
  50. package/lib/types/client/files-tree/FilesTree.d.ts +4 -1
  51. package/lib/types/client/files-tree/locales.d.ts +1 -1
  52. package/lib/types/client/locales.d.ts +40 -0
  53. package/lib/types/client/open-in-app/locales.d.ts +1 -1
  54. package/lib/types/client/ops-settings.d.ts +6 -0
  55. package/lib/types/client/skills-settings.d.ts +13 -0
  56. package/lib/types/client/styles.d.ts +1 -1
  57. package/lib/types/command.d.ts +3 -3
  58. package/lib/types/compat/auto-review-client.d.ts +58 -0
  59. package/lib/types/compat/connection-rpc.d.ts +12 -0
  60. package/lib/types/compat/dsh-version.d.ts +1 -1
  61. package/lib/types/compat/files-tree.d.ts +36 -0
  62. package/lib/types/compat/fs-patch.d.ts +6 -0
  63. package/lib/types/compat/instruction-activity.d.ts +32 -1
  64. package/lib/types/compat/koffi.d.ts +2 -0
  65. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  66. package/lib/types/compat/ops-settings.d.ts +11 -0
  67. package/lib/types/compat/ops-tools.d.ts +25 -0
  68. package/lib/types/compat/skills-client.d.ts +1 -0
  69. package/lib/types/compat/tool-context.d.ts +4 -0
  70. package/lib/types/compat.d.ts +2 -2
  71. package/lib/types/fs.d.ts +3 -1
  72. package/lib/types/instructions.d.ts +6 -1
  73. package/lib/types/ops-compatibility.d.ts +34 -0
  74. package/lib/types/registry.d.ts +67 -8
  75. package/lib/types/review-context.d.ts +2 -0
  76. package/lib/types/roots.d.ts +114 -20
  77. package/lib/types/scope.d.ts +28 -2
  78. package/lib/workspace-files.js +3 -3
  79. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  80. package/locale/en.json +7 -0
  81. package/locale/zh.json +7 -0
  82. package/package.json +93 -82
  83. package/src/auto-review/README.en.md +1 -1
  84. package/src/auto-review/README.md +1 -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-Ck9wp4J4.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-B-oHkU6a.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";
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-CZElTNPz.js";
3
3
  export { MultiRootSandboxProvider, MultiRootSandboxProvider as default };
package/lib/scope.js CHANGED
@@ -1,5 +1,9 @@
1
+ import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
1
2
  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";
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";
@@ -156,6 +160,7 @@ var MultiRootScopeService = class extends Service {
156
160
  listeners = /* @__PURE__ */ new Set();
157
161
  /** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
158
162
  batchDepth = 0;
163
+ opsProtection = createOpsProtectionState();
159
164
  /** True when a batched write coalesced its notification into the flush. */
160
165
  batchDirty = false;
161
166
  admissionProof;
@@ -179,6 +184,13 @@ var MultiRootScopeService = class extends Service {
179
184
  * treated as evidence.
180
185
  */
181
186
  dshHome;
187
+ /**
188
+ * Whether the row carried a `dshHome` value at all — even an invalid one.
189
+ * The registry's protected-boundary set must tell "no home configured"
190
+ * (absent: nothing to protect) from "a home was supplied and rejected"
191
+ * (invalid: the boundary cannot be proven).
192
+ */
193
+ dshHomeSupplied;
182
194
  /** Observe registry publications; consumers still resolve authority from scopeOf(). */
183
195
  subscribe(listener) {
184
196
  this.listeners.add(listener);
@@ -228,6 +240,20 @@ var MultiRootScopeService = class extends Service {
228
240
  get toolAccessInvalid() {
229
241
  return this.capsInvalid;
230
242
  }
243
+ /**
244
+ * The validated `dshHome` row config, classified for the registry's
245
+ * protected-boundary set: `absent` when the row carried no value (nothing
246
+ * to protect), `invalid` when it carried a value the constructor rejected
247
+ * (the boundary cannot be proven), `configured` with the accepted absolute
248
+ * path. Read-only — this changes no scope or `protectedHome` semantics.
249
+ */
250
+ get dshHomeConfig() {
251
+ if (this.dshHome !== void 0) return {
252
+ kind: "configured",
253
+ path: this.dshHome
254
+ };
255
+ return this.dshHomeSupplied ? { kind: "invalid" } : { kind: "absent" };
256
+ }
231
257
  constructor(ctx, config = {}) {
232
258
  super(ctx, "multiRootScope");
233
259
  const toolAccess = config.toolAccess;
@@ -243,10 +269,44 @@ var MultiRootScopeService = class extends Service {
243
269
  this.capsInvalid = false;
244
270
  }
245
271
  const dshHome = config.dshHome;
272
+ this.dshHomeSupplied = dshHome !== void 0;
246
273
  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
274
  else this.dshHome = dshHome;
248
275
  aroundTool(ctx, (exec, next) => {
249
- return toolCapStore.run({ capped: this.capForTool(exec.name) }, next);
276
+ return this.withToolCap(exec.name, async () => {
277
+ const denial = opsDispatchBarrier(ctx, this, exec);
278
+ if (denial !== void 0) throw new Error(denial);
279
+ return await next();
280
+ });
281
+ });
282
+ ctx.inject([
283
+ "tools",
284
+ "sandboxPolicy",
285
+ "multiRootCompat",
286
+ "multiRootProviders"
287
+ ], (scoped) => {
288
+ if (typeof scoped.tools.guard !== "function") return;
289
+ scoped.effect(() => scoped.tools.guard((exec) => opsDispatchBarrier(ctx, this, exec)));
290
+ });
291
+ ctx.inject([
292
+ "subprocess",
293
+ "multiRootCompat",
294
+ "multiRootProviders"
295
+ ], (scoped) => {
296
+ scoped.effect(() => {
297
+ let active = true;
298
+ const restore = wrapMethod(scoped.get("subprocess"), "spawn", (original) => {
299
+ const scope = this;
300
+ return function(spec) {
301
+ if (active && opsCompatibilityToken(scoped) !== void 0 && scope.hasActiveDenyRoots() && !opsSpawnPermitted(scope.opsProtection) && !(scope.admissionRequired && scope.admissionPathsValid() && scope.permitsProfile(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");
302
+ return original.call(this, spec);
303
+ };
304
+ });
305
+ return () => {
306
+ active = false;
307
+ restore();
308
+ };
309
+ });
250
310
  });
251
311
  ctx.inject(["systemPrompt", "sandboxPolicy"], (scope) => {
252
312
  scope.systemPrompt.context({
@@ -327,16 +387,19 @@ var MultiRootScopeService = class extends Service {
327
387
  previewForTool(policy, toolName) {
328
388
  return this.project(policy, this.capForTool(toolName));
329
389
  }
390
+ withToolCap(name, fn) {
391
+ return toolCapStore.run({ capped: this.capForTool(name) }, fn);
392
+ }
330
393
  /** Execution and review use exactly the same parent-narrowing rule. */
331
394
  capForTool(toolName) {
332
395
  return this.capsInvalid || toolCapStore.getStore()?.capped === true || this.caps[toolName] === "read-only";
333
396
  }
334
397
  project(policy, capped, recorded, authority = this.registryAuthority) {
335
398
  const primaryRoot = canonicalPath(policy.workspaceRoot);
336
- const registered = recorded ?? this.rootsByPrimary.get(primaryRoot) ?? [];
399
+ const registered = recorded ?? this.rootsByPrimary.get(primaryKey(policy.workspaceRoot)) ?? [];
337
400
  const roots = authority === "unavailable" ? [] : sanitizeMembers(primaryRoot, registered);
338
401
  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);
402
+ const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
340
403
  const deniedRoots = [...new Set(denied)];
341
404
  let protectedHome;
342
405
  if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || authority === "unavailable")) {
@@ -419,10 +482,9 @@ var MultiRootScopeService = class extends Service {
419
482
  }
420
483
  /** The sanitized members of one primary root, in registry order. */
421
484
  membersOf(primaryRoot) {
422
- const key = canonicalPath(primaryRoot);
423
- const registered = this.rootsByPrimary.get(key);
485
+ const registered = this.rootsByPrimary.get(primaryKey(primaryRoot));
424
486
  if (registered === void 0) return [];
425
- return sanitizeMembers(key, registered);
487
+ return sanitizeMembers(canonicalPath(primaryRoot), registered);
426
488
  }
427
489
  /**
428
490
  * Replace the roots registered for one primary root.
@@ -430,11 +492,12 @@ var MultiRootScopeService = class extends Service {
430
492
  * M1 uses this from tests and smoke scripts only; M3 feeds it from plugin
431
493
  * storage. It is a plain setter so the transition swaps the data source
432
494
  * without touching any provider.
433
- * @param primaryRoot - canonical primary root to register roots under.
495
+ * @param primaryRoot - primary root the roots belong to; the Registry key
496
+ * is its upstream spelling via {@link primaryKey}, never re-resolved.
434
497
  * @param roots - the additional roots, in display order.
435
498
  */
436
499
  setAdditionalRoots(primaryRoot, roots) {
437
- const key = canonicalPath(primaryRoot);
500
+ const key = primaryKey(primaryRoot);
438
501
  if (roots.length === 0) this.rootsByPrimary.delete(key);
439
502
  else this.rootsByPrimary.set(key, [...roots]);
440
503
  this.notifySubscribers();
@@ -0,0 +1,104 @@
1
+ import { n as isPackageInstalled } from "./agent-instructions-BXuKhOzr.js";
2
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/usingCtx.js
3
+ function _usingCtx() {
4
+ var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
5
+ var n = Error();
6
+ return n.name = "SuppressedError", n.error = r, n.suppressed = e, n;
7
+ }, e = {}, n = [];
8
+ function using(r, e) {
9
+ if (null != e) {
10
+ if (Object(e) !== e) throw new TypeError("using declarations can only be used with objects, functions, null, or undefined.");
11
+ if (r) var o = e[Symbol.asyncDispose || Symbol["for"]("Symbol.asyncDispose")];
12
+ if (void 0 === o && (o = e[Symbol.dispose || Symbol["for"]("Symbol.dispose")], r)) var t = o;
13
+ if ("function" != typeof o) throw new TypeError("Object is not disposable.");
14
+ t && (o = function o() {
15
+ try {
16
+ t.call(e);
17
+ } catch (r) {
18
+ return Promise.reject(r);
19
+ }
20
+ }), n.push({
21
+ v: e,
22
+ d: o,
23
+ a: r
24
+ });
25
+ } else r && n.push({
26
+ d: e,
27
+ a: r
28
+ });
29
+ return e;
30
+ }
31
+ return {
32
+ e,
33
+ u: using.bind(null, !1),
34
+ a: using.bind(null, !0),
35
+ d: function d() {
36
+ var o, t = this.e, s = 0;
37
+ function next() {
38
+ for (; o = n.pop();) try {
39
+ if (!o.a && 1 === s) return s = 0, n.push(o), Promise.resolve().then(next);
40
+ if (o.d) {
41
+ var r = o.d.call(o.v);
42
+ if (o.a) return s |= 2, Promise.resolve(r).then(next, err);
43
+ } else s |= 1;
44
+ } catch (r) {
45
+ return err(r);
46
+ }
47
+ if (1 === s) return t !== e ? Promise.reject(t) : Promise.resolve();
48
+ if (t !== e) throw t;
49
+ }
50
+ function err(n) {
51
+ return t = t !== e ? new r(n, t) : n, next();
52
+ }
53
+ return next();
54
+ }
55
+ };
56
+ }
57
+ //#endregion
58
+ //#region src/compat/skills.ts
59
+ /** Never infer authority from cwd, a preset key, or a lookalike Agent. */
60
+ function skillAgent(ctx, options) {
61
+ const scope = options.scope;
62
+ if (typeof scope !== "object" || scope === null || !("id" in scope) || typeof scope.id !== "string") return void 0;
63
+ const agent = ctx.get("agents")?.get(scope.id);
64
+ return agent === scope ? agent : void 0;
65
+ }
66
+ function skillOrigin(ctx, options) {
67
+ const cwd = skillAgent(ctx, options)?.session.header.cwd;
68
+ return typeof cwd === "string" && cwd !== "" ? cwd : void 0;
69
+ }
70
+ /** Absence is optional; a present but broken module must fail visibly. */
71
+ async function filesystemSkillsApi() {
72
+ if (!isPackageInstalled("@deepseek-ai/dsh-skill-filesystem")) return void 0;
73
+ const api = await import("@deepseek-ai/dsh-skill-filesystem");
74
+ if (typeof api.FileSystemSkillProvider !== "function") throw new Error("multi-root skills: FileSystemSkillProvider is unavailable");
75
+ return api;
76
+ }
77
+ function volatileField(schema) {
78
+ const method = schema.volatile;
79
+ return typeof method === "function" ? method.call(schema) : schema;
80
+ }
81
+ function configValue(value) {
82
+ if (typeof value === "object" && value !== null && Symbol.for("cosmokit.volatile.write") in value) {
83
+ const get = value.get;
84
+ if (typeof get === "function") return get.call(value);
85
+ }
86
+ return value;
87
+ }
88
+ /** Cold Browser Catalog supplies a Session header, never a fabricated Agent. */
89
+ async function observedSkillOrigin(ctx, sessionId) {
90
+ try {
91
+ var _usingCtx$1 = _usingCtx();
92
+ const query = ctx.get("sessionQuery");
93
+ if (query === void 0) throw new Error("multi-root skills: SessionQuery is unavailable");
94
+ const cwd = _usingCtx$1.u(await query.observeSession(sessionId)).header.cwd;
95
+ if (typeof cwd !== "string" || cwd === "") throw new Error("multi-root skills: Session has no origin");
96
+ return cwd;
97
+ } catch (_) {
98
+ _usingCtx$1.e = _;
99
+ } finally {
100
+ _usingCtx$1.d();
101
+ }
102
+ }
103
+ //#endregion
104
+ export { skillOrigin as a, skillAgent as i, filesystemSkillsApi as n, volatileField as o, observedSkillOrigin as r, configValue as t };