@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.en.md +44 -1
  2. package/CHANGELOG.md +44 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +57 -0
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +1 -1
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/contract.d.ts +31 -19
  64. package/lib/types/dialects.d.ts +99 -3
  65. package/lib/types/fs.d.ts +32 -10
  66. package/lib/types/index.d.ts +7 -2
  67. package/lib/types/process-fence.d.ts +54 -0
  68. package/lib/types/profile-isolation.d.ts +178 -0
  69. package/lib/types/providers.d.ts +70 -0
  70. package/lib/types/registry.d.ts +132 -31
  71. package/lib/types/roots.d.ts +100 -13
  72. package/lib/types/sandbox.d.ts +59 -3
  73. package/lib/types/scope.d.ts +200 -14
  74. package/lib/types/windows-broker.d.ts +17 -0
  75. package/lib/types/windows-deny.d.ts +1 -0
  76. package/lib/types/windows-profile.d.ts +10 -13
  77. package/lib/types/windows-runner-contract.d.ts +97 -0
  78. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  79. package/lib/windows-runner.js +1256 -13
  80. package/lib/workspace-files.js +138 -3
  81. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  82. package/package.json +84 -62
  83. package/scripts/windows-broker/install.ps1 +89 -0
  84. package/scripts/windows-broker/rights.cs +58 -0
  85. package/scripts/windows-broker/service.cs +323 -0
  86. package/scripts/windows-broker/verify-install.ps1 +80 -0
  87. package/lib/command-CTdNvwt1.js +0 -791
  88. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  89. package/lib/sandbox-BRXl_FpF.js +0 -132
  90. package/lib/types/client/RootFileTree.d.ts +0 -12
  91. package/lib/types/panel-files.d.ts +0 -3
  92. package/lib/windows-profile-CiFsFYIV.js +0 -216
  93. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -1,6 +1,7 @@
1
+ import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-CEfmZzu0.js";
2
+ import { statSync } from "node:fs";
1
3
  import { isAbsolute, join, sep } from "node:path";
2
4
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
3
- import { statSync } from "node:fs";
4
5
  import { homedir } from "node:os";
5
6
  //#region src/roots.ts
6
7
  /**
@@ -27,10 +28,18 @@ import { homedir } from "node:os";
27
28
  * 6. `duplicate` — another registered root canonicalizes to the same path.
28
29
  * 7. `nested` — the candidate lies under a registered root, or contains one.
29
30
  * Same reasoning as rule 5, applied to the additional roots.
31
+ * 8. `unsupported-path` — on Windows, a network or device path (`\\server`,
32
+ * `\\?\…`, `\\.\…`): the ACL confinement cannot be shown to cover it, so it
33
+ * is rejected lexically before any filesystem touch.
30
34
  *
31
35
  * The stored side of the same rules lives in {@link classifyStoredRoots}, which
32
36
  * must never throw: a registration that stopped being usable is REPORTED, not
33
- * deleted, so the operator can see it and remove it deliberately.
37
+ * deleted, so the operator can see it and remove it deliberately. It runs in
38
+ * two passes — every record's own structural trust and availability first,
39
+ * then overlap between the RECORDED boundaries of all structurally sound
40
+ * records — so the answer never depends on registry order, and a pair of
41
+ * records whose grants collide (`membership-conflict`) keeps its trustworthy
42
+ * negative boundary even while its membership is withheld.
34
43
  *
35
44
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/roots
36
45
  */
