@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.8
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.
- package/CHANGELOG.en.md +51 -1
- package/CHANGELOG.md +51 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +69 -4
- package/lib/client.js +2678 -276
- package/lib/client.js.map +1 -1
- package/lib/command-CESuwlVt.js +1306 -0
- package/lib/command.js +2 -2
- package/lib/compat-DR2LvmYZ.js +600 -0
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-Nlp2AiRX.js} +56 -24
- package/lib/fs.js +2 -1
- package/lib/index.js +10 -8
- package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +7 -2
- package/lib/method-wrapper-CFY3p3j2.js +26 -0
- package/lib/process-fence.js +280 -0
- package/lib/providers.js +87 -0
- package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
- package/lib/registry.js +2 -2
- package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
- package/lib/sandbox-CVAcT_gy.js +743 -0
- package/lib/sandbox.js +2 -1
- package/lib/scope.js +229 -20
- package/lib/types/client/RootSwitcher.d.ts +63 -0
- package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
- package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
- package/lib/types/client/files-tree/adapter.d.ts +94 -0
- package/lib/types/client/files-tree/directory-node.d.ts +62 -0
- package/lib/types/client/files-tree/face.d.ts +107 -0
- package/lib/types/client/files-tree/locales.d.ts +10 -0
- package/lib/types/client/files-tree/primitives.d.ts +33 -0
- package/lib/types/client/files-tree/store.d.ts +81 -0
- package/lib/types/client/locales.d.ts +56 -10
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
- package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
- package/lib/types/client/open-in-app/adapter.d.ts +46 -0
- package/lib/types/client/open-in-app/applications.d.ts +4 -0
- package/lib/types/client/open-in-app/controller.d.ts +53 -0
- package/lib/types/client/open-in-app/locales.d.ts +10 -0
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
- package/lib/types/client/open-in-app/open-path.d.ts +13 -0
- package/lib/types/client/panel-client.d.ts +2 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +16 -3
- package/lib/types/compat/client-icons.d.ts +18 -0
- package/lib/types/compat/dsh-version.d.ts +57 -9
- package/lib/types/compat/open-in-app-routes.d.ts +38 -0
- package/lib/types/compat/process-entrypoints.d.ts +31 -0
- package/lib/types/compat/process-result.d.ts +14 -0
- package/lib/types/compat/pty-marker.d.ts +83 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/compat/storage-zod.d.ts +65 -0
- package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
- package/lib/types/compat/windows-native.d.ts +68 -0
- package/lib/types/compat/windows-path-guard.d.ts +56 -0
- package/lib/types/compat/workspace-files.d.ts +2 -0
- package/lib/types/compat.d.ts +15 -5
- package/lib/types/contract.d.ts +31 -19
- package/lib/types/dialects.d.ts +99 -3
- package/lib/types/fs.d.ts +32 -10
- package/lib/types/index.d.ts +8 -3
- package/lib/types/process-fence.d.ts +54 -0
- package/lib/types/profile-isolation.d.ts +178 -0
- package/lib/types/providers.d.ts +70 -0
- package/lib/types/registry.d.ts +132 -31
- package/lib/types/roots.d.ts +100 -13
- package/lib/types/sandbox.d.ts +59 -3
- package/lib/types/scope.d.ts +200 -14
- package/lib/types/windows-broker.d.ts +17 -0
- package/lib/types/windows-deny.d.ts +1 -0
- package/lib/types/windows-profile.d.ts +10 -13
- package/lib/types/windows-runner-contract.d.ts +97 -0
- package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
- package/lib/windows-runner.js +1256 -13
- package/lib/workspace-files.js +138 -3
- package/lib/workspace-routing-YPxaQ4V3.js +26 -0
- package/package.json +84 -62
- package/scripts/windows-broker/install.ps1 +89 -0
- package/scripts/windows-broker/rights.cs +58 -0
- package/scripts/windows-broker/service.cs +323 -0
- package/scripts/windows-broker/verify-install.ps1 +80 -0
- package/lib/command-CTdNvwt1.js +0 -791
- package/lib/compat-B6ZqbQBy.js +0 -295
- package/lib/method-wrapper-Zc-JlDP3.js +0 -51
- package/lib/sandbox-BRXl_FpF.js +0 -132
- package/lib/types/client/RootFileTree.d.ts +0 -12
- package/lib/types/panel-files.d.ts +0 -3
- package/lib/windows-profile-CiFsFYIV.js +0 -216
- package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
|
@@ -1,11 +1,58 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-CEfmZzu0.js";
|
|
2
|
+
import { a as classifyStoredRoots, d as reservedRootBoundary, f as resolveRootIndex, i as canonicalRoot, m as validateRootCandidate, n as additionalRootId, r as availableRoots, s as expandRootInput, t as RootValidationError, u as removeStatusAt } from "./roots-B-0l8mNG.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
2
4
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { dirname, isAbsolute } from "node:path";
|
|
5
5
|
import { closeSync, fstatSync, mkdirSync, openSync, statSync } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute } from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
6
8
|
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
7
|
-
import { z } from "zod";
|
|
8
9
|
import { tryLockExclusive } from "@deepseek-ai/node-addon-system/flock";
|
|
10
|
+
//#region src/compat/storage-zod.ts
|
|
11
|
+
/**
|
|
12
|
+
* Build the domain record schemas with the zod module the installed
|
|
13
|
+
* `@deepseek-ai/dsh-storage-domain` resolves.
|
|
14
|
+
*
|
|
15
|
+
* ```text
|
|
16
|
+
* through 0.2.1-alpha.2 storage-domain declares zod ^4.4.3
|
|
17
|
+
* from 0.2.1-alpha.3 storage-domain declares zod 4.4.3 exactly
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* `domainTable` types its `schema` parameter as `ZodType`, resolved from
|
|
21
|
+
* whichever zod copy storage-domain itself depends on — never from the copy
|
|
22
|
+
* this package installs for its own validation work (`src/contract.ts`
|
|
23
|
+
* parses the panel wire shapes with it). While upstream declared `^4.4.3`,
|
|
24
|
+
* both sides resolved to one install; the exact `4.4.3` pin since
|
|
25
|
+
* `0.2.1-alpha.3` splits the tree into two resolutions of the same v4 line,
|
|
26
|
+
* and zod v4 brands every type with the minor it was compiled by, so a
|
|
27
|
+
* schema produced by `4.6.x` is not assignable to a `ZodType` declared by
|
|
28
|
+
* `4.4.3`. Pinning this package to `4.4.3` would only move the split to the
|
|
29
|
+
* other side: on the supported releases before the pin changed, upstream's
|
|
30
|
+
* `^4.4.3` resolves to `4.6.x` and the pinned copy is the foreign one.
|
|
31
|
+
*
|
|
32
|
+
* The fix resolves the BUILDER the same way the boundary resolves the TYPE:
|
|
33
|
+
* `require('zod')` from inside the storage-domain package. A schema built by
|
|
34
|
+
* that namespace is by construction an instance of the `ZodType` the
|
|
35
|
+
* installed `domainTable` accepts, on every supported release — whichever
|
|
36
|
+
* zod version a release pins, both sides keep coming from one module.
|
|
37
|
+
*
|
|
38
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/storage-zod
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* The `z` namespace as the installed `@deepseek-ai/dsh-storage-domain`
|
|
42
|
+
* resolves it.
|
|
43
|
+
*
|
|
44
|
+
* A resolution failure propagates rather than falling back to this package's
|
|
45
|
+
* zod: a storage-domain that no longer depends on zod changes what
|
|
46
|
+
* `domainTable` accepts, and that is an adapter decision, not a silent
|
|
47
|
+
* degradation. The failure surfaces where a missing peer already would — at
|
|
48
|
+
* module load, before any row runs.
|
|
49
|
+
* @returns the zod module, typed as the used surface only.
|
|
50
|
+
*/
|
|
51
|
+
function domainZod() {
|
|
52
|
+
const require = createRequire(import.meta.url);
|
|
53
|
+
return createRequire(require.resolve("@deepseek-ai/dsh-storage-domain/package.json"))("zod");
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
9
56
|
//#region src/registry-lease.ts
|
|
10
57
|
/**
|
|
11
58
|
* Store-wide kernel lease for the multi-root registry.
|
|
@@ -76,7 +123,7 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
|
|
|
76
123
|
mode: 448
|
|
77
124
|
});
|
|
78
125
|
if (process.platform === "win32") {
|
|
79
|
-
const { acquireLockHandleWin32 } = await import("./registry-lease-win32-
|
|
126
|
+
const { acquireLockHandleWin32 } = await import("./registry-lease-win32-DTO6QpT9.js");
|
|
80
127
|
let handle;
|
|
81
128
|
try {
|
|
82
129
|
handle = await acquireLockHandleWin32(leasePath);
|
|
@@ -128,7 +175,7 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
|
|
|
128
175
|
if (this.released) return;
|
|
129
176
|
this.released = true;
|
|
130
177
|
if (this.held.kind === "win32") {
|
|
131
|
-
const { releaseLockHandleWin32 } = await import("./registry-lease-win32-
|
|
178
|
+
const { releaseLockHandleWin32 } = await import("./registry-lease-win32-DTO6QpT9.js");
|
|
132
179
|
await releaseLockHandleWin32(this.held.handle);
|
|
133
180
|
return;
|
|
134
181
|
}
|
|
@@ -176,7 +223,9 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
|
|
|
176
223
|
* memory-authoritative after open, so two DSH processes sharing a storage
|
|
177
224
|
* root cannot each keep a live domain snapshot. A store-wide kernel lease
|
|
178
225
|
* (ADR-0007) elects one authority; a contended process publishes an empty
|
|
179
|
-
* scope
|
|
226
|
+
* scope marked `authority: 'unavailable'` — under which Full-access is
|
|
227
|
+
* limited to primary-root-only writes — rejects mutations, and retries the lease
|
|
228
|
+
* from {@link MultiRootRegistry.refresh}.
|
|
180
229
|
*
|
|
181
230
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/registry
|
|
182
231
|
*/
|
|
@@ -191,19 +240,36 @@ const MAX_ALIAS_LENGTH = 120;
|
|
|
191
240
|
* locale; the panel also shows this string verbatim via `RootsView.unavailable`.
|
|
192
241
|
*/
|
|
193
242
|
const REGISTRY_CONTENDED_MESSAGE = "the root registry is owned by another DSH process; additional roots are not granted here. Close the other process, then retry.";
|
|
194
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Default cap on stored additional-root records per primary root. The bench
|
|
245
|
+
* evidence for this default lives in `docs/reference/bench-root-scale-*.json`
|
|
246
|
+
* (`pnpm bench:roots`): classification and argv widening stay flat far beyond
|
|
247
|
+
* it, so the cap exists to bound the operator-facing list and the widened
|
|
248
|
+
* profile size, not to protect the classifier.
|
|
249
|
+
*/
|
|
250
|
+
const DEFAULT_MAX_ADDITIONAL_ROOTS = 16;
|
|
251
|
+
/** Hard ceiling for `maxAdditionalRoots`: values above it are config-invalid. */
|
|
252
|
+
const MAX_ADDITIONAL_ROOTS_CEILING = 64;
|
|
253
|
+
/**
|
|
254
|
+
* The durable-boundary validators. The builder is the zod module the
|
|
255
|
+
* INSTALLED storage-domain resolves, not this package's own `zod` copy:
|
|
256
|
+
* `domainTable` types `schema` as the `ZodType` of ITS resolved zod, and
|
|
257
|
+
* since `0.2.1-alpha.3` upstream pins that copy at `4.4.3` exactly while
|
|
258
|
+
* this package floats on `^4.6.2` for its wire validation. The two copies
|
|
259
|
+
* brand incompatible types on either side of that upstream change, so the
|
|
260
|
+
* schema is built where the boundary type comes from — see
|
|
261
|
+
* `src/compat/storage-zod.ts`.
|
|
262
|
+
*/
|
|
263
|
+
const z = domainZod();
|
|
195
264
|
const persistedRoot = z.object({
|
|
196
265
|
id: z.string().min(1),
|
|
197
266
|
path: z.string().min(1),
|
|
198
267
|
recordedPath: z.string().min(1).optional(),
|
|
268
|
+
readOnlyRecordedPath: z.string().min(1).optional(),
|
|
269
|
+
readWriteRecordedPath: z.string().min(1).optional(),
|
|
199
270
|
alias: z.string().optional(),
|
|
200
271
|
addedAt: z.string().min(1)
|
|
201
272
|
});
|
|
202
|
-
/**
|
|
203
|
-
* One primary root's whole registration list. The domain's `single` layout
|
|
204
|
-
* stores the entire unit as one document, so keys stay opaque strings — which
|
|
205
|
-
* is exactly what lets the canonical primary root itself be the key.
|
|
206
|
-
*/
|
|
207
273
|
const persistedPrimaryRoot = z.object({ roots: z.array(persistedRoot) });
|
|
208
274
|
/**
|
|
209
275
|
* The domain declaration: version 1, `single` layout, one `roots` table keyed
|
|
@@ -221,6 +287,7 @@ const multiRootDomainSpec = defineDomain({
|
|
|
221
287
|
var MultiRootRegistry = class extends Service {
|
|
222
288
|
static inject = [
|
|
223
289
|
"multiRootCompat",
|
|
290
|
+
"multiRootProviders",
|
|
224
291
|
"storageDomain",
|
|
225
292
|
"multiRootScope"
|
|
226
293
|
];
|
|
@@ -248,13 +315,27 @@ var MultiRootRegistry = class extends Service {
|
|
|
248
315
|
loggedContended = false;
|
|
249
316
|
/** True once the fiber disposer has run: no further acquisition may happen. */
|
|
250
317
|
disposed = false;
|
|
318
|
+
/** The validated per-primary-root cap (the default when the row omits it). */
|
|
319
|
+
maxRoots;
|
|
320
|
+
/** Keys already warned about a historical over-limit store (one warning each). */
|
|
321
|
+
warnedOverLimit = /* @__PURE__ */ new Set();
|
|
251
322
|
/**
|
|
252
323
|
* @param ctx - the host context; `storageDomain` and `multiRootScope` are injected.
|
|
253
|
-
* @param config - the patch row's `leasePath`.
|
|
324
|
+
* @param config - the patch row's `leasePath` and `maxAdditionalRoots`.
|
|
254
325
|
*/
|
|
255
326
|
constructor(ctx, config = {}) {
|
|
256
327
|
super(ctx, "multiRootRegistry");
|
|
257
328
|
this.leasePath = config.leasePath;
|
|
329
|
+
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
330
|
+
const configured = config.maxAdditionalRoots === void 0 ? 16 : config.maxAdditionalRoots;
|
|
331
|
+
if (Number.isInteger(configured) && configured >= 1 && configured <= 64) this.maxRoots = configured;
|
|
332
|
+
else {
|
|
333
|
+
this.maxRoots = 16;
|
|
334
|
+
this.authorityState = {
|
|
335
|
+
kind: "config-invalid",
|
|
336
|
+
reason: `${DOMAIN_NAME}: maxAdditionalRoots must be an integer between 1 and 64, got ${JSON.stringify(configured)}`
|
|
337
|
+
};
|
|
338
|
+
}
|
|
258
339
|
}
|
|
259
340
|
/**
|
|
260
341
|
* Become the Registry Authority if possible: acquire the store-wide lease,
|
|
@@ -268,6 +349,10 @@ var MultiRootRegistry = class extends Service {
|
|
|
268
349
|
*/
|
|
269
350
|
async [Service.init]() {
|
|
270
351
|
this.ctx.effect(() => () => this.releaseAuthority(), "multi-root-registry: authority release");
|
|
352
|
+
if (this.authorityState.kind === "config-invalid") {
|
|
353
|
+
this.ctx.logger.error(`multi-root-workspace: ${this.authorityState.reason}; no additional root is granted`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
271
356
|
await this.ensureAuthority();
|
|
272
357
|
}
|
|
273
358
|
/**
|
|
@@ -281,7 +366,8 @@ var MultiRootRegistry = class extends Service {
|
|
|
281
366
|
switch (this.authorityState.kind) {
|
|
282
367
|
case "active": return;
|
|
283
368
|
case "contended": return REGISTRY_CONTENDED_MESSAGE;
|
|
284
|
-
case "storage-failed":
|
|
369
|
+
case "storage-failed":
|
|
370
|
+
case "config-invalid": return this.authorityState.reason;
|
|
285
371
|
}
|
|
286
372
|
}
|
|
287
373
|
/**
|
|
@@ -292,6 +378,21 @@ var MultiRootRegistry = class extends Service {
|
|
|
292
378
|
return this.authorityState;
|
|
293
379
|
}
|
|
294
380
|
/**
|
|
381
|
+
* The configured cap on stored additional-root records per primary root.
|
|
382
|
+
* Surfaces show it next to the count so "N of LIMIT" is always explainable.
|
|
383
|
+
*/
|
|
384
|
+
get limit() {
|
|
385
|
+
return this.maxRoots;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Whether one primary root's stored record count exceeds {@link limit} — a
|
|
389
|
+
* historical over-limit store, which stays fully classified and granted.
|
|
390
|
+
* @param primaryRoot - the session workspace root (any spelling).
|
|
391
|
+
*/
|
|
392
|
+
overLimit(primaryRoot) {
|
|
393
|
+
return this.statusesOf(canonicalRoot(primaryRoot)).length > this.maxRoots;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
295
396
|
* Every registration of one primary root, in registry order, as last
|
|
296
397
|
* classified. A READ of the in-memory state: it never touches the filesystem
|
|
297
398
|
* and never writes. Surfaces that must re-check the directories first call
|
|
@@ -342,24 +443,41 @@ var MultiRootRegistry = class extends Service {
|
|
|
342
443
|
this.requireActive();
|
|
343
444
|
const key = canonicalRoot(primaryRoot);
|
|
344
445
|
return await this.serialize(key, async () => {
|
|
345
|
-
const
|
|
446
|
+
const earlier = this.cache.get(key);
|
|
447
|
+
const statuses = this.reclassify(key);
|
|
448
|
+
const alias = normalizeAlias(input.alias);
|
|
449
|
+
const expanded = expandRootInput(input.path);
|
|
450
|
+
if (process.platform === "win32" && isWindowsNetworkOrDevicePath(expanded)) throw new RootValidationError("unsupported-path", `Windows ACL confinement cannot cover network or device paths: ${expanded}`);
|
|
451
|
+
const preliminary = isAbsolute(expanded) ? canonicalRoot(expanded) : void 0;
|
|
452
|
+
const revived = preliminary === void 0 ? -1 : statuses.findIndex((status) => {
|
|
453
|
+
if (process.platform === "win32" && isWindowsNetworkOrDevicePath(status.path)) return false;
|
|
454
|
+
if (status.path !== preliminary && canonicalRoot(status.path) !== preliminary) return false;
|
|
455
|
+
const before = earlier?.find((candidate) => candidate.id === status.id);
|
|
456
|
+
return status.state !== "available" || before !== void 0 && before.state !== "available";
|
|
457
|
+
});
|
|
458
|
+
if (revived === -1 && statuses.length >= this.maxRoots) throw new RootValidationError("root-limit-reached", `this workspace already has ${statuses.length} additional roots; the configured limit is ${this.maxRoots}. Remove one first, or raise maxAdditionalRoots.`, { reference: String(this.maxRoots) });
|
|
346
459
|
const canonical = validateRootCandidate(input.path, {
|
|
347
460
|
primaryRoot: key,
|
|
348
|
-
existing:
|
|
461
|
+
existing: statuses.flatMap((status, index) => {
|
|
462
|
+
if (index === revived) return [];
|
|
463
|
+
const boundary = reservedRootBoundary(status);
|
|
464
|
+
return boundary === void 0 ? [] : [boundary];
|
|
465
|
+
})
|
|
349
466
|
});
|
|
350
|
-
const
|
|
351
|
-
const revived = statuses.findIndex((status) => status.path === canonical || canonicalRoot(status.path) === canonical);
|
|
467
|
+
const access = input.access ?? "inherit";
|
|
352
468
|
const next = revived === -1 ? [...statuses, {
|
|
353
469
|
id: additionalRootId(randomUUID()),
|
|
354
470
|
path: canonical,
|
|
355
|
-
|
|
471
|
+
access,
|
|
472
|
+
...grantFields(access, canonical),
|
|
356
473
|
state: "available",
|
|
357
474
|
...alias === void 0 ? {} : { alias },
|
|
358
475
|
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
359
476
|
}] : statuses.map((status, index) => index === revived ? {
|
|
360
|
-
...status,
|
|
477
|
+
...dropGrantFields(status),
|
|
361
478
|
path: canonical,
|
|
362
|
-
|
|
479
|
+
access: input.access ?? status.access,
|
|
480
|
+
...grantFields(input.access ?? status.access, canonical),
|
|
363
481
|
addedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
364
482
|
...alias === void 0 ? {} : { alias }
|
|
365
483
|
} : status);
|
|
@@ -367,6 +485,47 @@ var MultiRootRegistry = class extends Service {
|
|
|
367
485
|
});
|
|
368
486
|
}
|
|
369
487
|
/**
|
|
488
|
+
* Change one registration's write authority WITHOUT re-recording its path.
|
|
489
|
+
*
|
|
490
|
+
* Re-resolving is not re-authorizing: the grant keeps the canonical
|
|
491
|
+
* directory it was recorded for; only which persisted field carries it
|
|
492
|
+
* changes. The three levels are not a total order — `inherit` is weaker
|
|
493
|
+
* than `read-write` under a read-only session but stronger under
|
|
494
|
+
* Full-access — so "upgrade" cannot be a single direction. The gate is
|
|
495
|
+
* therefore: pinning to `read-only` is always allowed (a withheld root may
|
|
496
|
+
* be restricted while it is withheld), while ANY other target requires the
|
|
497
|
+
* root to be `available` under the FRESH classification — every
|
|
498
|
+
* non-read-only level can widen authority under some session mode, and a
|
|
499
|
+
* root that is not currently granted must not be granted anything by a
|
|
500
|
+
* metadata change.
|
|
501
|
+
* @param primaryRoot - the session workspace root the root belongs to.
|
|
502
|
+
* @param ref - which registered root to change.
|
|
503
|
+
* @param access - the new write authority.
|
|
504
|
+
* @returns the updated status list.
|
|
505
|
+
* @throws {RootValidationError} `not-found` when the reference matches
|
|
506
|
+
* nothing, `root-unavailable` when the target is not `available` and the
|
|
507
|
+
* new level is not `read-only`.
|
|
508
|
+
*/
|
|
509
|
+
async setAccess(primaryRoot, ref, access) {
|
|
510
|
+
await this.ensureAuthority();
|
|
511
|
+
this.requireActive();
|
|
512
|
+
const key = canonicalRoot(primaryRoot);
|
|
513
|
+
return await this.serialize(key, async () => {
|
|
514
|
+
const statuses = this.reclassify(key);
|
|
515
|
+
const targetIndex = resolveRootIndex(statuses, ref);
|
|
516
|
+
const target = statuses[targetIndex];
|
|
517
|
+
if (target.access === access) return statuses;
|
|
518
|
+
if (access !== "read-only" && target.state !== "available") throw new RootValidationError("root-unavailable", `"${target.path}" is ${target.state} and cannot change to "${access}"; restore the directory first — only "read-only" may be set while a root is not granted`, { reference: target.id });
|
|
519
|
+
const recorded = target.recordedPath ?? target.readOnlyRecordedPath ?? target.readWriteRecordedPath;
|
|
520
|
+
const next = statuses.map((status, index) => index === targetIndex ? {
|
|
521
|
+
...dropGrantFields(status),
|
|
522
|
+
access,
|
|
523
|
+
...recorded === void 0 ? {} : grantFields(access, recorded)
|
|
524
|
+
} : status);
|
|
525
|
+
return await this.persist(key, next);
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
370
529
|
* Remove one registration.
|
|
371
530
|
* @param primaryRoot - the session workspace root the root belongs to.
|
|
372
531
|
* @param ref - which registered root to remove.
|
|
@@ -391,7 +550,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
391
550
|
await this.ensureAuthority();
|
|
392
551
|
this.requireActive();
|
|
393
552
|
const key = canonicalRoot(primaryRoot);
|
|
394
|
-
return await this.serialize(key, async () => await this.persist(key, removeStatusAt(this.
|
|
553
|
+
return await this.serialize(key, async () => await this.persist(key, removeStatusAt(this.reclassify(key), ref)));
|
|
395
554
|
}
|
|
396
555
|
/**
|
|
397
556
|
* Set or clear one registration's display alias.
|
|
@@ -406,7 +565,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
406
565
|
this.requireActive();
|
|
407
566
|
const key = canonicalRoot(primaryRoot);
|
|
408
567
|
return await this.serialize(key, async () => {
|
|
409
|
-
const statuses = this.
|
|
568
|
+
const statuses = this.reclassify(key);
|
|
410
569
|
const targetIndex = resolveRootIndex(statuses, ref);
|
|
411
570
|
const normalized = normalizeAlias(alias);
|
|
412
571
|
const next = statuses.map((status, index) => {
|
|
@@ -433,7 +592,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
433
592
|
this.requireActive();
|
|
434
593
|
const key = canonicalRoot(primaryRoot);
|
|
435
594
|
return await this.serialize(key, async () => {
|
|
436
|
-
const statuses = this.
|
|
595
|
+
const statuses = this.reclassify(key);
|
|
437
596
|
const targetIndex = resolveRootIndex(statuses, ref);
|
|
438
597
|
const anchorIndex = beforeRef === void 0 ? void 0 : resolveRootIndex(statuses, beforeRef);
|
|
439
598
|
if (anchorIndex === targetIndex) return statuses;
|
|
@@ -494,6 +653,10 @@ var MultiRootRegistry = class extends Service {
|
|
|
494
653
|
const key = canonicalRoot(primaryRoot);
|
|
495
654
|
const statuses = classifyStoredRoots(key, record.roots.map(toRegisteredRoot));
|
|
496
655
|
this.cache.set(key, statuses);
|
|
656
|
+
if (statuses.length > this.maxRoots && !this.warnedOverLimit.has(key)) {
|
|
657
|
+
this.warnedOverLimit.add(key);
|
|
658
|
+
this.ctx.logger.warn(`multi-root-workspace: ${statuses.length} roots are stored for ${key}, above the configured limit of ${this.maxRoots}; all of them stay granted, only new adds are refused`);
|
|
659
|
+
}
|
|
497
660
|
this.publish(key, statuses);
|
|
498
661
|
return statuses;
|
|
499
662
|
}
|
|
@@ -571,25 +734,36 @@ var MultiRootRegistry = class extends Service {
|
|
|
571
734
|
/**
|
|
572
735
|
* Push one status list into the scope and notify listeners.
|
|
573
736
|
*
|
|
574
|
-
* What is published is the effective GRANT —
|
|
575
|
-
* withheld registration can never reach a provider
|
|
576
|
-
*
|
|
737
|
+
* What is published is the effective GRANT — membership requires
|
|
738
|
+
* `available`, so a withheld registration can never reach a provider as a
|
|
739
|
+
* grant. Withheld entries still publish for their DENY boundary alone;
|
|
740
|
+
* that includes `membership-conflict` records, whose recorded grant is
|
|
741
|
+
* intact and trustworthy even though their membership cannot stand. When
|
|
742
|
+
* the grant and the withheld set are unchanged (the common case for a
|
|
743
|
+
* read-path refresh on a
|
|
577
744
|
* healthy workspace) the scope is left alone and no listener fires: a refresh
|
|
578
745
|
* exists to keep the two in sync, not to churn on every listing — and, since
|
|
579
746
|
* the scope rebuilds its root table on each call, skipping the no-op write is
|
|
580
747
|
* also what keeps the read path cheap.
|
|
748
|
+
*
|
|
749
|
+
* Available roots keep registry order: instruction discovery consumes them
|
|
750
|
+
* under a shared byte budget, so a move must update the live scope immediately.
|
|
751
|
+
* Aliases and no-op moves still leave the signature unchanged.
|
|
581
752
|
*/
|
|
582
753
|
publish(key, statuses) {
|
|
583
754
|
const withheld = statuses.filter((status) => status.state !== "available");
|
|
584
|
-
const signature = [...statuses.filter((status) => status.state === "available").map((status) => `available:${status.
|
|
585
|
-
if (this.published.get(key) === signature) return;
|
|
755
|
+
const signature = [...statuses.filter((status) => status.state === "available").map((status) => `available:${status.access}:${recordedGrantOf(status)}`), ...withheld.map((status) => `${status.state}:${status.invalidReason ?? ""}:${status.access}:${status.path}:${recordedGrantOf(status) ?? ""}`)].join("\n");
|
|
756
|
+
if ((this.published.get(key) ?? "") === signature) return;
|
|
586
757
|
this.published.set(key, signature);
|
|
587
758
|
const effective = statuses.flatMap((status) => {
|
|
588
|
-
|
|
759
|
+
const recorded = recordedGrantOf(status);
|
|
760
|
+
if (status.state === "invalid" && status.invalidReason !== "membership-conflict" || recorded === void 0) return [];
|
|
589
761
|
return [{
|
|
590
762
|
id: status.id,
|
|
591
763
|
path: status.path,
|
|
592
|
-
recordedPath:
|
|
764
|
+
recordedPath: recorded,
|
|
765
|
+
access: status.access,
|
|
766
|
+
...status.state === "available" ? {} : { withheld: true },
|
|
593
767
|
...status.alias === void 0 ? {} : { alias: status.alias }
|
|
594
768
|
}];
|
|
595
769
|
});
|
|
@@ -606,6 +780,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
606
780
|
*/
|
|
607
781
|
requireTable() {
|
|
608
782
|
if (this.authorityState.kind === "contended") throw new RootValidationError("registry-contended", REGISTRY_CONTENDED_MESSAGE);
|
|
783
|
+
if (this.authorityState.kind === "config-invalid") throw new RootValidationError("config-invalid", `the root registry configuration is invalid (${this.authorityState.reason}); no additional root is granted`);
|
|
609
784
|
if (this.authorityState.kind === "storage-failed") throw new RootValidationError("storage-unavailable", `the root registry store is unavailable (${this.authorityState.reason}); remove or repair \$DSH_HOME/storages/multi_root_workspace.json and restart dsh`);
|
|
610
785
|
if (this.table === void 0) throw new Error("multi-root registry is not started yet");
|
|
611
786
|
return this.table;
|
|
@@ -628,6 +803,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
628
803
|
await this.serializeAuthority(async () => {
|
|
629
804
|
if (this.disposed) return;
|
|
630
805
|
if (this.authorityState.kind === "active") return;
|
|
806
|
+
if (this.authorityState.kind === "config-invalid") return;
|
|
631
807
|
if (this.lease !== void 0) {
|
|
632
808
|
await this.openDomain();
|
|
633
809
|
return;
|
|
@@ -676,10 +852,12 @@ var MultiRootRegistry = class extends Service {
|
|
|
676
852
|
this.cache.clear();
|
|
677
853
|
this.published.clear();
|
|
678
854
|
for (const [primaryRoot, record] of this.table.entries()) this.accept(primaryRoot, record);
|
|
855
|
+
this.ctx.multiRootScope.setRegistryAuthority(true);
|
|
679
856
|
}
|
|
680
857
|
/** Fail closed: empty scope, no table, contended state. */
|
|
681
858
|
enterContended() {
|
|
682
859
|
this.withdrawPublished();
|
|
860
|
+
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
683
861
|
this.table = void 0;
|
|
684
862
|
this.authorityState = { kind: "contended" };
|
|
685
863
|
if (!this.loggedContended) {
|
|
@@ -690,6 +868,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
690
868
|
/** Fail closed: empty scope, no table, storage-failed state. The lease, if held, stays. */
|
|
691
869
|
enterStorageFailed(reason) {
|
|
692
870
|
this.withdrawPublished();
|
|
871
|
+
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
693
872
|
this.table = void 0;
|
|
694
873
|
this.authorityState = {
|
|
695
874
|
kind: "storage-failed",
|
|
@@ -720,6 +899,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
720
899
|
await this.serializeAuthority(async () => {
|
|
721
900
|
this.table = void 0;
|
|
722
901
|
this.authorityState = { kind: "contended" };
|
|
902
|
+
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
723
903
|
await this.drainMutations();
|
|
724
904
|
this.withdrawPublished();
|
|
725
905
|
const domain = this.domain;
|
|
@@ -755,22 +935,63 @@ var MultiRootRegistry = class extends Service {
|
|
|
755
935
|
return await run;
|
|
756
936
|
}
|
|
757
937
|
};
|
|
938
|
+
/** The recorded canonical directory a record was granted for, whichever grant field holds it. */
|
|
939
|
+
function recordedGrantOf(record) {
|
|
940
|
+
return record.recordedPath ?? record.readOnlyRecordedPath ?? record.readWriteRecordedPath;
|
|
941
|
+
}
|
|
942
|
+
/** The grant fields one access level persists: exactly one key, always. */
|
|
943
|
+
function grantFields(access, recorded) {
|
|
944
|
+
if (access === "read-only") return { readOnlyRecordedPath: recorded };
|
|
945
|
+
if (access === "read-write") return { readWriteRecordedPath: recorded };
|
|
946
|
+
return { recordedPath: recorded };
|
|
947
|
+
}
|
|
948
|
+
/** Strip every grant field so a re-write can set exactly the right one back. */
|
|
949
|
+
function dropGrantFields(status) {
|
|
950
|
+
const { recordedPath: _inherit, readOnlyRecordedPath: _read, readWriteRecordedPath: _write, ...rest } = status;
|
|
951
|
+
return rest;
|
|
952
|
+
}
|
|
758
953
|
/** Project one status back to the durable record shape. */
|
|
759
954
|
function toPersistedRoot(status) {
|
|
955
|
+
const grant = [
|
|
956
|
+
status.recordedPath,
|
|
957
|
+
status.readOnlyRecordedPath,
|
|
958
|
+
status.readWriteRecordedPath
|
|
959
|
+
].filter((grant) => grant !== void 0 && grant !== "").length > 1 ? {
|
|
960
|
+
...status.recordedPath === void 0 ? {} : { recordedPath: status.recordedPath },
|
|
961
|
+
...status.readOnlyRecordedPath === void 0 ? {} : { readOnlyRecordedPath: status.readOnlyRecordedPath },
|
|
962
|
+
...status.readWriteRecordedPath === void 0 ? {} : { readWriteRecordedPath: status.readWriteRecordedPath }
|
|
963
|
+
} : status.access === "read-only" ? status.readOnlyRecordedPath === void 0 || status.readOnlyRecordedPath === "" ? {} : { readOnlyRecordedPath: status.readOnlyRecordedPath } : status.access === "read-write" ? status.readWriteRecordedPath === void 0 || status.readWriteRecordedPath === "" ? {} : { readWriteRecordedPath: status.readWriteRecordedPath } : status.recordedPath === void 0 || status.recordedPath === "" ? {} : { recordedPath: status.recordedPath };
|
|
760
964
|
return {
|
|
761
965
|
id: status.id,
|
|
762
966
|
path: status.path,
|
|
763
|
-
...
|
|
967
|
+
...grant,
|
|
764
968
|
...status.alias === void 0 ? {} : { alias: status.alias },
|
|
765
969
|
addedAt: status.addedAt
|
|
766
970
|
};
|
|
767
971
|
}
|
|
768
|
-
/**
|
|
972
|
+
/**
|
|
973
|
+
* Recover the registered-root view of a stored record entry.
|
|
974
|
+
*
|
|
975
|
+
* Access is derived from WHICH grant field is present, never read from a
|
|
976
|
+
* stored `access` key: the persisted layout carries no such key on purpose.
|
|
977
|
+
* That is the downgrade safety in action — an older plugin version parses the
|
|
978
|
+
* same document with a schema that strips unknown keys, so a read-only record
|
|
979
|
+
* (no `recordedPath`) reaches the old classifier as "no grant recorded" and is
|
|
980
|
+
* reported `invalid`: downgrading can never escalate a read-only registration
|
|
981
|
+
* into a writable grant.
|
|
982
|
+
*/
|
|
769
983
|
function toRegisteredRoot(entry) {
|
|
984
|
+
const hasInheritGrant = entry.recordedPath !== void 0 && entry.recordedPath !== "";
|
|
985
|
+
const hasReadGrant = entry.readOnlyRecordedPath !== void 0 && entry.readOnlyRecordedPath !== "";
|
|
986
|
+
const hasWriteGrant = entry.readWriteRecordedPath !== void 0 && entry.readWriteRecordedPath !== "";
|
|
987
|
+
const access = hasWriteGrant && !hasInheritGrant && !hasReadGrant ? "read-write" : hasReadGrant && !hasInheritGrant && !hasWriteGrant ? "read-only" : "inherit";
|
|
770
988
|
return {
|
|
771
989
|
id: additionalRootId(entry.id),
|
|
772
990
|
path: entry.path,
|
|
991
|
+
access,
|
|
773
992
|
...entry.recordedPath === void 0 ? {} : { recordedPath: entry.recordedPath },
|
|
993
|
+
...entry.readOnlyRecordedPath === void 0 ? {} : { readOnlyRecordedPath: entry.readOnlyRecordedPath },
|
|
994
|
+
...entry.readWriteRecordedPath === void 0 ? {} : { readWriteRecordedPath: entry.readWriteRecordedPath },
|
|
774
995
|
...entry.alias === void 0 ? {} : { alias: entry.alias },
|
|
775
996
|
addedAt: entry.addedAt
|
|
776
997
|
};
|
|
@@ -790,4 +1011,4 @@ function normalizeAlias(alias) {
|
|
|
790
1011
|
return trimmed;
|
|
791
1012
|
}
|
|
792
1013
|
//#endregion
|
|
793
|
-
export {
|
|
1014
|
+
export { MultiRootRegistry as a, multiRootDomainSpec as c, MAX_ALIAS_LENGTH as i, RegistryAuthorityLease as l, DOMAIN_NAME as n, REGISTRY_CONTENDED_MESSAGE as o, MAX_ADDITIONAL_ROOTS_CEILING as r, TABLE_NAME as s, DEFAULT_MAX_ADDITIONAL_ROOTS as t, RegistryLeaseContendedError as u };
|
package/lib/registry.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { DOMAIN_NAME, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec };
|
|
1
|
+
import { a as MultiRootRegistry, c as multiRootDomainSpec, i as MAX_ALIAS_LENGTH, n as DOMAIN_NAME, o as REGISTRY_CONTENDED_MESSAGE, r as MAX_ADDITIONAL_ROOTS_CEILING, s as TABLE_NAME, t as DEFAULT_MAX_ADDITIONAL_ROOTS } from "./registry-DCxC5Lay.js";
|
|
2
|
+
export { DEFAULT_MAX_ADDITIONAL_ROOTS, DOMAIN_NAME, MAX_ADDITIONAL_ROOTS_CEILING, MAX_ALIAS_LENGTH, MultiRootRegistry, MultiRootRegistry as default, REGISTRY_CONTENDED_MESSAGE, TABLE_NAME, multiRootDomainSpec };
|