@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,12 +1,11 @@
|
|
|
1
1
|
import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
|
|
2
|
-
import { r as canonicalPath
|
|
3
|
-
import {
|
|
2
|
+
import { r as canonicalPath } from "./windows-runner-contract-B5R2K3mM.js";
|
|
3
|
+
import { g as resolveRootRef, i as canonicalRoot, o as entryRootRef, r as availableRoots, t as RootValidationError } from "./roots-CmaWxRVy.js";
|
|
4
4
|
import { c as skillsRequestSchema, i as parsePanelCall, r as effectiveRootAccess, s as SKILLS_ENDPOINTS, t as PANEL_CHANNEL } from "./contract-CStSEOcK.js";
|
|
5
|
-
import { n as AuthorityRpcError } from "./protocol-
|
|
5
|
+
import { n as AuthorityRpcError } from "./protocol-CwDnFEyK.js";
|
|
6
6
|
import { closeSync, lstatSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { isAbsolute, join } from "node:path";
|
|
8
8
|
import "@deepseek-ai/cordis";
|
|
9
|
-
import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
|
|
10
9
|
//#region src/compat/connection-rpc.ts
|
|
11
10
|
/**
|
|
12
11
|
* Newer Cordis keeps a service getter's definition site when tracing it, so
|
|
@@ -29,6 +28,24 @@ function connectionRpcFor(ctx, connection) {
|
|
|
29
28
|
return connection.rpc;
|
|
30
29
|
}
|
|
31
30
|
//#endregion
|
|
31
|
+
//#region src/compat/session-existence.ts
|
|
32
|
+
/**
|
|
33
|
+
* Whether `sessionId` exists in the mounted persistence backend.
|
|
34
|
+
* @param ctx - the plugin fiber context; optional services read via `ctx.get`.
|
|
35
|
+
* @param sessionId - the panel request's session id.
|
|
36
|
+
* @param signal - the request's cancellation, forwarded when the backend takes options.
|
|
37
|
+
* @returns `true` only when a snapshot proves the session exists.
|
|
38
|
+
*/
|
|
39
|
+
async function persistedSessionExists(ctx, sessionId, signal) {
|
|
40
|
+
try {
|
|
41
|
+
const persistence = ctx.get("sessionPersistence");
|
|
42
|
+
if (typeof persistence?.stat !== "function") return false;
|
|
43
|
+
return await persistence.stat(sessionId, signal === void 0 ? void 0 : { signal }) !== void 0;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
32
49
|
//#region src/profile-isolation.ts
|
|
33
50
|
/**
|
|
34
51
|
* Profile pnpm import isolation — the host-side maintenance behind
|
|
@@ -397,7 +414,7 @@ function inspectHome(home) {
|
|
|
397
414
|
throw new Error(`the DSH home ${home} cannot be inspected: ${describeError(error)}`);
|
|
398
415
|
}
|
|
399
416
|
if (stat.isSymbolicLink()) throw new Error(`the DSH home ${home} is a reparse point — refusing to work through it`);
|
|
400
|
-
const canonical = canonicalPath
|
|
417
|
+
const canonical = canonicalPath(home);
|
|
401
418
|
if (!stat.isDirectory()) throw new Error(`the DSH home ${canonical} is not a directory`);
|
|
402
419
|
const scan = {
|
|
403
420
|
directories: 0,
|
|
@@ -649,8 +666,8 @@ function remediationSteps(report) {
|
|
|
649
666
|
//#endregion
|
|
650
667
|
//#region src/command.ts
|
|
651
668
|
/**
|
|
652
|
-
* The user-facing surface: the `/workspace-folders` command
|
|
653
|
-
* RPC channel.
|
|
669
|
+
* The user-facing surface: the `/workspace-folders` command, its `/add-dirs`
|
|
670
|
+
* alias for adding a root, and the panel's RPC channel.
|
|
654
671
|
*
|
|
655
672
|
* They share one module because they are one thing — the operator-facing edge
|
|
656
673
|
* over `ctx.multiRootRegistry`. The command serves the text surfaces (the
|
|
@@ -685,6 +702,8 @@ const inject = [
|
|
|
685
702
|
];
|
|
686
703
|
/** The command name, without the leading slash. */
|
|
687
704
|
const COMMAND_NAME = "workspace-folders";
|
|
705
|
+
/** The shortcut for `/workspace-folders add`, without the leading slash. */
|
|
706
|
+
const ADD_COMMAND_NAME = "add-dirs";
|
|
688
707
|
/**
|
|
689
708
|
* Parse the text that follows `/workspace-folders`.
|
|
690
709
|
* @param rawInput - the verbatim `rawInput` of the command invocation.
|
|
@@ -772,6 +791,15 @@ function apply(ctx, config = {}) {
|
|
|
772
791
|
input: { hint: "[list|add [path] [--inherit|--read-only|--read-write]|remove <n|path>|alias <n|path> [name]|alias-id <n|path> <id>|skills <n|path> on|off|access <n|path> <level>|reveal <n|path>|isolate-profiles [--apply]|help]" },
|
|
773
792
|
handler: async (invocation) => await runCommand(ctx, invocation, config)
|
|
774
793
|
});
|
|
794
|
+
ctx.commands.register({
|
|
795
|
+
name: ADD_COMMAND_NAME,
|
|
796
|
+
description: `Add an additional workspace root (alias for /${COMMAND_NAME} add)`,
|
|
797
|
+
input: { hint: "[absolute path] [--inherit|--read-only|--read-write]" },
|
|
798
|
+
handler: async (invocation) => await runCommand(ctx, {
|
|
799
|
+
...invocation,
|
|
800
|
+
rawInput: `add ${invocation.rawInput}`
|
|
801
|
+
}, config)
|
|
802
|
+
});
|
|
775
803
|
ctx.inject(["connection", "webServer"], (connectionCtx) => {
|
|
776
804
|
const connection = connectionCtx.get("connection");
|
|
777
805
|
if (connection === void 0) {
|
|
@@ -957,6 +985,7 @@ function helpText() {
|
|
|
957
985
|
`/${COMMAND_NAME} — additional workspace roots of this session`,
|
|
958
986
|
` /${COMMAND_NAME} list`,
|
|
959
987
|
` /${COMMAND_NAME} add [absolute path] [--inherit|--read-only|--read-write] (no path opens the directory picker when one is composed)`,
|
|
988
|
+
` /${ADD_COMMAND_NAME} [absolute path] [--inherit|--read-only|--read-write] (alias for /${COMMAND_NAME} add)`,
|
|
960
989
|
` /${COMMAND_NAME} remove <n|path>`,
|
|
961
990
|
` /${COMMAND_NAME} alias <n|path> [name] (no name clears the alias)`,
|
|
962
991
|
` /${COMMAND_NAME} access <n|path> inherit|read-only|read-write`,
|
|
@@ -1092,15 +1121,41 @@ async function targetOf(registry, primaryRoot, text) {
|
|
|
1092
1121
|
*
|
|
1093
1122
|
* @param ctx - the host context that may carry `sessions`.
|
|
1094
1123
|
* @param sessionId - the id the panel sent; already required by the wire schema.
|
|
1095
|
-
* @returns the
|
|
1124
|
+
* @returns the session's cwd spelling, verbatim — the Registry key.
|
|
1096
1125
|
* @throws {RootValidationError} `session-not-found` when the service is absent,
|
|
1097
1126
|
* the id does not name a live session, or that session has no cwd.
|
|
1098
1127
|
*/
|
|
1099
1128
|
function resolvePanelPrimaryRoot(ctx, sessionId) {
|
|
1100
1129
|
const session = ctx.get("sessions")?.get(sessionId);
|
|
1101
1130
|
const cwd = session?.header.cwd;
|
|
1102
|
-
if (session === void 0 || cwd === void 0 || cwd === "") throw new RootValidationError("session-not-found", `session "${sessionId}" is not an active host session`, { reference: sessionId });
|
|
1103
|
-
return
|
|
1131
|
+
if (session === void 0 || cwd === void 0 || cwd === "" || !isAbsolute(cwd)) throw new RootValidationError("session-not-found", `session "${sessionId}" is not an active host session`, { reference: sessionId });
|
|
1132
|
+
return cwd;
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Whether the panel's resolver would now name a primary root for the session —
|
|
1136
|
+
* the exact live-and-usable-cwd predicate, re-run so a resume that landed
|
|
1137
|
+
* between a rejection and its classification is recognized instead of
|
|
1138
|
+
* duplicating the cwd rule.
|
|
1139
|
+
*/
|
|
1140
|
+
function panelSessionResolvable(ctx, sessionId) {
|
|
1141
|
+
try {
|
|
1142
|
+
resolvePanelPrimaryRoot(ctx, sessionId);
|
|
1143
|
+
return true;
|
|
1144
|
+
} catch {
|
|
1145
|
+
return false;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Whether the sessions store names no live session for `sessionId` — or
|
|
1150
|
+
* cannot even answer, in which case "not live" stays unproven and the
|
|
1151
|
+
* caller keeps the code the flow already had.
|
|
1152
|
+
*/
|
|
1153
|
+
function liveSessionAbsent(ctx, sessionId) {
|
|
1154
|
+
try {
|
|
1155
|
+
return ctx.get("sessions")?.get(sessionId) === void 0;
|
|
1156
|
+
} catch {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1104
1159
|
}
|
|
1105
1160
|
/** Project one status into the panel's view. */
|
|
1106
1161
|
function toRootView(status, index) {
|
|
@@ -1183,6 +1238,7 @@ async function rootsViewOf(ctx, registry, primaryRoot, request) {
|
|
|
1183
1238
|
* through the channel.
|
|
1184
1239
|
*/
|
|
1185
1240
|
async function dispatchPanelRequest(ctx, endpoint, payload, signal) {
|
|
1241
|
+
let panelSessionId;
|
|
1186
1242
|
try {
|
|
1187
1243
|
if (SKILLS_ENDPOINTS.includes(endpoint)) {
|
|
1188
1244
|
const parsedSkills = skillsRequestSchema.safeParse(payload);
|
|
@@ -1220,6 +1276,7 @@ async function dispatchPanelRequest(ctx, endpoint, payload, signal) {
|
|
|
1220
1276
|
}
|
|
1221
1277
|
};
|
|
1222
1278
|
const request = parsed.value;
|
|
1279
|
+
panelSessionId = request.sessionId;
|
|
1223
1280
|
const registry = ctx.multiRootRegistry;
|
|
1224
1281
|
const primaryRoot = resolvePanelPrimaryRoot(ctx, request.sessionId);
|
|
1225
1282
|
switch (request.endpoint) {
|
|
@@ -1291,7 +1348,15 @@ async function dispatchPanelRequest(ctx, endpoint, payload, signal) {
|
|
|
1291
1348
|
}
|
|
1292
1349
|
}
|
|
1293
1350
|
} catch (error) {
|
|
1294
|
-
if (error instanceof RootValidationError
|
|
1351
|
+
if (error instanceof RootValidationError) return {
|
|
1352
|
+
ok: false,
|
|
1353
|
+
error: {
|
|
1354
|
+
code: error.code === "session-not-found" && panelSessionId !== void 0 && (panelSessionResolvable(ctx, panelSessionId) || liveSessionAbsent(ctx, panelSessionId) && await persistedSessionExists(ctx, panelSessionId, signal)) ? "session-not-live" : error.code,
|
|
1355
|
+
message: error.message,
|
|
1356
|
+
details: {}
|
|
1357
|
+
}
|
|
1358
|
+
};
|
|
1359
|
+
if (error instanceof AuthorityRpcError) return {
|
|
1295
1360
|
ok: false,
|
|
1296
1361
|
error: {
|
|
1297
1362
|
code: error.code,
|
package/lib/command.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as renderRootsReport, c as revealArgv, i as parseFoldersCommand, n as apply, o as resolvePanelPrimaryRoot, r as inject, s as revealAccepted, t as COMMAND_NAME } from "./command-
|
|
1
|
+
import { a as renderRootsReport, c as revealArgv, i as parseFoldersCommand, n as apply, o as resolvePanelPrimaryRoot, r as inject, s as revealAccepted, t as COMMAND_NAME } from "./command-DaVJ-mK3.js";
|
|
2
2
|
export { COMMAND_NAME, apply, inject, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealAccepted, revealArgv };
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { c as SUPPORTED_DSH_RELEASES, l as classifyInstallation, t as COMPAT_ENFORCEMENT_ENV, u as compatEnforcement } from "./dsh-version-DCwF7REk.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
6
|
+
//#region src/compat/pty-marker.ts
|
|
7
|
+
/**
|
|
8
|
+
* Boot-time probe for the agent-PTY marker contract.
|
|
9
|
+
*
|
|
10
|
+
* `multi-root-process-fence` tells an agent terminal from the user's own by
|
|
11
|
+
* one env fact: every supported `dsh-terminal-bash` backend stamps
|
|
12
|
+
* `DSH_PTY_SESSION_ID` (alongside `DSH_SESSION_ID`) into the PTY spawn spec,
|
|
13
|
+
* while the panel's terminal controller stamps only `DSH_SESSION_ID`. That
|
|
14
|
+
* marker is the ONLY discriminator — a backend that ever stops stamping it
|
|
15
|
+
* would reclassify agent PTYs as user terminals and pass them through
|
|
16
|
+
* unconfined under a restricted `danger-full-access`, which is a silent
|
|
17
|
+
* fail-open on a security boundary.
|
|
18
|
+
*
|
|
19
|
+
* The marker is an env key inside upstream's emitted code, not an exported
|
|
20
|
+
* symbol, so the honest structural probe is content: the installed backend
|
|
21
|
+
* package must still write the marker. This module is called from the
|
|
22
|
+
* `multi-root-compat` gate (`src/compat.ts`) BEFORE the service registers —
|
|
23
|
+
* like the version verdict it is a precondition, not a diagnostic, and a
|
|
24
|
+
* breach refuses the composition the same way `mixed`/`incomplete` do: it is
|
|
25
|
+
* not relaxable by `DSH_MULTI_ROOT_COMPAT=warn`.
|
|
26
|
+
*
|
|
27
|
+
* Resolution follows the optional-peer precedent (`agent-instructions.ts`):
|
|
28
|
+
* the backend is not a peer of this plugin, so the plugin anchor is tried
|
|
29
|
+
* first (flat layouts) and the `@deepseek-ai/dsh` host anchor second — the
|
|
30
|
+
* same host-anchor fallback pattern as `dsh-version.ts` — because the
|
|
31
|
+
* backend rides inside the harness meta package's own dependency tree.
|
|
32
|
+
*
|
|
33
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/pty-marker
|
|
34
|
+
*/
|
|
35
|
+
/** The env key that identifies an agent-launched PTY spawn spec. */
|
|
36
|
+
const AGENT_PTY_MARKER = "DSH_PTY_SESSION_ID";
|
|
37
|
+
/** The upstream package whose backend owns the marker stamp. */
|
|
38
|
+
const TERMINAL_BACKEND = "@deepseek-ai/dsh-terminal-bash";
|
|
39
|
+
/** The harness meta package the backend rides inside (its dependency). */
|
|
40
|
+
const HOST_ANCHOR = "@deepseek-ai/dsh/package.json";
|
|
41
|
+
/** Scan budget: enough for any plausible backend layout, bounded forever. */
|
|
42
|
+
const MAX_SOURCE_FILES = 256;
|
|
43
|
+
const MAX_TOTAL_BYTES = 8388608;
|
|
44
|
+
/**
|
|
45
|
+
* Raised when the installed terminal backend does not uphold the marker
|
|
46
|
+
* contract. Thrown from `apply` like `DshCompatUnsupportedError`, so it
|
|
47
|
+
* surfaces as a plugin load failure with the diagnosis attached.
|
|
48
|
+
*/
|
|
49
|
+
var PtyMarkerContractError = class extends Error {
|
|
50
|
+
/** The resolved backend path when one was found. */
|
|
51
|
+
backend;
|
|
52
|
+
constructor(message, backend) {
|
|
53
|
+
super(message);
|
|
54
|
+
this.name = "PtyMarkerContractError";
|
|
55
|
+
this.backend = backend;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the backend package root through the two anchors the contract can
|
|
60
|
+
* reach: this module first (flat layouts where the backend is a direct
|
|
61
|
+
* install), then the `@deepseek-ai/dsh` host manifest (pnpm-style layouts
|
|
62
|
+
* where it is the meta package's own dependency — the
|
|
63
|
+
* host-anchor fallback pattern in `dsh-version.ts`). A resolution failure that is not a
|
|
64
|
+
* genuine NOT_FOUND propagates: a resolvable-but-broken specifier is not
|
|
65
|
+
* evidence of absence.
|
|
66
|
+
*/
|
|
67
|
+
function resolveBackendRoot() {
|
|
68
|
+
const self = createRequire(import.meta.url);
|
|
69
|
+
const anchors = [self];
|
|
70
|
+
try {
|
|
71
|
+
anchors.push(createRequire(self.resolve(HOST_ANCHOR)));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!isNotFound(error)) throw error;
|
|
74
|
+
}
|
|
75
|
+
for (const anchor of anchors) try {
|
|
76
|
+
return dirname(anchor.resolve(`${TERMINAL_BACKEND}/package.json`));
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!isNotFound(error)) throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function isNotFound(error) {
|
|
82
|
+
const code = error?.code;
|
|
83
|
+
return code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND";
|
|
84
|
+
}
|
|
85
|
+
/** Recursively collect `.js` sources under the package root, within budget. */
|
|
86
|
+
function listSources(root) {
|
|
87
|
+
const files = [];
|
|
88
|
+
const visit = (dir) => {
|
|
89
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
90
|
+
if (files.length >= MAX_SOURCE_FILES) return;
|
|
91
|
+
const path = join(dir, entry.name);
|
|
92
|
+
if (entry.isDirectory() && entry.name !== "node_modules") visit(path);
|
|
93
|
+
else if (entry.isFile() && entry.name.endsWith(".js")) files.push(path);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
visit(root);
|
|
97
|
+
return files;
|
|
98
|
+
}
|
|
99
|
+
const defaultIo = {
|
|
100
|
+
resolveBackendRoot,
|
|
101
|
+
listSources,
|
|
102
|
+
read: (path) => readFileSync(path, "utf8")
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Judge the installed terminal backend against the marker contract.
|
|
106
|
+
*
|
|
107
|
+
* Never throws: every outcome — absent package, unreadable source, missing
|
|
108
|
+
* marker — is folded into the verdict so the caller's reaction is a policy
|
|
109
|
+
* decision in one place.
|
|
110
|
+
* @param io - filesystem seams; tests inject them, production uses real fs.
|
|
111
|
+
* @returns the verdict.
|
|
112
|
+
*/
|
|
113
|
+
function agentPtyMarkerVerdict(io = defaultIo) {
|
|
114
|
+
let root;
|
|
115
|
+
try {
|
|
116
|
+
root = io.resolveBackendRoot();
|
|
117
|
+
} catch {
|
|
118
|
+
return "unverifiable";
|
|
119
|
+
}
|
|
120
|
+
if (root === void 0) return "uninstalled";
|
|
121
|
+
let files;
|
|
122
|
+
try {
|
|
123
|
+
files = io.listSources(root);
|
|
124
|
+
} catch {
|
|
125
|
+
return "unverifiable";
|
|
126
|
+
}
|
|
127
|
+
let remaining = MAX_TOTAL_BYTES;
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
let content;
|
|
130
|
+
try {
|
|
131
|
+
content = io.read(file);
|
|
132
|
+
} catch {
|
|
133
|
+
return "unverifiable";
|
|
134
|
+
}
|
|
135
|
+
if (content.length > remaining) return "unverifiable";
|
|
136
|
+
if (content.includes("DSH_PTY_SESSION_ID")) return "stamped";
|
|
137
|
+
remaining -= content.length;
|
|
138
|
+
}
|
|
139
|
+
return "unverifiable";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Enforce the contract: refuse when the marker cannot be confirmed.
|
|
143
|
+
*
|
|
144
|
+
* @param io - filesystem seams; tests inject them.
|
|
145
|
+
* @throws {PtyMarkerContractError} on `unverifiable`.
|
|
146
|
+
*/
|
|
147
|
+
function assertAgentPtyMarker(io = defaultIo) {
|
|
148
|
+
if (agentPtyMarkerVerdict(io) !== "unverifiable") return;
|
|
149
|
+
let backend;
|
|
150
|
+
try {
|
|
151
|
+
backend = io.resolveBackendRoot();
|
|
152
|
+
} catch {}
|
|
153
|
+
throw new PtyMarkerContractError(`multi-root workspace: the installed ${TERMINAL_BACKEND} does not stamp ${AGENT_PTY_MARKER} on agent PTY spawns, or its sources cannot be verified. That marker is the only fact that separates an agent terminal from the user's own — without it the process fence cannot confine a restricted danger-full-access agent PTY without confining the user's terminal too. The plugin refuses to run rather than let agent terminals pass unconfined. See docs/decisions/ADR-0017-three-state-root-access-and-sandbox-modes.md.`, backend);
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/compat.ts
|
|
157
|
+
/**
|
|
158
|
+
* `multi-root-compat`: the startup gate that turns the DSH compatibility
|
|
159
|
+
* contract from documentation into a precondition.
|
|
160
|
+
*
|
|
161
|
+
* Every widened-authority row of this bundle — `multi-root-providers`,
|
|
162
|
+
* `multi-root-registry`, `multi-root-instructions`, `multi-root-lsp`,
|
|
163
|
+
* `multi-root-workspace-files`, `multi-root-process-fence` — injects
|
|
164
|
+
* `multiRootCompat`. Cordis will not start a plugin whose injected service is
|
|
165
|
+
* absent, so this row is what decides whether they run at all:
|
|
166
|
+
*
|
|
167
|
+
* ```text
|
|
168
|
+
* compat check passes → ctx.multiRootCompat exists → the gated rows start
|
|
169
|
+
* compat check fails → the row throws → they never start
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* The two PROVIDER rows (`multi-root-fs`, `multi-root-sandbox`) deliberately
|
|
173
|
+
* do NOT inject the gate: their widening is scope-driven, and the gated
|
|
174
|
+
* registry can never populate the scope on a refused installation — so they
|
|
175
|
+
* degenerate to the upstream providers. That is what keeps a refused gate a
|
|
176
|
+
* loud degradation (the plugin's features absent, the profile intact) instead
|
|
177
|
+
* of a dead profile: upstream `fs-sandbox`/`sandbox` are already disabled by
|
|
178
|
+
* the patch, so holding `ctx.fs`/`ctx.sandbox` back would take `ctx.shell`,
|
|
179
|
+
* `permissionPresets` and every confining tool down with them.
|
|
180
|
+
*
|
|
181
|
+
* That is why the gate is a plain `apply` and not a `Service` subclass mounted
|
|
182
|
+
* directly: the check has to be able to fail BEFORE the service exists. A
|
|
183
|
+
* `Service` constructor has already run `super(ctx, name)` — and therefore
|
|
184
|
+
* already claimed the service key — by the time it could inspect anything.
|
|
185
|
+
*
|
|
186
|
+
* The verdict itself, and the allowlist it is judged against, live in
|
|
187
|
+
* `./compat/dsh-version.ts`. This module owns only the reaction to it.
|
|
188
|
+
*
|
|
189
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/compat
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* Raised when the installed DSH release is not one this plugin is verified
|
|
193
|
+
* against. It is thrown from `apply`, so it surfaces as a plugin load failure
|
|
194
|
+
* with the full diagnosis attached — never as a silently degraded sandbox.
|
|
195
|
+
*/
|
|
196
|
+
var DshCompatUnsupportedError = class extends Error {
|
|
197
|
+
/** The report that produced the refusal. */
|
|
198
|
+
report;
|
|
199
|
+
constructor(report) {
|
|
200
|
+
super(report.message);
|
|
201
|
+
this.name = "DshCompatUnsupportedError";
|
|
202
|
+
this.report = report;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* The `ctx.multiRootCompat` service: the resolved compatibility verdict, held
|
|
207
|
+
* where the gated providers can read it.
|
|
208
|
+
*
|
|
209
|
+
* It intentionally exposes no per-version capability flags. The adapters under
|
|
210
|
+
* `./compat/` detect the shape they need STRUCTURALLY — whether a return value
|
|
211
|
+
* is a thenable, which export name a module carries — because a structural
|
|
212
|
+
* probe also survives upstream reshaping something within one release, while a
|
|
213
|
+
* version comparison does not. The release string here is for installation and
|
|
214
|
+
* feature-availability gates (such as the OPS 0.2.* policy), and diagnostics.
|
|
215
|
+
*/
|
|
216
|
+
var MultiRootCompatService = class extends Service {
|
|
217
|
+
/** The verdict this composition started under. */
|
|
218
|
+
report;
|
|
219
|
+
constructor(ctx, report) {
|
|
220
|
+
super(ctx, "multiRootCompat");
|
|
221
|
+
this.report = report;
|
|
222
|
+
}
|
|
223
|
+
/** The single DSH release every inspected core package agreed on. */
|
|
224
|
+
get release() {
|
|
225
|
+
return this.report.release;
|
|
226
|
+
}
|
|
227
|
+
/** Whether the installation is on the allowlist. */
|
|
228
|
+
get supported() {
|
|
229
|
+
return this.report.verdict === "supported";
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
/** This row needs nothing: it is the thing everything else waits for. */
|
|
233
|
+
const inject = [];
|
|
234
|
+
/**
|
|
235
|
+
* Apply the gate policy to one report.
|
|
236
|
+
*
|
|
237
|
+
* Under `DSH_MULTI_ROOT_COMPAT=warn` exactly one verdict is relaxed: an
|
|
238
|
+
* `unsupported` release that every inspected package agrees on. That is the
|
|
239
|
+
* only shape the upgrade smoke probes — a coherent tree on a release the
|
|
240
|
+
* allowlist does not name yet, which is how a release earns its place there.
|
|
241
|
+
* `mixed` and `incomplete` describe an installation the contract cannot judge
|
|
242
|
+
* at all, so they are refused in either mode; production deployments leave the
|
|
243
|
+
* variable unset and get the refusal for everything.
|
|
244
|
+
* @param report - the verdict to act on.
|
|
245
|
+
* @param enforcement - how to react to a non-`supported` verdict.
|
|
246
|
+
* @returns the warning to log, or `undefined` when there is nothing to say.
|
|
247
|
+
* @throws {DshCompatUnsupportedError} when refusing to run.
|
|
248
|
+
*/
|
|
249
|
+
function assertSupportedInstallation(report, enforcement) {
|
|
250
|
+
if (report.verdict === "supported") return void 0;
|
|
251
|
+
if (enforcement === "enforce" || report.verdict !== "unsupported") throw new DshCompatUnsupportedError(report);
|
|
252
|
+
return `${report.message}\n\nProceeding anyway because ${COMPAT_ENFORCEMENT_ENV}=warn. This is an upgrade-smoke mode: additional workspace roots are about to be granted through a sandbox profile shape that has not been verified on this release. Supported releases: ${SUPPORTED_DSH_RELEASES.join(", ")}.`;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Judge the installation, then either refuse to provide the service or provide
|
|
256
|
+
* it.
|
|
257
|
+
* @param ctx - the row's context.
|
|
258
|
+
*/
|
|
259
|
+
function apply(ctx) {
|
|
260
|
+
const report = classifyInstallation();
|
|
261
|
+
const warning = assertSupportedInstallation(report, compatEnforcement());
|
|
262
|
+
if (warning !== void 0) ctx.logger.warn(warning);
|
|
263
|
+
assertAgentPtyMarker();
|
|
264
|
+
ctx.plugin(MultiRootCompatService, report);
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
export { inject as a, agentPtyMarkerVerdict as c, assertSupportedInstallation as i, assertAgentPtyMarker as l, MultiRootCompatService as n, AGENT_PTY_MARKER as o, apply as r, PtyMarkerContractError as s, DshCompatUnsupportedError as t };
|
package/lib/compat.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-
|
|
1
|
+
import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-CTJbpCxu.js";
|
|
2
2
|
export { DshCompatUnsupportedError, MultiRootCompatService, apply, assertSupportedInstallation, inject };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as DialectUnrecognizedError } from "./windows-runner-contract-B5R2K3mM.js";
|
|
2
|
-
import { l as isCanonicallyUnder } from "./roots-
|
|
2
|
+
import { l as isCanonicallyUnder } from "./roots-CmaWxRVy.js";
|
|
3
3
|
import { posix } from "node:path";
|
|
4
4
|
//#region src/compat/sandbox-confine.ts
|
|
5
5
|
/** Whether a value is a thenable, i.e. whether the installed `confine` is async. */
|