@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +44 -1
- package/CHANGELOG.md +44 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +57 -0
- 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-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
- 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-Kup4_ZIb.js +747 -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 +1 -1
- 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/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 +7 -2
- 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/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
package/lib/types/contract.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* from endpoint to response type, so the panel cannot silently decode one
|
|
14
14
|
* endpoint's answer as another's. `reveal` answers with
|
|
15
15
|
* {@link RevealedView}; root-management endpoints answer with
|
|
16
|
-
* {@link RootsView}
|
|
16
|
+
* {@link RootsView}.
|
|
17
17
|
* The host builds those answers in `command.ts` and the
|
|
18
18
|
* panel parses them here — one definition, two consumers.
|
|
19
19
|
* - **Runtime validation at the boundary.** The payload arrives from a browser
|
|
@@ -31,21 +31,9 @@
|
|
|
31
31
|
/** The absolute logical channel both halves speak over. */
|
|
32
32
|
export declare const PANEL_CHANNEL = "/multi-root-workspace";
|
|
33
33
|
/** Endpoint names, relative to {@link PANEL_CHANNEL}. */
|
|
34
|
-
export type PanelEndpoint = 'list' | 'add' | 'remove' | 'alias' | 'move' | '
|
|
34
|
+
export type PanelEndpoint = 'list' | 'add' | 'remove' | 'alias' | 'move' | 'setAccess' | 'reveal';
|
|
35
35
|
/** Every endpoint name, for exhaustive iteration in tests and callers. */
|
|
36
36
|
export declare const PANEL_ENDPOINTS: readonly PanelEndpoint[];
|
|
37
|
-
export interface FilesView {
|
|
38
|
-
readonly path: string;
|
|
39
|
-
readonly entries: readonly {
|
|
40
|
-
readonly name: string;
|
|
41
|
-
readonly type: 'file' | 'directory' | 'other';
|
|
42
|
-
}[];
|
|
43
|
-
readonly truncated: boolean;
|
|
44
|
-
}
|
|
45
|
-
export interface FilePreview {
|
|
46
|
-
readonly text: string;
|
|
47
|
-
readonly eof: boolean;
|
|
48
|
-
}
|
|
49
37
|
/**
|
|
50
38
|
* How one registered root currently stands, as the panel renders it. Kept in
|
|
51
39
|
* step with the root vocabulary in `src/roots.ts` deliberately: this module is
|
|
@@ -53,6 +41,17 @@ export interface FilePreview {
|
|
|
53
41
|
* value the two sides share is spelled out rather than imported.
|
|
54
42
|
*/
|
|
55
43
|
export type RootState = 'available' | 'missing' | 'redirected' | 'invalid';
|
|
44
|
+
/**
|
|
45
|
+
* The write authority one registration carries (B9 three-state), spelled out
|
|
46
|
+
* rather than imported: this module is inlined into the browser bundle and
|
|
47
|
+
* must not pull in `node:*`, so the one value the two sides share is
|
|
48
|
+
* duplicated here.
|
|
49
|
+
*/
|
|
50
|
+
export type RootAccess = 'inherit' | 'read-write' | 'read-only';
|
|
51
|
+
export type WorkspaceMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
52
|
+
export type EffectiveRootAccess = 'read-only' | 'read-write' | 'full-access' | 'unavailable';
|
|
53
|
+
/** The shared nine-cell rule; tool dispatch may subsequently narrow writes. */
|
|
54
|
+
export declare function effectiveRootAccess(access: RootAccess, mode: WorkspaceMode): EffectiveRootAccess;
|
|
56
55
|
/** One root as the panel renders it. */
|
|
57
56
|
export interface RootView {
|
|
58
57
|
/** 1-based position in the returned snapshot. */
|
|
@@ -67,6 +66,10 @@ export interface RootView {
|
|
|
67
66
|
readonly addedAt: string;
|
|
68
67
|
/** Whether the directory is writable right now, withheld, or unusable. */
|
|
69
68
|
readonly state: RootState;
|
|
69
|
+
/** The write authority the registration carries. */
|
|
70
|
+
readonly access: RootAccess;
|
|
71
|
+
/** Host-derived session permission, before any per-tool cap. */
|
|
72
|
+
readonly effectiveAccess?: EffectiveRootAccess;
|
|
70
73
|
/** Why the root is not `available`; a code the panel localizes. */
|
|
71
74
|
readonly detail?: string;
|
|
72
75
|
}
|
|
@@ -89,8 +92,18 @@ export interface RootsView {
|
|
|
89
92
|
readonly primaryName?: string;
|
|
90
93
|
/** Execution directory only; never a registry key or an authorization grant. */
|
|
91
94
|
readonly effectiveCwd?: string;
|
|
95
|
+
readonly mode?: WorkspaceMode;
|
|
96
|
+
readonly cappedTools?: readonly string[];
|
|
97
|
+
readonly toolAccessInvalid?: boolean;
|
|
92
98
|
/** The additional roots, in registry order. */
|
|
93
99
|
readonly roots: readonly RootView[];
|
|
100
|
+
/** The host cap; MAX_SAFE_INTEGER means a legacy host reported no limit. */
|
|
101
|
+
readonly limit: number;
|
|
102
|
+
/**
|
|
103
|
+
* How many records are stored, in every state. `count > limit` is a
|
|
104
|
+
* historical over-limit store: everything stays granted, only adds refuse.
|
|
105
|
+
*/
|
|
106
|
+
readonly count: number;
|
|
94
107
|
/** Set when the store itself could not be read; the panel shows this verbatim. */
|
|
95
108
|
readonly unavailable?: string;
|
|
96
109
|
}
|
|
@@ -106,9 +119,8 @@ export interface PanelResponseMap {
|
|
|
106
119
|
readonly remove: RootsView;
|
|
107
120
|
readonly alias: RootsView;
|
|
108
121
|
readonly move: RootsView;
|
|
122
|
+
readonly setAccess: RootsView;
|
|
109
123
|
readonly reveal: RevealedView;
|
|
110
|
-
readonly files: FilesView;
|
|
111
|
-
readonly readFile: FilePreview;
|
|
112
124
|
}
|
|
113
125
|
/**
|
|
114
126
|
* The fields a request may carry. `sessionId` is required on every endpoint:
|
|
@@ -123,10 +135,12 @@ export interface PanelRequest {
|
|
|
123
135
|
readonly entry?: RootEntryView;
|
|
124
136
|
/** Compatibility reference; accepted only when the id is unique. */
|
|
125
137
|
readonly id?: string;
|
|
126
|
-
/** Candidate directory (add)
|
|
138
|
+
/** Candidate directory (add). */
|
|
127
139
|
readonly path?: string;
|
|
128
140
|
/** Display alias (add/alias); empty clears it. */
|
|
129
141
|
readonly alias?: string;
|
|
142
|
+
/** Write authority (add — defaults to `inherit` — and setAccess). */
|
|
143
|
+
readonly access?: RootAccess;
|
|
130
144
|
/** Exact anchor row (move): the target is placed in front of it; absent moves it last. */
|
|
131
145
|
readonly beforeEntry?: RootEntryView;
|
|
132
146
|
/** Compatibility anchor; accepted only when the id is unique. */
|
|
@@ -152,8 +166,6 @@ export type Parsed<T> = {
|
|
|
152
166
|
readonly ok: false;
|
|
153
167
|
readonly message: string;
|
|
154
168
|
};
|
|
155
|
-
export declare function parseFilesView(value: unknown): Parsed<FilesView>;
|
|
156
|
-
export declare function parseFilePreview(value: unknown): Parsed<FilePreview>;
|
|
157
169
|
/**
|
|
158
170
|
* Validate the payload the host received for one endpoint.
|
|
159
171
|
* @param endpoint - the endpoint the channel dispatched (the RPC method).
|
package/lib/types/dialects.d.ts
CHANGED
|
@@ -35,16 +35,20 @@
|
|
|
35
35
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/dialects
|
|
36
36
|
*/
|
|
37
37
|
import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
38
|
+
import { DialectUnrecognizedError } from './windows-runner-contract.ts';
|
|
38
39
|
/** The kernel runner dialects a confined argv can speak. */
|
|
39
40
|
export type RunnerDialect = 'seatbelt' | 'bwrap' | 'landlock' | 'windows-acl';
|
|
40
41
|
/**
|
|
41
42
|
* Raised when a confined argv cannot be recognized or widened. The provider
|
|
42
43
|
* converts this into the fail-closed `SandboxUnavailableError`: an unrecognized
|
|
43
44
|
* dialect must never degrade into "the primary root was granted after all".
|
|
45
|
+
*
|
|
46
|
+
* The class itself lives in `windows-runner-contract.ts`: the standalone
|
|
47
|
+
* Windows runner's module graph may import node builtins only, so the shared
|
|
48
|
+
* error type cannot sit in a module that (even type-only) references upstream
|
|
49
|
+
* packages. Re-exported here unchanged — `instanceof` identity is preserved.
|
|
44
50
|
*/
|
|
45
|
-
export
|
|
46
|
-
constructor(message: string);
|
|
47
|
-
}
|
|
51
|
+
export { DialectUnrecognizedError };
|
|
48
52
|
/** One wrapped argv split into its runner/profile part and the caller's argv. */
|
|
49
53
|
export interface ConfinedShape {
|
|
50
54
|
/** The recognized dialect. */
|
|
@@ -90,3 +94,95 @@ export declare function detectDialect(profileArgs: readonly string[]): RunnerDia
|
|
|
90
94
|
* @returns the widened profile arguments.
|
|
91
95
|
*/
|
|
92
96
|
export declare function widenProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], policy: SandboxPolicy, additionalRoots: readonly string[]): string[];
|
|
97
|
+
/**
|
|
98
|
+
* Graft writable grants onto a `read-only` profile — the B9 cell where a fixed
|
|
99
|
+
* `read-write` additional root stays writable while the session mode denies
|
|
100
|
+
* every other write. Each dialect appends its own grant spelling AFTER
|
|
101
|
+
* upstream's deny-all baseline, so the appended form wins:
|
|
102
|
+
*
|
|
103
|
+
* - seatbelt: SBPL is last-match-wins — a trailing `(allow file-write*
|
|
104
|
+
* (subpath …))` overrides the blanket `(deny file-write*)` for those roots;
|
|
105
|
+
* - bwrap: mounts apply in order — a `--bind W W` after `--ro-bind / /`
|
|
106
|
+
* re-mounts that subtree writable inside the read-only namespace;
|
|
107
|
+
* - landlock: grants union — an extra `--rw W` widens exactly that subtree.
|
|
108
|
+
*
|
|
109
|
+
* `windows-acl` cannot be spelled this way and goes through
|
|
110
|
+
* `widenWindowsProfile` (the standalone runner grants only the listed roots).
|
|
111
|
+
* @param dialect - the recognized dialect.
|
|
112
|
+
* @param profileArgs - the upstream read-only profile arguments.
|
|
113
|
+
* @param additionalRoots - canonical roots the effective scope keeps writable.
|
|
114
|
+
* @returns the grafted profile arguments.
|
|
115
|
+
*/
|
|
116
|
+
export declare function graftProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], additionalRoots: readonly string[]): string[];
|
|
117
|
+
/**
|
|
118
|
+
* Deny `deniedRoots` inside an upstream `workspace-write` profile — the B9 edge
|
|
119
|
+
* where an explicit `read-only` (or tool-capped) additional root sits under a
|
|
120
|
+
* directory the profile grants writable (the platform temp area, which an
|
|
121
|
+
* additional root may legitimately be registered beneath).
|
|
122
|
+
*
|
|
123
|
+
* A denied root outside every grant needs no profile work at all: the mode
|
|
124
|
+
* already denies it. Per dialect:
|
|
125
|
+
*
|
|
126
|
+
* - seatbelt appends a trailing `(deny file-write* (subpath …) (literal …))`
|
|
127
|
+
* form — last match wins over every earlier allow, and a redundant deny on
|
|
128
|
+
* an ungranted root is harmless. The literals pin every ancestor of a denied
|
|
129
|
+
* root: Seatbelt checks the operated path only, so `rename`ing an ancestor
|
|
130
|
+
* would otherwise move the whole tree out from under its `subpath` deny;
|
|
131
|
+
* - bwrap re-mounts the root `--ro-bind` AFTER the writable mounts (a later
|
|
132
|
+
* mount wins inside the namespace). The source is mandatory: if a denied
|
|
133
|
+
* root disappears, namespace setup fails before the command can recreate it.
|
|
134
|
+
* This also restores read access to denied roots under a `--tmpfs` target.
|
|
135
|
+
* Ancestors of a denied root that lie strictly inside a writable BIND target
|
|
136
|
+
* are self-bound (`--bind A A`) BEFORE the ro-binds: the kernel refuses to
|
|
137
|
+
* rename a mountpoint, so the deny can no longer be walked off by renaming
|
|
138
|
+
* the tree's parent. Ancestors inside a `--tmpfs` target are never bound —
|
|
139
|
+
* a rename there only moves namespace-private mounts, while the self-bind
|
|
140
|
+
* would expose and make writable the host content the tmpfs hides — and
|
|
141
|
+
* ancestors outside every writable target are not renamable anyway;
|
|
142
|
+
* - landlock cannot subtract: a denied root under an `--rw` grant throws —
|
|
143
|
+
* union semantics have no deny primitive;
|
|
144
|
+
* - windows-acl goes through `widenWindowsProfile`, whose runner-side ACL
|
|
145
|
+
* grant cannot subtract either — its caller checks the same overlap. The
|
|
146
|
+
* deny ACE does follow its directory object through a parent rename, but
|
|
147
|
+
* the moved tree's NEW path no longer matches the recorded deny set — so
|
|
148
|
+
* the ambient deny runner pins every ancestor of each deny root with a
|
|
149
|
+
* non-delete-sharing handle for the child lifetime (`windows-path-guard.ts`,
|
|
150
|
+
* `windows-deny.ts`).
|
|
151
|
+
* @param dialect - the recognized dialect.
|
|
152
|
+
* @param profileArgs - the upstream workspace-write profile arguments.
|
|
153
|
+
* @param deniedRoots - canonical roots this call must not write.
|
|
154
|
+
* @returns the profile arguments with the in-profile denies applied.
|
|
155
|
+
*/
|
|
156
|
+
export declare function denyProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], deniedRoots: readonly string[]): string[];
|
|
157
|
+
/**
|
|
158
|
+
* Restrict an ambient-writable execution to everything EXCEPT `deniedRoots` —
|
|
159
|
+
* the B9 cell `danger-full-access` + explicit `read-only`. The consumers of
|
|
160
|
+
* this profile (shell, PTY, PTC) bypass `confine()` entirely under that mode,
|
|
161
|
+
* so the process fence builds this profile from an upstream skeleton instead:
|
|
162
|
+
* the caller probes `confine` once under `read-only` to obtain the runner's
|
|
163
|
+
* argument prefix, and this function rewrites the file-effect part.
|
|
164
|
+
*
|
|
165
|
+
* - seatbelt: `(version 1) (allow default)` kept from the profile head, plus a
|
|
166
|
+
* trailing `(deny file-write* (subpath …) (literal …))` — allow-default with
|
|
167
|
+
* subpath carve-outs, the inverse of the confined profiles. The literals
|
|
168
|
+
* pin every ancestor of a denied root, so `rename`ing the tree's parent
|
|
169
|
+
* cannot walk it out from under the deny (Seatbelt checks the operated
|
|
170
|
+
* path only — verified natively on macOS);
|
|
171
|
+
* - bwrap: `--ro-bind / /` becomes `--bind / /` (ambient writable namespace),
|
|
172
|
+
* each denied root is re-mounted `--ro-bind` after it, and every ancestor
|
|
173
|
+
* of a denied root is self-bound `--bind A A` between the root mount and the
|
|
174
|
+
* ro-binds — a mountpoint cannot be renamed (EBUSY), so the deny follows no
|
|
175
|
+
* rename of the tree's ancestors. A missing source fails the launch,
|
|
176
|
+
* retaining the registered negative authorization;
|
|
177
|
+
* - landlock cannot express "everything except" — its launcher only unions
|
|
178
|
+
* allowlists, so it throws. windows-acl expresses the cell through the
|
|
179
|
+
* standalone ambient-deny runner instead of this profile rewrite: standing
|
|
180
|
+
* deny ACEs on every denied object, plus ancestor pin handles held through
|
|
181
|
+
* the child and its drained Job descendants so no rename can vacate the
|
|
182
|
+
* registered spelling (`windows-deny.ts`, `windows-path-guard.ts`).
|
|
183
|
+
* @param dialect - the recognized dialect.
|
|
184
|
+
* @param profileArgs - the upstream read-only skeleton's profile arguments.
|
|
185
|
+
* @param deniedRoots - canonical roots the execution must not write.
|
|
186
|
+
* @returns the restricted profile arguments.
|
|
187
|
+
*/
|
|
188
|
+
export declare function restrictProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], deniedRoots: readonly string[]): string[];
|
package/lib/types/fs.d.ts
CHANGED
|
@@ -65,22 +65,44 @@ export declare class MultiRootFileSystem extends LocalFileSystem {
|
|
|
65
65
|
version: FsVersion;
|
|
66
66
|
}, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
|
|
67
67
|
/**
|
|
68
|
-
* The writable roots of one
|
|
69
|
-
*
|
|
70
|
-
* widened by the scope's
|
|
68
|
+
* The writable roots of one confined call: the upstream derivation (primary
|
|
69
|
+
* root plus the platform temp areas, canonical and deduplicated — empty
|
|
70
|
+
* under `read-only`) widened by the scope's EFFECTIVE writable members.
|
|
71
|
+
* Under `read-only` that is exactly the fixed `read-write` roots (the tool
|
|
72
|
+
* cap may already have emptied the scope's list), so an explicit grant
|
|
73
|
+
* survives a globally read-only session. An unavailable registry authority
|
|
74
|
+
* never contributes members: a process that cannot read the store grants
|
|
75
|
+
* only the primary root (and no shared temp grant).
|
|
71
76
|
* @param policy - the per-call policy.
|
|
72
77
|
* @param scope - the scope resolved once for this call; both the fence and
|
|
73
78
|
* the denial message below share it instead of re-resolving.
|
|
74
|
-
* @returns the canonical writable roots; empty
|
|
79
|
+
* @returns the canonical writable roots; possibly empty.
|
|
75
80
|
*/
|
|
76
81
|
private rootsFor;
|
|
77
82
|
/**
|
|
78
|
-
* Enforce the per-call policy against `target` and return the EXACT target
|
|
79
|
-
* mutation must use, so the checked identity is the mutated one (no
|
|
80
|
-
* check-here-write-there TOCTOU).
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
83
|
+
* Enforce the per-call policy against `target` and return the EXACT target
|
|
84
|
+
* the mutation must use, so the checked identity is the mutated one (no
|
|
85
|
+
* check-here-write-there TOCTOU). Deny precedes containment: a root in
|
|
86
|
+
* `scope.deniedRoots` — an explicit `read-only` root, or any member when the
|
|
87
|
+
* call is tool-capped — refuses writes under EVERY mode, including
|
|
88
|
+
* `danger-full-access` and where a broader grant (the platform temp area)
|
|
89
|
+
* would otherwise cover the path. `read-only` then grants only the fixed
|
|
90
|
+
* `read-write` members; `workspace-write` requires containment under the
|
|
91
|
+
* widened writable set.
|
|
92
|
+
*
|
|
93
|
+
* `danger-full-access` is the only mode with a fast path, and only while
|
|
94
|
+
* the deny set is empty: it then returns the caller's target untouched,
|
|
95
|
+
* upstream-exact. Once ANY deny boundary exists the caller's `FsTarget` may
|
|
96
|
+
* be stale — the directory it resolved through can since have been replaced
|
|
97
|
+
* by a symlink into a denied root — so the fence re-resolves
|
|
98
|
+
* `target.displayPath`, checks the FRESH target key, and mutates that fresh
|
|
99
|
+
* target rather than the stale one the caller resolved earlier.
|
|
100
|
+
*
|
|
101
|
+
* One more Full-access rule: while the registry authority is `unavailable`
|
|
102
|
+
* the empty deny set is not trustworthy — the store this process cannot
|
|
103
|
+
* read may hold explicit `read-only` registrations — so the mode degrades
|
|
104
|
+
* to primary-root-only writes (no shared temp or additional-root grants)
|
|
105
|
+
* until the authority returns.
|
|
84
106
|
* @param target - the resolved target about to be mutated.
|
|
85
107
|
* @param sandboxPolicy - the per-call policy; omit for the deployment fallback.
|
|
86
108
|
* @returns the fresh canonical target the mutation must use.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -25,12 +25,16 @@ export { classifyInstallation, compatEnforcement, CORE_PACKAGES, OPTIONAL_CORE_P
|
|
|
25
25
|
export type { CompatEnforcement, CompatReport, CompatVerdict, InspectedPackage, SupportedDshRelease, VersionReader, } from './compat/dsh-version.ts';
|
|
26
26
|
export { instructionsApi, renderInstructions } from './compat/agent-instructions.ts';
|
|
27
27
|
export type { InstructionsApi, RenderedInstructions } from './compat/agent-instructions.ts';
|
|
28
|
+
export { AGENT_PTY_MARKER, agentPtyMarkerVerdict, assertAgentPtyMarker, PtyMarkerContractError } from './compat/pty-marker.ts';
|
|
29
|
+
export type { AgentPtyMarkerIo, AgentPtyMarkerVerdict } from './compat/pty-marker.ts';
|
|
28
30
|
export { createInstructionMessage } from './compat/llm-message.ts';
|
|
29
31
|
export type { InstructionMessageInput } from './compat/llm-message.ts';
|
|
30
32
|
export { widenConfined } from './compat/sandbox-confine.ts';
|
|
31
33
|
export type { ConfineCall, UpstreamConfined } from './compat/sandbox-confine.ts';
|
|
32
34
|
export { composeInstructionMessage, DEFAULT_MAX_BYTES, PLUGIN_SOURCE } from './instructions.ts';
|
|
33
35
|
export type { Config as InstructionsConfig } from './instructions.ts';
|
|
36
|
+
export { MultiRootProviders, brandMultiRootProvider, isMultiRootProvider, MULTI_ROOT_PROVIDER_BRAND } from './providers.ts';
|
|
37
|
+
export type { ProviderKind } from './providers.ts';
|
|
34
38
|
export { MultiRootFileSystem } from './fs.ts';
|
|
35
39
|
export type { Config as FileSystemConfig } from './fs.ts';
|
|
36
40
|
export { MultiRootSandboxProvider } from './sandbox.ts';
|
|
@@ -38,14 +42,15 @@ export type { Config as SandboxConfig } from './sandbox.ts';
|
|
|
38
42
|
export { MultiRootScopeService, sanitizeAdditionalRoots, renderWorkspaceRootsContext } from './scope.ts';
|
|
39
43
|
export type { AdditionalWorkspaceRoot, FilesystemScope } from './scope.ts';
|
|
40
44
|
export { MultiRootRegistry, DOMAIN_NAME, MAX_ALIAS_LENGTH, REGISTRY_CONTENDED_MESSAGE, multiRootDomainSpec } from './registry.ts';
|
|
45
|
+
export { DEFAULT_MAX_ADDITIONAL_ROOTS, MAX_ADDITIONAL_ROOTS_CEILING } from './registry.ts';
|
|
41
46
|
export type { AddRootInput, Config as RegistryConfig, PersistedPrimaryRoot, RegistryAuthorityState } from './registry.ts';
|
|
42
47
|
export { RegistryAuthorityLease, RegistryLeaseContendedError } from './registry-lease.ts';
|
|
43
48
|
export { availableRoots, canonicalRoot, classifyStoredRoots, expandRootInput, indexedStatuses, isCanonicallyUnder, additionalRootId, removeStatusAt, resolveRootRef, RootValidationError, validateRootCandidate, } from './roots.ts';
|
|
44
49
|
export type { AdditionalRootId, RegisteredRoot, RootRef, RootState, RootStatus, RootValidationCode, } from './roots.ts';
|
|
45
50
|
export { COMMAND_NAME, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealArgv } from './command.ts';
|
|
46
51
|
export type { FoldersCommand } from './command.ts';
|
|
47
|
-
export { PANEL_CHANNEL, PANEL_ENDPOINTS, parsePanelCall, parseRevealedView, parseRootsView
|
|
48
|
-
export type {
|
|
52
|
+
export { PANEL_CHANNEL, PANEL_ENDPOINTS, parsePanelCall, parseRevealedView, parseRootsView } from './contract.ts';
|
|
53
|
+
export type { PanelCall, PanelEndpoint, PanelFailure, PanelRequest, PanelResponseMap, RevealedView, RootView, RootsView, } from './contract.ts';
|
|
49
54
|
/**
|
|
50
55
|
* Carrier plugin face for the bare-package-name loader row (see the module
|
|
51
56
|
* doc). It deliberately provides nothing: every host responsibility is owned
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `multi-root-process-fence`: the B9 seam for the one cell `ctx.sandbox.confine`
|
|
3
|
+
* never sees — `danger-full-access` with explicit `read-only` additional roots.
|
|
4
|
+
*
|
|
5
|
+
* Upstream, every confined consumer routes through `ctx.sandbox.confine`, which
|
|
6
|
+
* this bundle's provider already widens (grants, the read-only fixed-write
|
|
7
|
+
* graft, and the workspace-write deny edge). But the shell executor, the PTC
|
|
8
|
+
* runtime and the PTY backend all short-circuit that mode: `policy.mode ===
|
|
9
|
+
* 'danger-full-access'` returns the bare argv, so an explicit `read-only` root
|
|
10
|
+
* would silently stay writable. This row closes the gap without touching any
|
|
11
|
+
* upstream executor:
|
|
12
|
+
*
|
|
13
|
+
* - `ctx.subprocess.spawn` is wrapped to prepend a precomputed restricted
|
|
14
|
+
* profile whenever the AsyncLocalStorage below carries one. `spawn` is
|
|
15
|
+
* synchronous, so the profile — which never depends on the callee's argv —
|
|
16
|
+
* is built by the wrapping consumer, not here.
|
|
17
|
+
* - `ctx.shell.execute` (bash and pwsh alike, and `start` where an older
|
|
18
|
+
* release still exposes it — `compat/process-entrypoints.ts` owns which
|
|
19
|
+
* entries a mounted executor actually carries) resolves the per-call policy
|
|
20
|
+
* exactly the way the executor itself does, and — under Full-access with
|
|
21
|
+
* deny roots — asks the sandbox provider for the restricted prefix and runs
|
|
22
|
+
* the call inside the store, so every subprocess the executor launches is
|
|
23
|
+
* confined.
|
|
24
|
+
* - `ctx.ptcRuntime.run` does the same for `run_code`.
|
|
25
|
+
* - `ctx.subprocess.spawnTerminal` keys on the `DSH_PTY_SESSION_ID` marker:
|
|
26
|
+
* every supported `dsh-terminal-bash` backend stamps it (alongside
|
|
27
|
+
* `DSH_SESSION_ID`) when it launches an agent's PTY, and no other caller —
|
|
28
|
+
* above all the panel's terminal controller — ever sets it. A spec without
|
|
29
|
+
* the marker is a USER terminal, passed through untouched in every mode:
|
|
30
|
+
* the sandbox protects agent behavior, and upstream never confines the
|
|
31
|
+
* user's own terminal. Because that marker is the ONLY discriminator, the
|
|
32
|
+
* `multi-root-compat` gate asserts it at boot (`compat/pty-marker.ts`):
|
|
33
|
+
* an installed backend whose emitted code no longer stamps it fails the
|
|
34
|
+
* plugin load rather than silently reclassifying agent PTYs as the user's.
|
|
35
|
+
* `ctx.terminals` itself is deliberately NOT wrapped:
|
|
36
|
+
* upstream mounts it inside `optional-persistent-terminals` with
|
|
37
|
+
* `isolate: { terminals: true }`, so the registry entry is unreachable from
|
|
38
|
+
* this row — the shared `spawnTerminal` seam plus the env discriminator
|
|
39
|
+
* covers both the un-isolated SDK mount and the isolated web mount.
|
|
40
|
+
*
|
|
41
|
+
* The same seam carries the unknown-authority downgrade: while the scope
|
|
42
|
+
* reports `authority: 'unavailable'` — a contended, storage-failed or
|
|
43
|
+
* config-invalid registry publishes an empty deny set that is NOT evidence —
|
|
44
|
+
* `danger-full-access` executions run inside a primary-root-only
|
|
45
|
+
* profile (with the deny set applied) instead of ambient.
|
|
46
|
+
*
|
|
47
|
+
* Windows uses the standalone ambient deny-token runner. Landlock cannot subtract a subtree, so that combination selects bubblewrap.
|
|
48
|
+
* An unusable mount runner fails without starting an unconfined process.
|
|
49
|
+
*
|
|
50
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/process-fence
|
|
51
|
+
*/
|
|
52
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
53
|
+
export declare const inject: string[];
|
|
54
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile pnpm import isolation — the host-side maintenance behind
|
|
3
|
+
* `/workspace-folders isolate-profiles` (branch review Phase C2, finding R4).
|
|
4
|
+
*
|
|
5
|
+
* Why it exists: a Windows deny ACE applies to the file OBJECT, not to one
|
|
6
|
+
* hardlink name. The pnpm profile defaults upstream writes (`initProfile` in
|
|
7
|
+
* dsh-app-boot) never set `packageImportMethod`, so `dsh plugin add` imports
|
|
8
|
+
* the store into `<home>/profiles/<name>/node_modules` BY HARDLINK. Denying
|
|
9
|
+
* `$DSH_HOME` then rewrites security descriptors shared with every other name
|
|
10
|
+
* of the same objects — including the global pnpm store and other projects
|
|
11
|
+
* linking it. The C1 preflight therefore refuses such a home before any ACL
|
|
12
|
+
* write; this module is the remediation it points at: it detects the confirmed
|
|
13
|
+
* profiles, writes `packageImportMethod: copy` conservatively, and scans the
|
|
14
|
+
* whole home for the multi-link files that still need a reinstall.
|
|
15
|
+
*
|
|
16
|
+
* Boundaries of the subset it touches:
|
|
17
|
+
* - profile identification is conservative — only a directory under
|
|
18
|
+
* `profiles/` whose `package.json` parses to an object carrying
|
|
19
|
+
* `dsh.profile` as an object counts; everything else is enumerated but
|
|
20
|
+
* never written;
|
|
21
|
+
* - the YAML handling is a strict SUBSET scanner, not a parser substitute —
|
|
22
|
+
* the runtime cannot depend on a YAML package, so anything ambiguous is
|
|
23
|
+
* reported as a conflict and refused instead of guessed;
|
|
24
|
+
* - every write is atomic (temp sibling + rename) and never crosses a reparse
|
|
25
|
+
* point;
|
|
26
|
+
* - the whole-home scan never descends a reparse point: with plain `lstat` a
|
|
27
|
+
* symlink/junction reports as a link, and other reparse flavours still fall
|
|
28
|
+
* under the runner's own Win32-attribute preflight, which remains the
|
|
29
|
+
* authoritative gate;
|
|
30
|
+
* - the command sets configuration — it never deletes `node_modules`, never
|
|
31
|
+
* runs pnpm, and never repairs ACLs or ownership written in the past.
|
|
32
|
+
*
|
|
33
|
+
* @module profile-isolation
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Mirrors `PROFILE_PNPM_WORKSPACE` written by `initProfile` in
|
|
37
|
+
* `@deepseek-ai/dsh-app-boot`; that constant is not exported, so the template
|
|
38
|
+
* is duplicated here and must stay in sync with upstream.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PROFILE_PNPM_WORKSPACE = "packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n";
|
|
41
|
+
/** One accepted top-level `key: value` line, located in the source text. */
|
|
42
|
+
export interface WorkspaceYamlEntry {
|
|
43
|
+
/** The unquoted scalar when the value is one; `undefined` for block/complex. */
|
|
44
|
+
readonly value: string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* `scalar` — a same-line plain or quoted scalar; `block` — the value is
|
|
47
|
+
* empty, so a nested block (or an explicit comment) follows; `complex` —
|
|
48
|
+
* the value opens with a YAML node property or collection marker
|
|
49
|
+
* (`&`, `*`, `!`, `|`, `>`, `{`, `[`) or is a malformed scalar the subset
|
|
50
|
+
* refuses to interpret.
|
|
51
|
+
*/
|
|
52
|
+
readonly kind: 'scalar' | 'block' | 'complex';
|
|
53
|
+
/** 1-based line number the key sits on. */
|
|
54
|
+
readonly line: number;
|
|
55
|
+
/** UTF-16 code-unit offset where the value token begins. */
|
|
56
|
+
readonly valueStart: number;
|
|
57
|
+
/** UTF-16 code-unit offset just past the value token (before ` #comment` or EOL). */
|
|
58
|
+
readonly valueEnd: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read the top-level `key: value` lines of a `pnpm-workspace.yaml` text.
|
|
62
|
+
*
|
|
63
|
+
* This is a strict subset scanner, deliberately NOT a YAML parser: it accepts
|
|
64
|
+
* only top-level `key:` lines (plain or quoted keys) plus indented nested
|
|
65
|
+
* content it never interprets, and throws — naming the file and the 1-based
|
|
66
|
+
* line — on an indented first entry, tabs in indentation, `%` / `---` / `...` directives, duplicate
|
|
67
|
+
* top-level keys, or any other non-indented content line outside the key
|
|
68
|
+
* form. A `#` preceded by whitespace ends a plain scalar; quoted scalars are
|
|
69
|
+
* unquoted for the reported value. `valueStart`/`valueEnd` are the UTF-16
|
|
70
|
+
* offsets of the value token itself, so a caller can replace exactly the
|
|
71
|
+
* scalar while comments and line endings survive untouched.
|
|
72
|
+
*
|
|
73
|
+
* @param text - the yaml text to scan.
|
|
74
|
+
* @param path - the file name used in rejection messages.
|
|
75
|
+
* @returns the top-level entries keyed by their unquoted names.
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseWorkspaceYamlKeys(text: string, path?: string): ReadonlyMap<string, WorkspaceYamlEntry>;
|
|
78
|
+
/** What one confirmed profile's `pnpm-workspace.yaml` says about imports. */
|
|
79
|
+
export interface ProfileYamlReport {
|
|
80
|
+
/**
|
|
81
|
+
* `absent` — `packageImportMethod` is not configured (the file may still
|
|
82
|
+
* exist); `copy` — it is a same-line scalar `copy`; `conflict` — it exists
|
|
83
|
+
* but is not a plain `copy` scalar, or the yaml exists but is a reparse
|
|
84
|
+
* point / non-file; `unreadable` — the file cannot be read or the subset
|
|
85
|
+
* scanner rejected it.
|
|
86
|
+
*/
|
|
87
|
+
readonly state: 'absent' | 'copy' | 'conflict' | 'unreadable';
|
|
88
|
+
/** Human-readable reason for `conflict`/`unreadable`. */
|
|
89
|
+
readonly detail?: string;
|
|
90
|
+
}
|
|
91
|
+
/** One confirmed profile and its yaml verdict. */
|
|
92
|
+
export interface ProfileReport {
|
|
93
|
+
/** The profile directory name under `profiles/`. */
|
|
94
|
+
readonly name: string;
|
|
95
|
+
/** The profile's absolute path under the canonical home. */
|
|
96
|
+
readonly path: string;
|
|
97
|
+
/** The `packageImportMethod` verdict for its workspace yaml. */
|
|
98
|
+
readonly yaml: ProfileYamlReport;
|
|
99
|
+
}
|
|
100
|
+
/** The read-only whole-home scan accompanying every report. */
|
|
101
|
+
export interface HomeScan {
|
|
102
|
+
/** Directories successfully listed (including the home itself). */
|
|
103
|
+
readonly directories: number;
|
|
104
|
+
/** Non-directory entries inspected — plain files and reparse leaves alike. */
|
|
105
|
+
readonly files: number;
|
|
106
|
+
/** Plain files carrying more than one name (`st_nlink > 1`). */
|
|
107
|
+
readonly multiLinkTotal: number;
|
|
108
|
+
/** Up to {@link SAMPLE_CAP} absolute paths of multi-link files. */
|
|
109
|
+
readonly multiLinkPaths: readonly string[];
|
|
110
|
+
/** Entries `lstat`/`readdir` failed on (first {@link SAMPLE_CAP}). */
|
|
111
|
+
readonly uninspectable: readonly {
|
|
112
|
+
path: string;
|
|
113
|
+
reason: string;
|
|
114
|
+
}[];
|
|
115
|
+
/** Total uninspectable entries — may exceed the retained paths. */
|
|
116
|
+
readonly uninspectableTotal: number;
|
|
117
|
+
}
|
|
118
|
+
/** The read-only verdict for one canonical home. */
|
|
119
|
+
export interface IsolateReport {
|
|
120
|
+
/** The canonical DSH home the report covers. */
|
|
121
|
+
readonly home: string;
|
|
122
|
+
/** Confirmed profiles only; non-profile entries are listed separately. */
|
|
123
|
+
readonly profiles: readonly ProfileReport[];
|
|
124
|
+
/** `profiles/` entries that are not confirmed profiles — never written. */
|
|
125
|
+
readonly nonProfileEntries: readonly string[];
|
|
126
|
+
/** The whole-home link/uninspectable counts. */
|
|
127
|
+
readonly scan: HomeScan;
|
|
128
|
+
/** True whenever anything could not be inspected — never a clean result. */
|
|
129
|
+
readonly incomplete: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** The per-profile outcome of one `--apply` run. */
|
|
132
|
+
export interface ProfileAction {
|
|
133
|
+
readonly name: string;
|
|
134
|
+
/**
|
|
135
|
+
* `written` — the yaml was created or gained the key; `replaced` — a
|
|
136
|
+
* same-line scalar was swapped in place; `noop` — already `copy`;
|
|
137
|
+
* `refused` — a conflict/unreadable file the command will not guess at.
|
|
138
|
+
*/
|
|
139
|
+
readonly action: 'written' | 'replaced' | 'noop' | 'refused';
|
|
140
|
+
readonly detail?: string;
|
|
141
|
+
}
|
|
142
|
+
/** The report of one `--apply` run: the read verdict plus the actions. */
|
|
143
|
+
export interface ApplyReport extends IsolateReport {
|
|
144
|
+
readonly actions: readonly ProfileAction[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Read-only inspection of a DSH home: confirmed profiles under `profiles/`,
|
|
148
|
+
* each one's `packageImportMethod` verdict, and a whole-home `lstat` scan
|
|
149
|
+
* counting directories, files, multi-link files, and uninspectable entries.
|
|
150
|
+
* Nothing is written. The report is `incomplete` — never clean — whenever any
|
|
151
|
+
* entry could not be inspected.
|
|
152
|
+
* @param home - the DSH home to inspect (canonicalized).
|
|
153
|
+
* @returns the isolation report.
|
|
154
|
+
*/
|
|
155
|
+
export declare function isolateReport(home: string): IsolateReport;
|
|
156
|
+
/**
|
|
157
|
+
* Write `packageImportMethod: copy` into every fixable confirmed profile.
|
|
158
|
+
* A missing file receives the upstream profile template plus the copy line;
|
|
159
|
+
* a missing key is appended; a same-line non-`copy` scalar is replaced by
|
|
160
|
+
* byte range so comments and CRLF survive; a block/complex/duplicate-key or
|
|
161
|
+
* unreadable file is refused untouched. Every write lands atomically through
|
|
162
|
+
* a sibling temp file + rename, and the command never descends or writes
|
|
163
|
+
* through a reparse point.
|
|
164
|
+
* @param home - the DSH home to migrate (canonicalized).
|
|
165
|
+
* @returns the read verdict plus one action per confirmed profile.
|
|
166
|
+
*/
|
|
167
|
+
export declare function applyIsolateConfig(home: string): ApplyReport;
|
|
168
|
+
/**
|
|
169
|
+
* The remediation steps the user must run by hand — this command writes
|
|
170
|
+
* configuration only. For every confirmed profile whose dependencies still
|
|
171
|
+
* need re-installing as copies (config changed under it, or links were seen
|
|
172
|
+
* inside it), the steps say: quit the hosts using it, remove its
|
|
173
|
+
* `node_modules` yourself, reinstall from its lockfile. Refused profiles get
|
|
174
|
+
* a manual-resolution line instead of automated steps.
|
|
175
|
+
* @param report - the {@link ApplyReport} (or a check report) to explain.
|
|
176
|
+
* @returns the guidance lines, in English.
|
|
177
|
+
*/
|
|
178
|
+
export declare function remediationSteps(report: IsolateReport): string[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MultiRootProviders`: the fail-loud guard that proves the services mounted as
|
|
3
|
+
* `ctx.fs` and `ctx.sandbox` are THIS bundle's providers — the only ones whose
|
|
4
|
+
* widening the registry, the instruction row, LSP routing and workspace-files
|
|
5
|
+
* are allowed to attach to.
|
|
6
|
+
*
|
|
7
|
+
* Why a brand and not `instanceof`: the plugin may be installed twice (a
|
|
8
|
+
* duplicated install forks the class identity), and cordis private fields are
|
|
9
|
+
* not a public contract. A well-known symbol survives both. The brand is
|
|
10
|
+
* non-enumerable so it never leaks into spreads, JSON, or surface payloads.
|
|
11
|
+
*
|
|
12
|
+
* Deterministic outcome under a provider conflict: cordis refuses a second
|
|
13
|
+
* `ctx.provide` of an already-registered key, so whoever mounts the colliding
|
|
14
|
+
* provider second loses loudly — but a conflicting plugin can also DISABLE this
|
|
15
|
+
* bundle's rows (the same mechanism this bundle uses on upstream), leaving a
|
|
16
|
+
* foreign `ctx.fs`/`ctx.sandbox` in place. In that case this service's
|
|
17
|
+
* `[Service.init]` logs which service is foreign and throws: the row never
|
|
18
|
+
* activates, so every row injecting `multiRootProviders` — the registry above
|
|
19
|
+
* all — never starts, the scope stays empty, and nothing is ever granted,
|
|
20
|
+
* regardless of patch order. Cordis re-evaluates an injecting fiber when an
|
|
21
|
+
* injected service is unprovided and re-provided, so a LATE replacement re-runs
|
|
22
|
+
* the same check.
|
|
23
|
+
*
|
|
24
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/providers
|
|
25
|
+
*/
|
|
26
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
27
|
+
/**
|
|
28
|
+
* The well-known brand key both providers carry. `Symbol.for` makes it
|
|
29
|
+
* identical across duplicated installs of this package.
|
|
30
|
+
*/
|
|
31
|
+
export declare const MULTI_ROOT_PROVIDER_BRAND: unique symbol;
|
|
32
|
+
/** The two service keys this bundle takes over. */
|
|
33
|
+
export type ProviderKind = 'fs' | 'sandbox';
|
|
34
|
+
/**
|
|
35
|
+
* Stamp the multi-root provider brand on a service value. Non-enumerable:
|
|
36
|
+
* the mark must not travel into structural copies.
|
|
37
|
+
* @param value - the service instance being mounted.
|
|
38
|
+
* @param kind - which provider key it claims.
|
|
39
|
+
* @returns the same value, branded.
|
|
40
|
+
*/
|
|
41
|
+
export declare function brandMultiRootProvider<T>(value: T, kind: ProviderKind): T;
|
|
42
|
+
/**
|
|
43
|
+
* Whether `value` is this bundle's provider of `kind`. Structural on purpose:
|
|
44
|
+
* no `instanceof`, so a duplicated install still recognizes the brand.
|
|
45
|
+
* @param value - the service mounted under the key.
|
|
46
|
+
* @param kind - the provider key being checked.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isMultiRootProvider(value: unknown, kind: ProviderKind): boolean;
|
|
49
|
+
declare module '@deepseek-ai/cordis' {
|
|
50
|
+
interface Context {
|
|
51
|
+
multiRootProviders: MultiRootProviders;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The conflict gate: `ctx.multiRootProviders` exists only while `ctx.fs` and
|
|
56
|
+
* `ctx.sandbox` are this bundle's branded providers. Every row that widens an
|
|
57
|
+
* authority surface injects it, so a foreign provider composition loses the
|
|
58
|
+
* whole gated set instead of silently widening an unverified one.
|
|
59
|
+
*/
|
|
60
|
+
export declare class MultiRootProviders extends Service {
|
|
61
|
+
static provide: string;
|
|
62
|
+
static inject: string[];
|
|
63
|
+
/**
|
|
64
|
+
* Verify the provider identities once per activation. A foreign service is
|
|
65
|
+
* named in the log and the throw leaves this row — and every dependent —
|
|
66
|
+
* unstarted: fail loud, never a widened grant.
|
|
67
|
+
*/
|
|
68
|
+
protected [Service.init](): void;
|
|
69
|
+
}
|
|
70
|
+
export default MultiRootProviders;
|