@dsh-electron/dsh-plugin-multi-root-workspace 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +32 -1
- package/CHANGELOG.md +32 -1
- package/README.en.md +25 -8
- package/README.md +25 -8
- package/cordis.patch.yml +5 -1
- package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
- package/lib/auto-review.js +2 -2
- package/lib/client.js +969 -100
- package/lib/client.js.map +1 -1
- package/lib/{command-Dcz6nULo.js → command-DaVJ-mK3.js} +76 -11
- package/lib/command.js +1 -1
- package/lib/compat-CTJbpCxu.js +267 -0
- package/lib/compat.js +1 -1
- package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
- package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
- package/lib/index.js +10 -8
- package/lib/{instructions-KsUwUFE0.js → instructions-Ca80SkZq.js} +696 -27
- package/lib/instructions.js +1 -1
- package/lib/llm-message-CepyzEBQ.js +84 -0
- package/lib/lsp.js +4 -4
- package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
- package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
- package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
- package/lib/ops-compatibility.js +58 -0
- package/lib/ops-settings-UN2oOKEp.js +40 -0
- package/lib/ops-tools-k4JYaOrM.js +152 -0
- package/lib/process-fence.js +193 -34
- package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
- package/lib/{registry-DR_sMuvS.js → registry-BibiukWk.js} +236 -80
- package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
- package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
- package/lib/registry.js +2 -2
- package/lib/{review-context-NDpKeMCc.js → review-context-OxO-fMWR.js} +24 -7
- package/lib/review-context.js +1 -1
- package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
- package/lib/{sandbox-C-vCJCp3.js → sandbox-aobdyQGi.js} +7 -4
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +162 -12
- package/lib/skills-utyuvmwr.js +104 -0
- package/lib/skills.js +5 -106
- package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
- package/lib/types/authority/protocol.d.ts +5 -1
- package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
- package/lib/types/client/files-tree/adapter.d.ts +69 -16
- package/lib/types/client/files-tree/face.d.ts +31 -2
- package/lib/types/client/files-tree/locate.d.ts +74 -0
- package/lib/types/client/files-tree/navigator.d.ts +98 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/locales.d.ts +30 -0
- package/lib/types/client/ops-settings.d.ts +6 -0
- package/lib/types/client/skills-settings.d.ts +3 -2
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +3 -3
- package/lib/types/compat/files-tree.d.ts +8 -1
- package/lib/types/compat/forge-entrypoints.d.ts +44 -0
- package/lib/types/compat/instruction-activity.d.ts +32 -1
- package/lib/types/compat/ops-confirmation.d.ts +17 -0
- package/lib/types/compat/ops-settings.d.ts +11 -0
- package/lib/types/compat/ops-tools.d.ts +25 -0
- package/lib/types/compat/session-existence.d.ts +41 -0
- package/lib/types/compat/skills-client.d.ts +1 -0
- package/lib/types/compat/tool-context.d.ts +4 -0
- package/lib/types/compat.d.ts +2 -2
- package/lib/types/instructions.d.ts +6 -1
- package/lib/types/ops-compatibility.d.ts +34 -0
- package/lib/types/registry-operation.d.ts +7 -0
- package/lib/types/registry.d.ts +67 -8
- package/lib/types/review-context.d.ts +2 -0
- package/lib/types/roots.d.ts +116 -20
- package/lib/types/scope.d.ts +95 -3
- package/lib/workspace-files.js +4 -4
- package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
- package/package.json +5 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { c as stableOpsJson, l as touchedPathOfToolCall, s as pendingInstructionTargets } from "./instructions-Ca80SkZq.js";
|
|
3
|
+
import { r as opsCompatibilityToken } from "./ops-settings-UN2oOKEp.js";
|
|
4
|
+
import { l as isCanonicallyUnder } from "./roots-CmaWxRVy.js";
|
|
4
5
|
import { createHash } from "node:crypto";
|
|
5
6
|
import { resolve } from "node:path";
|
|
6
7
|
import { Service } from "@deepseek-ai/cordis";
|
|
@@ -50,7 +51,7 @@ var MultiRootReviewContext = class extends Service {
|
|
|
50
51
|
const policy = this.ctx.sandboxPolicy.resolve({ session: agent.session });
|
|
51
52
|
const primary = canonicalPath(policy.workspaceRoot);
|
|
52
53
|
if (agent.session.header.cwd !== void 0 && canonicalPath(agent.session.header.cwd) !== primary) throw new Error("review-context: inconsistent session origin");
|
|
53
|
-
await this.ctx.multiRootRegistry.refresh(
|
|
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
|
|
77
|
-
const
|
|
78
|
-
const
|
|
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)]
|
|
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) {
|
package/lib/review-context.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as MultiRootReviewContext } from "./review-context-
|
|
1
|
+
import { t as MultiRootReviewContext } from "./review-context-OxO-fMWR.js";
|
|
2
2
|
export { MultiRootReviewContext, MultiRootReviewContext as default };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-B5R2K3mM.js";
|
|
2
2
|
import { statSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, join, sep } from "node:path";
|
|
3
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
4
4
|
import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
//#region src/roots.ts
|
|
@@ -20,15 +20,21 @@ import { homedir } from "node:os";
|
|
|
20
20
|
* misconfiguration, not a path to guess at.
|
|
21
21
|
* 2. `missing` — the directory does not exist.
|
|
22
22
|
* 3. `not-a-directory` — it exists but is not a directory.
|
|
23
|
-
* 4. `
|
|
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
|
-
*
|
|
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* Same reasoning as rule
|
|
31
|
-
*
|
|
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
|
|
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
|
-
|
|
295
|
-
|
|
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
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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
|
|
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,
|
|
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-
|
|
2
|
+
import { a as splitConfined, c as mapConfined, i as restrictProfileArgs, l as widenConfined, o as widenProfileArgs, r as graftProfileArgs, t as denyProfileArgs } from "./dialects-CdlG3RF9.js";
|
|
3
3
|
import { brandMultiRootProvider } from "./providers.js";
|
|
4
4
|
import { SandboxUnavailableError, canonicalPath } from "@deepseek-ai/dsh-sandbox";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -105,8 +105,9 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
|
|
|
105
105
|
*/
|
|
106
106
|
confine(argv, policy, signal) {
|
|
107
107
|
const scope = this.ctx.multiRootScope.resolve(policy);
|
|
108
|
+
const mint = this.ctx.multiRootScope.captureMint(scope.authority !== "unavailable" ? "ambient" : policy.mode === "read-only" ? "read-only" : policy.mode === "workspace-write" ? "clamped" : "ambient");
|
|
108
109
|
const remember = (facts) => {
|
|
109
|
-
this.ctx.multiRootScope.rememberProfile(facts.argv);
|
|
110
|
+
this.ctx.multiRootScope.rememberProfile(facts.argv, mint);
|
|
110
111
|
return facts;
|
|
111
112
|
};
|
|
112
113
|
if (policy.mode === "workspace-write" && scope.authority === "unavailable") {
|
|
@@ -211,6 +212,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
|
|
|
211
212
|
workspaceRoot: policy.workspaceRoot,
|
|
212
213
|
...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
|
|
213
214
|
};
|
|
215
|
+
const mint = this.ctx.multiRootScope.captureMint("ambient");
|
|
214
216
|
const probe = super.confine.call(this, ["true"], probePolicy, signal);
|
|
215
217
|
const restrict = (confined) => {
|
|
216
218
|
try {
|
|
@@ -232,7 +234,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
|
|
|
232
234
|
};
|
|
233
235
|
return widenConfined(() => probe, (facts) => {
|
|
234
236
|
const result = restrict(facts);
|
|
235
|
-
this.ctx.multiRootScope.rememberProfile(result.argv);
|
|
237
|
+
this.ctx.multiRootScope.rememberProfile(result.argv, mint);
|
|
236
238
|
return result;
|
|
237
239
|
}, ["true"], probePolicy, signal);
|
|
238
240
|
}
|
|
@@ -253,6 +255,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
|
|
|
253
255
|
workspaceRoot: policy.workspaceRoot,
|
|
254
256
|
...policy.sessionId === void 0 ? {} : { sessionId: policy.sessionId }
|
|
255
257
|
};
|
|
258
|
+
const mint = this.ctx.multiRootScope.captureMint("clamped");
|
|
256
259
|
const probe = super.confine.call(this, ["true"], probePolicy, signal);
|
|
257
260
|
const clamp = (confined) => {
|
|
258
261
|
try {
|
|
@@ -269,7 +272,7 @@ var MultiRootSandboxProvider = class extends LocalSandboxProvider {
|
|
|
269
272
|
};
|
|
270
273
|
return widenConfined(() => probe, (facts) => {
|
|
271
274
|
const result = clamp(facts);
|
|
272
|
-
this.ctx.multiRootScope.rememberProfile(result.argv);
|
|
275
|
+
this.ctx.multiRootScope.rememberProfile(result.argv, mint);
|
|
273
276
|
return result;
|
|
274
277
|
}, ["true"], probePolicy, signal);
|
|
275
278
|
}
|
package/lib/sandbox.js
CHANGED
package/lib/scope.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
1
|
+
import { t as wrapMethod } from "./method-wrapper-D4oUCsEF.js";
|
|
2
|
+
import { a as rememberOperationProfile, i as operationPermitsProfile, r as operationFrameState, t as currentRegistryOperation } from "./registry-operation-CIlmt6_c.js";
|
|
3
|
+
import { t as aroundTool } from "./tool-context-CCeY3mrK.js";
|
|
4
|
+
import { r as opsCompatibilityToken } from "./ops-settings-UN2oOKEp.js";
|
|
5
|
+
import { d as primaryKey } from "./roots-CmaWxRVy.js";
|
|
6
|
+
import { a as createOpsProtectionState, l as opsDispatchBarrier, u as opsSpawnPermitted } from "./ops-tools-k4JYaOrM.js";
|
|
3
7
|
import { r as effectiveRootAccess } from "./contract-CStSEOcK.js";
|
|
4
8
|
import { statSync } from "node:fs";
|
|
5
9
|
import { isAbsolute } from "node:path";
|
|
@@ -145,6 +149,8 @@ function renderWorkspaceRootsContext(options) {
|
|
|
145
149
|
}
|
|
146
150
|
/** The tool-dispatch cap carried across async boundaries by the waterfall. */
|
|
147
151
|
const toolCapStore = new AsyncLocalStorage();
|
|
152
|
+
/** Bounded process-wide ledger of unadmitted mints; a burst cannot grow it without bound. */
|
|
153
|
+
const MAX_MINTED_PROFILES = 256;
|
|
148
154
|
/**
|
|
149
155
|
* The `ctx.multiRootScope` service. Providers ask it — never the filesystem —
|
|
150
156
|
* what the current scope is, which is what keeps the fs fence and every kernel
|
|
@@ -156,10 +162,19 @@ var MultiRootScopeService = class extends Service {
|
|
|
156
162
|
listeners = /* @__PURE__ */ new Set();
|
|
157
163
|
/** Enclosing {@link MultiRootScopeService.batch} depth; inner calls nest. */
|
|
158
164
|
batchDepth = 0;
|
|
165
|
+
opsProtection = createOpsProtectionState();
|
|
159
166
|
/** True when a batched write coalesced its notification into the flush. */
|
|
160
167
|
batchDirty = false;
|
|
161
168
|
admissionProof;
|
|
162
169
|
/**
|
|
170
|
+
* The publication generation the mint ledger is keyed on: bumped when a
|
|
171
|
+
* root-table write or an authority transition changes the authorization
|
|
172
|
+
* basis (a repeat publish of identical content is not a publication), so a
|
|
173
|
+
* minted profile dies with the state it was computed from.
|
|
174
|
+
*/
|
|
175
|
+
publicationVersion = 0;
|
|
176
|
+
mintedProfiles = [];
|
|
177
|
+
/**
|
|
163
178
|
* The registry-authority fact the registry row publishes. Defaults to
|
|
164
179
|
* `available`: a composition without a registry row can never have
|
|
165
180
|
* registered roots, so nothing is unknown there. The registry sets it false
|
|
@@ -179,6 +194,13 @@ var MultiRootScopeService = class extends Service {
|
|
|
179
194
|
* treated as evidence.
|
|
180
195
|
*/
|
|
181
196
|
dshHome;
|
|
197
|
+
/**
|
|
198
|
+
* Whether the row carried a `dshHome` value at all — even an invalid one.
|
|
199
|
+
* The registry's protected-boundary set must tell "no home configured"
|
|
200
|
+
* (absent: nothing to protect) from "a home was supplied and rejected"
|
|
201
|
+
* (invalid: the boundary cannot be proven).
|
|
202
|
+
*/
|
|
203
|
+
dshHomeSupplied;
|
|
182
204
|
/** Observe registry publications; consumers still resolve authority from scopeOf(). */
|
|
183
205
|
subscribe(listener) {
|
|
184
206
|
this.listeners.add(listener);
|
|
@@ -216,6 +238,11 @@ var MultiRootScopeService = class extends Service {
|
|
|
216
238
|
}
|
|
217
239
|
for (const listener of this.listeners) listener();
|
|
218
240
|
}
|
|
241
|
+
/** A publication superseded the authorization basis: every outstanding minted profile dies. */
|
|
242
|
+
bumpPublication() {
|
|
243
|
+
this.publicationVersion += 1;
|
|
244
|
+
this.mintedProfiles.length = 0;
|
|
245
|
+
}
|
|
219
246
|
/**
|
|
220
247
|
* The sorted names of the tools `toolAccess` caps — read-only for the
|
|
221
248
|
* prompt contribution. An invalid config is not enumerated (it caps every
|
|
@@ -228,6 +255,20 @@ var MultiRootScopeService = class extends Service {
|
|
|
228
255
|
get toolAccessInvalid() {
|
|
229
256
|
return this.capsInvalid;
|
|
230
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* The validated `dshHome` row config, classified for the registry's
|
|
260
|
+
* protected-boundary set: `absent` when the row carried no value (nothing
|
|
261
|
+
* to protect), `invalid` when it carried a value the constructor rejected
|
|
262
|
+
* (the boundary cannot be proven), `configured` with the accepted absolute
|
|
263
|
+
* path. Read-only — this changes no scope or `protectedHome` semantics.
|
|
264
|
+
*/
|
|
265
|
+
get dshHomeConfig() {
|
|
266
|
+
if (this.dshHome !== void 0) return {
|
|
267
|
+
kind: "configured",
|
|
268
|
+
path: this.dshHome
|
|
269
|
+
};
|
|
270
|
+
return this.dshHomeSupplied ? { kind: "invalid" } : { kind: "absent" };
|
|
271
|
+
}
|
|
231
272
|
constructor(ctx, config = {}) {
|
|
232
273
|
super(ctx, "multiRootScope");
|
|
233
274
|
const toolAccess = config.toolAccess;
|
|
@@ -243,10 +284,44 @@ var MultiRootScopeService = class extends Service {
|
|
|
243
284
|
this.capsInvalid = false;
|
|
244
285
|
}
|
|
245
286
|
const dshHome = config.dshHome;
|
|
287
|
+
this.dshHomeSupplied = dshHome !== void 0;
|
|
246
288
|
if (dshHome !== void 0 && (typeof dshHome !== "string" || dshHome.trim() === "" || !isAbsolute(dshHome))) ctx.logger.error(`multi-root-workspace: invalid dshHome config (expected an absolute path, got ${JSON.stringify(dshHome)}); the DSH-home deny boundary is disabled`);
|
|
247
289
|
else this.dshHome = dshHome;
|
|
248
290
|
aroundTool(ctx, (exec, next) => {
|
|
249
|
-
return
|
|
291
|
+
return this.withToolCap(exec.name, async () => {
|
|
292
|
+
const denial = opsDispatchBarrier(ctx, this, exec);
|
|
293
|
+
if (denial !== void 0) throw new Error(denial);
|
|
294
|
+
return await next();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
ctx.inject([
|
|
298
|
+
"tools",
|
|
299
|
+
"sandboxPolicy",
|
|
300
|
+
"multiRootCompat",
|
|
301
|
+
"multiRootProviders"
|
|
302
|
+
], (scoped) => {
|
|
303
|
+
if (typeof scoped.tools.guard !== "function") return;
|
|
304
|
+
scoped.effect(() => scoped.tools.guard((exec) => opsDispatchBarrier(ctx, this, exec)));
|
|
305
|
+
});
|
|
306
|
+
ctx.inject([
|
|
307
|
+
"subprocess",
|
|
308
|
+
"multiRootCompat",
|
|
309
|
+
"multiRootProviders"
|
|
310
|
+
], (scoped) => {
|
|
311
|
+
scoped.effect(() => {
|
|
312
|
+
let active = true;
|
|
313
|
+
const restore = wrapMethod(scoped.get("subprocess"), "spawn", (original) => {
|
|
314
|
+
const scope = this;
|
|
315
|
+
return function(spec) {
|
|
316
|
+
if (active && opsCompatibilityToken(scoped) !== void 0 && scope.hasActiveDenyRoots() && !opsSpawnPermitted(scope.opsProtection) && !(scope.admissionRequired && scope.admissionPathsValid() && scope.permitsOperationProfile(spec.argv))) throw new Error("multi-root workspace: an unattributed subprocess has no protected execution or live profile admission; refusing launch while OPS compatibility write boundaries apply");
|
|
317
|
+
return original.call(this, spec);
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return () => {
|
|
321
|
+
active = false;
|
|
322
|
+
restore();
|
|
323
|
+
};
|
|
324
|
+
});
|
|
250
325
|
});
|
|
251
326
|
ctx.inject(["systemPrompt", "sandboxPolicy"], (scope) => {
|
|
252
327
|
scope.systemPrompt.context({
|
|
@@ -280,12 +355,74 @@ var MultiRootScopeService = class extends Service {
|
|
|
280
355
|
requireAdmission(proof) {
|
|
281
356
|
this.admissionProof = proof;
|
|
282
357
|
}
|
|
283
|
-
|
|
284
|
-
|
|
358
|
+
/**
|
|
359
|
+
* The stamp an asynchronously completing mint must carry: the publication
|
|
360
|
+
* generation and tool-cap basis at the mint's synchronous entry, before any
|
|
361
|
+
* provider probe could run. An in-flight mint that crosses a real
|
|
362
|
+
* publication keeps its entry generation, so the ledger refuses it rather
|
|
363
|
+
* than re-stamping stale authorization under the current publication.
|
|
364
|
+
* @param form - the profile form the mint will produce (`clamped` and
|
|
365
|
+
* `read-only` are the only ledger-eligible forms).
|
|
366
|
+
*/
|
|
367
|
+
captureMint(form) {
|
|
368
|
+
return {
|
|
369
|
+
version: this.publicationVersion,
|
|
370
|
+
capped: this.capped(),
|
|
371
|
+
form
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Record a confined profile this host's provider just minted. Only
|
|
376
|
+
* meaningful under the shared Registry (`admissionProof` set; without it
|
|
377
|
+
* `permitsProfile` never refuses): a live admission binds the profile to
|
|
378
|
+
* the operation and it dies with it; an expired admission never bequeaths
|
|
379
|
+
* one — a mint that outlived its operation is dropped, not demoted into
|
|
380
|
+
* the ledger; only with NO admission frame at all does the profile join
|
|
381
|
+
* the publication-keyed ledger stamped with `mint`, and only when the
|
|
382
|
+
* mint's form is primary-bounded — an `ambient` mint that arrives without
|
|
383
|
+
* a frame is dropped the same way.
|
|
384
|
+
* @param argv - the confined argv just minted.
|
|
385
|
+
* @param mint - the {@link captureMint} stamp taken at the mint's
|
|
386
|
+
* synchronous entry.
|
|
387
|
+
*/
|
|
388
|
+
rememberProfile(argv, mint) {
|
|
389
|
+
if (this.admissionProof === void 0) return;
|
|
390
|
+
const frame = operationFrameState(this.admissionProof);
|
|
391
|
+
if (frame === "expired") return;
|
|
392
|
+
if (frame === "active") {
|
|
393
|
+
rememberOperationProfile(this.admissionProof, argv, mint.capped);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (mint.form === "ambient") return;
|
|
397
|
+
this.mintedProfiles.push({
|
|
398
|
+
argv: [...argv],
|
|
399
|
+
capped: mint.capped,
|
|
400
|
+
version: mint.version
|
|
401
|
+
});
|
|
402
|
+
if (this.mintedProfiles.length > MAX_MINTED_PROFILES) this.mintedProfiles.shift();
|
|
285
403
|
}
|
|
404
|
+
/**
|
|
405
|
+
* Whether a confined argv may launch now: a profile minted inside this live
|
|
406
|
+
* admission, or — for a `confine`→`spawn` composition with no wrapped entry
|
|
407
|
+
* to admit it — a profile this host minted while its authorization
|
|
408
|
+
* publication is still current. Cached argv, argv minted under a superseded
|
|
409
|
+
* publication, and argv minted elsewhere stay refused.
|
|
410
|
+
*/
|
|
286
411
|
permitsProfile(argv) {
|
|
412
|
+
return this.admissionProof === void 0 || this.permitsOperationProfile(argv) || this.permitsMintedProfile(argv);
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* The strict form: only profiles minted inside this live admission. The OPS
|
|
416
|
+
* execution domain owns its profile exclusively and never honors the
|
|
417
|
+
* unadmitted mint ledger.
|
|
418
|
+
*/
|
|
419
|
+
permitsOperationProfile(argv) {
|
|
287
420
|
return this.admissionProof === void 0 || operationPermitsProfile(this.admissionProof, argv, this.capped());
|
|
288
421
|
}
|
|
422
|
+
permitsMintedProfile(argv) {
|
|
423
|
+
const capped = this.capped(), version = this.publicationVersion;
|
|
424
|
+
return this.mintedProfiles.some((entry) => entry.version === version && entry.capped === capped && entry.argv.length > 0 && entry.argv.length <= argv.length && entry.argv.every((arg, index) => argv[index] === arg));
|
|
425
|
+
}
|
|
289
426
|
get admissionRequired() {
|
|
290
427
|
return this.admissionProof !== void 0;
|
|
291
428
|
}
|
|
@@ -327,16 +464,19 @@ var MultiRootScopeService = class extends Service {
|
|
|
327
464
|
previewForTool(policy, toolName) {
|
|
328
465
|
return this.project(policy, this.capForTool(toolName));
|
|
329
466
|
}
|
|
467
|
+
withToolCap(name, fn) {
|
|
468
|
+
return toolCapStore.run({ capped: this.capForTool(name) }, fn);
|
|
469
|
+
}
|
|
330
470
|
/** Execution and review use exactly the same parent-narrowing rule. */
|
|
331
471
|
capForTool(toolName) {
|
|
332
472
|
return this.capsInvalid || toolCapStore.getStore()?.capped === true || this.caps[toolName] === "read-only";
|
|
333
473
|
}
|
|
334
474
|
project(policy, capped, recorded, authority = this.registryAuthority) {
|
|
335
475
|
const primaryRoot = canonicalPath(policy.workspaceRoot);
|
|
336
|
-
const registered = recorded ?? this.rootsByPrimary.get(
|
|
476
|
+
const registered = recorded ?? this.rootsByPrimary.get(primaryKey(policy.workspaceRoot)) ?? [];
|
|
337
477
|
const roots = authority === "unavailable" ? [] : sanitizeMembers(primaryRoot, registered);
|
|
338
478
|
const writable = capped || authority === "unavailable" ? [] : roots.filter((root) => effectivelyWritable(policy.mode, root.access)).map((root) => root.path);
|
|
339
|
-
const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" &&
|
|
479
|
+
const denied = registered.filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
|
|
340
480
|
const deniedRoots = [...new Set(denied)];
|
|
341
481
|
let protectedHome;
|
|
342
482
|
if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || authority === "unavailable")) {
|
|
@@ -419,10 +559,9 @@ var MultiRootScopeService = class extends Service {
|
|
|
419
559
|
}
|
|
420
560
|
/** The sanitized members of one primary root, in registry order. */
|
|
421
561
|
membersOf(primaryRoot) {
|
|
422
|
-
const
|
|
423
|
-
const registered = this.rootsByPrimary.get(key);
|
|
562
|
+
const registered = this.rootsByPrimary.get(primaryKey(primaryRoot));
|
|
424
563
|
if (registered === void 0) return [];
|
|
425
|
-
return sanitizeMembers(
|
|
564
|
+
return sanitizeMembers(canonicalPath(primaryRoot), registered);
|
|
426
565
|
}
|
|
427
566
|
/**
|
|
428
567
|
* Replace the roots registered for one primary root.
|
|
@@ -430,11 +569,13 @@ var MultiRootScopeService = class extends Service {
|
|
|
430
569
|
* M1 uses this from tests and smoke scripts only; M3 feeds it from plugin
|
|
431
570
|
* storage. It is a plain setter so the transition swaps the data source
|
|
432
571
|
* without touching any provider.
|
|
433
|
-
* @param primaryRoot -
|
|
572
|
+
* @param primaryRoot - primary root the roots belong to; the Registry key
|
|
573
|
+
* is its upstream spelling via {@link primaryKey}, never re-resolved.
|
|
434
574
|
* @param roots - the additional roots, in display order.
|
|
435
575
|
*/
|
|
436
576
|
setAdditionalRoots(primaryRoot, roots) {
|
|
437
|
-
const key =
|
|
577
|
+
const key = primaryKey(primaryRoot);
|
|
578
|
+
if (!sameAdditionalRoots(this.rootsByPrimary.get(key) ?? [], roots)) this.bumpPublication();
|
|
438
579
|
if (roots.length === 0) this.rootsByPrimary.delete(key);
|
|
439
580
|
else this.rootsByPrimary.set(key, [...roots]);
|
|
440
581
|
this.notifySubscribers();
|
|
@@ -454,8 +595,17 @@ var MultiRootScopeService = class extends Service {
|
|
|
454
595
|
const next = available ? "available" : "unavailable";
|
|
455
596
|
if (this.registryAuthority === next) return;
|
|
456
597
|
this.registryAuthority = next;
|
|
598
|
+
this.bumpPublication();
|
|
457
599
|
this.notifySubscribers();
|
|
458
600
|
}
|
|
459
601
|
};
|
|
602
|
+
/** Whether two persisted member lists carry the same authorization basis, in registry order. */
|
|
603
|
+
function sameAdditionalRoots(a, b) {
|
|
604
|
+
if (a.length !== b.length) return false;
|
|
605
|
+
return a.every((root, index) => {
|
|
606
|
+
const next = b[index];
|
|
607
|
+
return next !== void 0 && root.id === next.id && root.path === next.path && root.recordedPath === next.recordedPath && root.access === next.access && root.withheld === next.withheld && root.alias === next.alias;
|
|
608
|
+
});
|
|
609
|
+
}
|
|
460
610
|
//#endregion
|
|
461
611
|
export { MULTI_ROOT_CONTEXT_NAME, MultiRootScopeService, MultiRootScopeService as default, REGISTRY_UNAVAILABLE_NOTE, renderWorkspaceRootsContext, sanitizeAdditionalRoots };
|