@@ -117,7 +126,10 @@ function validateRootCandidate(raw, context) {
117
126
  const expanded = expandRootInput(raw, context.home);
118
127
  if (expanded === "") throw new RootValidationError("not-absolute", "a root path is required");
119
128
  if (!isAbsolute(expanded)) throw new RootValidationError("not-absolute", `"${expanded}" is not an absolute path; give an absolute directory or one starting with "~/"`, { reference: expanded });
129
+ const platform = context.platform ?? process.platform;
130
+ if (platform === "win32" && isWindowsNetworkOrDevicePath(expanded)) throw unsupportedPath(expanded);
120
131
  const canonical = canonicalRoot(expanded);
132
+ if (platform === "win32" && isWindowsNetworkOrDevicePath(canonical)) throw unsupportedPath(expanded);
121
133
  assertDirectory(canonical, expanded);
122
134
  const primaryRoot = canonicalRoot(context.primaryRoot);
123
135
  if (samePath(canonical, primaryRoot)) throw new RootValidationError("equals-primary", `"${canonical}" is this session's workspace root and is already writable`, {
@@ -144,6 +156,10 @@ function validateRootCandidate(raw, context) {
144
156
  }
145
157
  return canonical;
146
158
  }
159
+ /** The `unsupported-path` refusal, shared by the input and canonical checks. */
160
+ function unsupportedPath(asTyped) {
161
+ return new RootValidationError("unsupported-path", `"${asTyped}" is a network or device path, which Windows ACL confinement cannot cover`, { reference: asTyped });
162
+ }
147
163
  /** Assert that a canonical path is an existing directory. */
148
164
  function assertDirectory(canonical, asTyped) {
149
165
  let stats;
@@ -164,19 +180,71 @@ function assertDirectory(canonical, asTyped) {
164
180
  * `invalid` (kept, unusable).
165
181
  * @param primaryRoot - the session workspace root the records belong to.
166
182
  * @param records - the stored records, in registry order.
183
+ * @param platform - the platform the rules run for; defaults to `process.platform`.
167
184
  * @returns one status per record, in the same order.
168
185
  */
169
- function classifyStoredRoots(primaryRoot, records) {
186
+ function classifyStoredRoots(primaryRoot, records, platform = process.platform) {
170
187
  const primary = canonicalRoot(primaryRoot);
171
- const claimed = [];
172
188
  const duplicated = duplicatedIds(records);
173
- const statuses = [];
174
- for (const [index, record] of records.entries()) {
175
- const status = classifyStoredRoot(record, primary, claimed, duplicated.has(record.id), index);
176
- statuses.push(status);
177
- if (status.state === "available") claimed.push(status.path);
189
+ return markMembershipConflicts(records.map((record, index) => classifyStoredRoot(record, primary, duplicated.has(record.id), index, platform)));
190
+ }
191
+ /**
192
+ * Pass two of {@link classifyStoredRoots}: every pair of structurally sound
193
+ * records whose RECORDED boundaries overlap is a membership conflict, and
194
+ * BOTH are unusable — which side "won" must never depend on registry order
195
+ * (a reorder is cosmetic and cannot revoke a deny boundary). Missing and
196
+ * redirected records participate too: withheld roots carry no grant, but
197
+ * their recorded boundaries still describe restrictions the operator has not
198
+ * removed.
199
+ *
200
+ * The comparison is lexical over the recorded grant via
201
+ * {@link isCanonicallyUnder}/{@link samePath} — never the current resolution,
202
+ * which a replaced directory can steer. The conflict set is built from the
203
+ * ORIGINAL first-pass statuses, so a third overlapping record is marked even
204
+ * when the pair found first was already doomed.
205
+ */
206
+ function markMembershipConflicts(statuses) {
207
+ const boundaries = statuses.map((status) => status.state === "invalid" ? void 0 : singleGrantOf(status));
208
+ const conflicts = /* @__PURE__ */ new Map();
209
+ for (let index = 0; index < boundaries.length; index += 1) {
210
+ const boundary = boundaries[index];
211
+ if (boundary === void 0) continue;
212
+ for (let other = index + 1; other < boundaries.length; other += 1) {
213
+ const otherBoundary = boundaries[other];
214
+ if (otherBoundary === void 0) continue;
215
+ if (samePath(boundary, otherBoundary) || isCanonicallyUnder(boundary, otherBoundary) || isCanonicallyUnder(otherBoundary, boundary)) {
216
+ conflicts.set(index, [...conflicts.get(index) ?? [], otherBoundary]);
217
+ conflicts.set(other, [...conflicts.get(other) ?? [], boundary]);
218
+ }
219
+ }
178
220
  }
179
- return statuses;
221
+ if (conflicts.size === 0) return [...statuses];
222
+ return statuses.map((status, index) => {
223
+ const others = conflicts.get(index);
224
+ if (others === void 0) return status;
225
+ const boundary = boundaries[index];
226
+ const partners = others.map((other) => `"${other}"`).join(", ");
227
+ return {
228
+ ...status,
229
+ state: "invalid",
230
+ invalidReason: "membership-conflict",
231
+ detail: `the recorded grant "${boundary}" overlaps the recorded grant ${partners} — neither membership is granted until one registration is removed, and any recorded read-only restriction remains in force`
232
+ };
233
+ });
234
+ }
235
+ /**
236
+ * The single recorded grant a record or status carries — `undefined` unless
237
+ * EXACTLY one grant field holds a non-empty string. Shared by the conflict
238
+ * pass and {@link reservedRootBoundary}; the stored decoder is the source of
239
+ * the access level, so this never cross-checks the field against `access`.
240
+ */
241
+ function singleGrantOf(record) {
242
+ const grants = [
243
+ record.recordedPath,
244
+ record.readOnlyRecordedPath,
245
+ record.readWriteRecordedPath
246
+ ].filter((grant) => typeof grant === "string" && grant !== "");
247
+ return grants.length === 1 ? grants[0] : void 0;
180
248
  }
181
249
  /**
182
250
  * The ids that more than one record claims. Computed for the whole list up
@@ -192,15 +260,18 @@ function duplicatedIds(records) {
192
260
  return duplicated;
193
261
  }
194
262
  /**
195
- * Judge one stored record against the primary root and the roots claimed before
196
- * it.
263
+ * Judge one stored record's structural trust and current availability —
264
+ * WITHOUT looking at any other record (record-vs-record overlap is the second
265
+ * pass of {@link classifyStoredRoots}).
197
266
  *
198
267
  * Identity and the recorded directory are checked BEFORE the current
199
268
  * resolution is used for anything: a record whose path now resolves elsewhere
200
269
  * must not have its new (attacker-influenced) target participate in the
201
- * overlap rules.
270
+ * overlap rules. The primary-root rules therefore judge the RECORDED
271
+ * boundary — the directory the operator was granted — not what `path`
272
+ * resolves to today.
202
273
  */
203
- function classifyStoredRoot(record, primary, claimed, duplicatedId, position) {
274
+ function classifyStoredRoot(record, primary, duplicatedId, position, platform) {
204
275
  const invalid = (detail) => ({
205
276
  ...record,
206
277
  state: "invalid",
@@ -210,18 +281,24 @@ function classifyStoredRoot(record, primary, claimed, duplicatedId, position) {
210
281
  if (typeof record.id !== "string" || record.id === "") return invalid("the record has no id");
211
282
  if (duplicatedId) return invalid(`the id "${record.id}" is used by more than one record (entry ${position + 1})`);
212
283
  if (!isAbsolute(record.path)) return invalid(`"${record.path}" is not an absolute path`);
213
- const recorded = record.recordedPath;
284
+ const grants = [
285
+ record.recordedPath,
286
+ record.readOnlyRecordedPath,
287
+ record.readWriteRecordedPath
288
+ ].filter((grant) => typeof grant === "string" && grant !== "");
289
+ if (grants.length > 1) return invalid("the record declares more than one grant (ambiguous access)");
290
+ const recorded = grants[0];
214
291
  if (typeof recorded !== "string" || recorded === "") return invalid("the record does not say which directory it was granted for");
292
+ if (!isAbsolute(recorded)) return invalid(`the recorded grant "${recorded}" is not an absolute path`);
293
+ 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`);
215
296
  const canonical = canonicalRoot(record.path);
216
297
  if (!samePath(canonical, recorded)) return {
217
298
  ...record,
218
299
  state: "redirected",
219
300
  detail: `the path now resolves to "${canonical}" instead of the registered "${recorded}"`
220
301
  };
221
- if (samePath(canonical, primary)) return invalid(`"${canonical}" is this session's workspace root`);
222
- if (isCanonicallyUnder(canonical, primary) || isCanonicallyUnder(primary, canonical)) return invalid(`"${canonical}" overlaps this session's workspace root`);
223
- if (claimed.some((existing) => samePath(existing, canonical))) return invalid(`"${canonical}" is registered twice`);
224
- for (const existing of claimed) if (isCanonicallyUnder(canonical, existing) || isCanonicallyUnder(existing, canonical)) return invalid(`"${canonical}" overlaps the registered root "${existing}"`);
225
302
  let isDirectory = false;
226
303
  try {
227
304
  isDirectory = statSync(canonical).isDirectory();
@@ -249,6 +326,26 @@ function availableRoots(statuses) {
249
326
  return statuses.filter((status) => status.state === "available").map((status) => status.path);
250
327
  }
251
328
  /**
329
+ * The recorded boundary one status still RESERVES: the grant it was
330
+ * registered for, which a new or revived registration may not overlap merely
331
+ * because the root is currently unusable.
332
+ *
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
339
+ * classification.
340
+ * @param status - one classified registration.
341
+ * @returns the absolute recorded boundary, or `undefined` when there is none.
342
+ */
343
+ function reservedRootBoundary(status) {
344
+ if (status.state === "invalid" && status.invalidReason !== "membership-conflict") return void 0;
345
+ const boundary = singleGrantOf(status);
346
+ return boundary !== void 0 && isAbsolute(boundary) ? boundary : void 0;
347
+ }
348
+ /**
252
349
  * Pair every status with the 1-based ordinal the surfaces accept for it. The
253
350
  * ordinal belongs to the POSITION, so it stays correct even for records whose
254
351
  * id is duplicated or missing — which is exactly the case where a positional
@@ -342,4 +439,4 @@ function describeRef(ref) {
342
439
  }
343
440
  }
344
441
  //#endregion
345
- export { classifyStoredRoots as a, indexedStatuses as c, resolveRootIndex as d, resolveRootRef as f, canonicalRoot as i, isCanonicallyUnder as l, additionalRootId as n, entryRootRef as o, validateRootCandidate as p, availableRoots as r, expandRootInput as s, RootValidationError as t, removeStatusAt as u };
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 };