@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/registry.d.ts
CHANGED
|
@@ -37,14 +37,15 @@
|
|
|
37
37
|
* memory-authoritative after open, so two DSH processes sharing a storage
|
|
38
38
|
* root cannot each keep a live domain snapshot. A store-wide kernel lease
|
|
39
39
|
* (ADR-0007) elects one authority; a contended process publishes an empty
|
|
40
|
-
* scope
|
|
40
|
+
* scope marked `authority: 'unavailable'` — under which Full-access is
|
|
41
|
+
* limited to primary-root-only writes — rejects mutations, and retries the lease
|
|
42
|
+
* from {@link MultiRootRegistry.refresh}.
|
|
41
43
|
*
|
|
42
44
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/registry
|
|
43
45
|
*/
|
|
44
46
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
45
47
|
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
46
|
-
import {
|
|
47
|
-
import { type AdditionalRootId, type RegisteredRoot, type RootRef, type RootStatus } from './roots.ts';
|
|
48
|
+
import { type AdditionalRootId, type RegisteredRoot, type RootAccess, type RootRef, type RootStatus } from './roots.ts';
|
|
48
49
|
/** The domain name; also the store file stem (`$DSH_HOME/storages/<name>.json`). */
|
|
49
50
|
export declare const DOMAIN_NAME = "multi_root_workspace";
|
|
50
51
|
/** The single table name inside the domain. */
|
|
@@ -56,10 +57,20 @@ export declare const MAX_ALIAS_LENGTH = 120;
|
|
|
56
57
|
* locale; the panel also shows this string verbatim via `RootsView.unavailable`.
|
|
57
58
|
*/
|
|
58
59
|
export declare const REGISTRY_CONTENDED_MESSAGE = "the root registry is owned by another DSH process; additional roots are not granted here. Close the other process, then retry.";
|
|
60
|
+
/**
|
|
61
|
+
* Default cap on stored additional-root records per primary root. The bench
|
|
62
|
+
* evidence for this default lives in `docs/reference/bench-root-scale-*.json`
|
|
63
|
+
* (`pnpm bench:roots`): classification and argv widening stay flat far beyond
|
|
64
|
+
* it, so the cap exists to bound the operator-facing list and the widened
|
|
65
|
+
* profile size, not to protect the classifier.
|
|
66
|
+
*/
|
|
67
|
+
export declare const DEFAULT_MAX_ADDITIONAL_ROOTS = 16;
|
|
68
|
+
/** Hard ceiling for `maxAdditionalRoots`: values above it are config-invalid. */
|
|
69
|
+
export declare const MAX_ADDITIONAL_ROOTS_CEILING = 64;
|
|
59
70
|
/**
|
|
60
71
|
* Whether this process is the Registry Authority for the store-wide domain
|
|
61
72
|
* medium. Contended and storage-failed are both fail-closed: the scope stays
|
|
62
|
-
* empty and mutations reject.
|
|
73
|
+
* empty, is marked `authority: 'unavailable'`, and mutations reject.
|
|
63
74
|
*/
|
|
64
75
|
export type RegistryAuthorityState = {
|
|
65
76
|
readonly kind: 'active';
|
|
@@ -68,6 +79,9 @@ export type RegistryAuthorityState = {
|
|
|
68
79
|
} | {
|
|
69
80
|
readonly kind: 'storage-failed';
|
|
70
81
|
readonly reason: string;
|
|
82
|
+
} | {
|
|
83
|
+
readonly kind: 'config-invalid';
|
|
84
|
+
readonly reason: string;
|
|
71
85
|
};
|
|
72
86
|
/**
|
|
73
87
|
* Plugin configuration for the registry row. `leasePath` must sit beside the
|
|
@@ -78,6 +92,16 @@ export type RegistryAuthorityState = {
|
|
|
78
92
|
export interface Config {
|
|
79
93
|
/** Absolute path of the store-wide kernel lock file. */
|
|
80
94
|
readonly leasePath?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Cap on STORED additional-root records per primary root (every state
|
|
97
|
+
* counts, including withheld and unusable ones, so the count is stable and
|
|
98
|
+
* independent of the filesystem). Must be an integer in
|
|
99
|
+
* 1..{@link MAX_ADDITIONAL_ROOTS_CEILING}; an invalid value puts the
|
|
100
|
+
* registry in the fail-closed `config-invalid` state instead of throwing.
|
|
101
|
+
* A store that already exceeds the limit keeps every record and grants
|
|
102
|
+
* exactly what classification grants — only new adds are refused.
|
|
103
|
+
*/
|
|
104
|
+
readonly maxAdditionalRoots?: number;
|
|
81
105
|
}
|
|
82
106
|
declare module '@deepseek-ai/cordis' {
|
|
83
107
|
interface Context {
|
|
@@ -85,21 +109,54 @@ declare module '@deepseek-ai/cordis' {
|
|
|
85
109
|
}
|
|
86
110
|
}
|
|
87
111
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* is
|
|
112
|
+
* Stored shape of one root record.
|
|
113
|
+
*
|
|
114
|
+
* The access level is expressed by WHICH grant key is present, never by an
|
|
115
|
+
* `access` field on the same `recordedPath` (ADR-0017):
|
|
116
|
+
*
|
|
117
|
+
* - `recordedPath` means `inherit` — the historical writable grant, which
|
|
118
|
+
* under B9 keeps following the session mode. Upgrading a legacy record to
|
|
119
|
+
* `inherit` changes nothing about its effective authority.
|
|
120
|
+
* - `readOnlyRecordedPath` means `read-only`.
|
|
121
|
+
* - `readWriteRecordedPath` means fixed `read-write` (B9).
|
|
122
|
+
*
|
|
123
|
+
* That split is what makes this single layout downgrade-safe at domain
|
|
124
|
+
* version 1 (the JSON backend checks the version with strict equality, so a
|
|
125
|
+
* bump would break both upgrade and downgrade):
|
|
126
|
+
*
|
|
127
|
+
* - an OLDER plugin version parses this record with a schema that strips
|
|
128
|
+
* unknown keys, so a read-only or fixed read-write record loses its grant
|
|
129
|
+
* field and arrives with NO `recordedPath`; the old classification reports
|
|
130
|
+
* it `invalid` and grants nothing — a downgrade can never escalate either
|
|
131
|
+
* restricted level into a write grant. A fixed read-write record must
|
|
132
|
+
* therefore NEVER also write `recordedPath`: an older plugin would see the
|
|
133
|
+
* legacy field and grant it as plain read-write;
|
|
134
|
+
* - a record with `recordedPath` only is the writable grant it always was,
|
|
135
|
+
* so upgrading changes nothing for legacy records.
|
|
91
136
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
137
|
+
type PersistedRoot = {
|
|
138
|
+
id: string;
|
|
139
|
+
path: string;
|
|
140
|
+
/**
|
|
141
|
+
* Absent only in records written before the field existed (or by hand),
|
|
142
|
+
* and in non-inherit records. Such a record is reported `invalid` rather
|
|
143
|
+
* than granted on a guess — see classifyStoredRoots.
|
|
144
|
+
*/
|
|
145
|
+
recordedPath?: string | undefined;
|
|
146
|
+
readOnlyRecordedPath?: string | undefined;
|
|
147
|
+
readWriteRecordedPath?: string | undefined;
|
|
148
|
+
alias?: string | undefined;
|
|
149
|
+
addedAt: string;
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Stored shape of one primary root's records: the whole registration list.
|
|
153
|
+
* The domain's `single` layout stores the entire unit as one document, so
|
|
154
|
+
* keys stay opaque strings — which is exactly what lets the canonical
|
|
155
|
+
* primary root itself be the key.
|
|
156
|
+
*/
|
|
157
|
+
export type PersistedPrimaryRoot = {
|
|
158
|
+
roots: PersistedRoot[];
|
|
159
|
+
};
|
|
103
160
|
/**
|
|
104
161
|
* The domain declaration: version 1, `single` layout, one `roots` table keyed
|
|
105
162
|
* by the canonical primary root. Records that fail validation are backed up and
|
|
@@ -111,15 +168,7 @@ export declare const multiRootDomainSpec: {
|
|
|
111
168
|
version: number;
|
|
112
169
|
invalidRecords: "backup-and-skip";
|
|
113
170
|
tables: {
|
|
114
|
-
roots: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string,
|
|
115
|
-
roots: {
|
|
116
|
-
id: string;
|
|
117
|
-
path: string;
|
|
118
|
-
addedAt: string;
|
|
119
|
-
recordedPath?: string | undefined;
|
|
120
|
-
alias?: string | undefined;
|
|
121
|
-
}[];
|
|
122
|
-
}>;
|
|
171
|
+
roots: import("@deepseek-ai/dsh-storage-domain").DomainTableSpec<string, PersistedPrimaryRoot>;
|
|
123
172
|
};
|
|
124
173
|
};
|
|
125
174
|
/** Registration request for one new additional root. */
|
|
@@ -128,6 +177,12 @@ export interface AddRootInput {
|
|
|
128
177
|
readonly path: string;
|
|
129
178
|
/** Optional display alias. */
|
|
130
179
|
readonly alias?: string;
|
|
180
|
+
/**
|
|
181
|
+
* The write authority to register; defaults to `inherit` (the historical
|
|
182
|
+
* session-following grant). On a REVIVE the existing record keeps its
|
|
183
|
+
* access unless this field is given explicitly.
|
|
184
|
+
*/
|
|
185
|
+
readonly access?: RootAccess;
|
|
131
186
|
}
|
|
132
187
|
/** The registry service: `ctx.multiRootRegistry`. */
|
|
133
188
|
export declare class MultiRootRegistry extends Service {
|
|
@@ -156,9 +211,13 @@ export declare class MultiRootRegistry extends Service {
|
|
|
156
211
|
private loggedContended;
|
|
157
212
|
/** True once the fiber disposer has run: no further acquisition may happen. */
|
|
158
213
|
private disposed;
|
|
214
|
+
/** The validated per-primary-root cap (the default when the row omits it). */
|
|
215
|
+
private readonly maxRoots;
|
|
216
|
+
/** Keys already warned about a historical over-limit store (one warning each). */
|
|
217
|
+
private readonly warnedOverLimit;
|
|
159
218
|
/**
|
|
160
219
|
* @param ctx - the host context; `storageDomain` and `multiRootScope` are injected.
|
|
161
|
-
* @param config - the patch row's `leasePath`.
|
|
220
|
+
* @param config - the patch row's `leasePath` and `maxAdditionalRoots`.
|
|
162
221
|
*/
|
|
163
222
|
constructor(ctx: Context, config?: Config);
|
|
164
223
|
/**
|
|
@@ -185,6 +244,17 @@ export declare class MultiRootRegistry extends Service {
|
|
|
185
244
|
* Tests and diagnostics read this; surfaces use {@link unavailable}.
|
|
186
245
|
*/
|
|
187
246
|
get authority(): RegistryAuthorityState;
|
|
247
|
+
/**
|
|
248
|
+
* The configured cap on stored additional-root records per primary root.
|
|
249
|
+
* Surfaces show it next to the count so "N of LIMIT" is always explainable.
|
|
250
|
+
*/
|
|
251
|
+
get limit(): number;
|
|
252
|
+
/**
|
|
253
|
+
* Whether one primary root's stored record count exceeds {@link limit} — a
|
|
254
|
+
* historical over-limit store, which stays fully classified and granted.
|
|
255
|
+
* @param primaryRoot - the session workspace root (any spelling).
|
|
256
|
+
*/
|
|
257
|
+
overLimit(primaryRoot: string): boolean;
|
|
188
258
|
/**
|
|
189
259
|
* Every registration of one primary root, in registry order, as last
|
|
190
260
|
* classified. A READ of the in-memory state: it never touches the filesystem
|
|
@@ -225,6 +295,29 @@ export declare class MultiRootRegistry extends Service {
|
|
|
225
295
|
* @throws {RootValidationError} when the candidate violates a root rule.
|
|
226
296
|
*/
|
|
227
297
|
add(primaryRoot: string, input: AddRootInput): Promise<readonly RootStatus[]>;
|
|
298
|
+
/**
|
|
299
|
+
* Change one registration's write authority WITHOUT re-recording its path.
|
|
300
|
+
*
|
|
301
|
+
* Re-resolving is not re-authorizing: the grant keeps the canonical
|
|
302
|
+
* directory it was recorded for; only which persisted field carries it
|
|
303
|
+
* changes. The three levels are not a total order — `inherit` is weaker
|
|
304
|
+
* than `read-write` under a read-only session but stronger under
|
|
305
|
+
* Full-access — so "upgrade" cannot be a single direction. The gate is
|
|
306
|
+
* therefore: pinning to `read-only` is always allowed (a withheld root may
|
|
307
|
+
* be restricted while it is withheld), while ANY other target requires the
|
|
308
|
+
* root to be `available` under the FRESH classification — every
|
|
309
|
+
* non-read-only level can widen authority under some session mode, and a
|
|
310
|
+
* root that is not currently granted must not be granted anything by a
|
|
311
|
+
* metadata change.
|
|
312
|
+
* @param primaryRoot - the session workspace root the root belongs to.
|
|
313
|
+
* @param ref - which registered root to change.
|
|
314
|
+
* @param access - the new write authority.
|
|
315
|
+
* @returns the updated status list.
|
|
316
|
+
* @throws {RootValidationError} `not-found` when the reference matches
|
|
317
|
+
* nothing, `root-unavailable` when the target is not `available` and the
|
|
318
|
+
* new level is not `read-only`.
|
|
319
|
+
*/
|
|
320
|
+
setAccess(primaryRoot: string, ref: RootRef, access: RootAccess): Promise<readonly RootStatus[]>;
|
|
228
321
|
/**
|
|
229
322
|
* Remove one registration.
|
|
230
323
|
* @param primaryRoot - the session workspace root the root belongs to.
|
|
@@ -329,13 +422,21 @@ export declare class MultiRootRegistry extends Service {
|
|
|
329
422
|
/**
|
|
330
423
|
* Push one status list into the scope and notify listeners.
|
|
331
424
|
*
|
|
332
|
-
* What is published is the effective GRANT —
|
|
333
|
-
* withheld registration can never reach a provider
|
|
334
|
-
*
|
|
425
|
+
* What is published is the effective GRANT — membership requires
|
|
426
|
+
* `available`, so a withheld registration can never reach a provider as a
|
|
427
|
+
* grant. Withheld entries still publish for their DENY boundary alone;
|
|
428
|
+
* that includes `membership-conflict` records, whose recorded grant is
|
|
429
|
+
* intact and trustworthy even though their membership cannot stand. When
|
|
430
|
+
* the grant and the withheld set are unchanged (the common case for a
|
|
431
|
+
* read-path refresh on a
|
|
335
432
|
* healthy workspace) the scope is left alone and no listener fires: a refresh
|
|
336
433
|
* exists to keep the two in sync, not to churn on every listing — and, since
|
|
337
434
|
* the scope rebuilds its root table on each call, skipping the no-op write is
|
|
338
435
|
* also what keeps the read path cheap.
|
|
436
|
+
*
|
|
437
|
+
* Available roots keep registry order: instruction discovery consumes them
|
|
438
|
+
* under a shared byte budget, so a move must update the live scope immediately.
|
|
439
|
+
* Aliases and no-op moves still leave the signature unchanged.
|
|
339
440
|
*/
|
|
340
441
|
private publish;
|
|
341
442
|
/**
|
package/lib/types/roots.d.ts
CHANGED
|
@@ -22,13 +22,23 @@
|
|
|
22
22
|
* 6. `duplicate` — another registered root canonicalizes to the same path.
|
|
23
23
|
* 7. `nested` — the candidate lies under a registered root, or contains one.
|
|
24
24
|
* Same reasoning as rule 5, applied to the additional roots.
|
|
25
|
+
* 8. `unsupported-path` — on Windows, a network or device path (`\\server`,
|
|
26
|
+
* `\\?\…`, `\\.\…`): the ACL confinement cannot be shown to cover it, so it
|
|
27
|
+
* is rejected lexically before any filesystem touch.
|
|
25
28
|
*
|
|
26
29
|
* The stored side of the same rules lives in {@link classifyStoredRoots}, which
|
|
27
30
|
* must never throw: a registration that stopped being usable is REPORTED, not
|
|
28
|
-
* deleted, so the operator can see it and remove it deliberately.
|
|
31
|
+
* deleted, so the operator can see it and remove it deliberately. It runs in
|
|
32
|
+
* two passes — every record's own structural trust and availability first,
|
|
33
|
+
* then overlap between the RECORDED boundaries of all structurally sound
|
|
34
|
+
* records — so the answer never depends on registry order, and a pair of
|
|
35
|
+
* records whose grants collide (`membership-conflict`) keeps its trustworthy
|
|
36
|
+
* negative boundary even while its membership is withheld.
|
|
29
37
|
*
|
|
30
38
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/roots
|
|
31
39
|
*/
|
|
40
|
+
import { isWindowsNetworkOrDevicePath } from './windows-runner-contract.ts';
|
|
41
|
+
export { isWindowsNetworkOrDevicePath };
|
|
32
42
|
/**
|
|
33
43
|
* Stable identity of one registered additional root. A generated uuid, never
|
|
34
44
|
* the path: a path is re-canonicalized on every read and a display alias may
|
|
@@ -44,6 +54,19 @@ export type AdditionalRootId = string & {
|
|
|
44
54
|
* @returns the branded id.
|
|
45
55
|
*/
|
|
46
56
|
export declare function additionalRootId(value: string): AdditionalRootId;
|
|
57
|
+
/**
|
|
58
|
+
* The write authority one registration carries (B9, ADR-0017).
|
|
59
|
+
*
|
|
60
|
+
* - `inherit` — the root follows the session mode: writable under
|
|
61
|
+
* `workspace-write` and unrestricted under `danger-full-access`, read-only
|
|
62
|
+
* under `read-only`. This is what legacy `recordedPath` records mean.
|
|
63
|
+
* - `read-only` — the root stays a workspace member (instructions, LSP,
|
|
64
|
+
* workspace-file routing, and reads all still see it) but a write is refused
|
|
65
|
+
* under EVERY mode, including `danger-full-access`.
|
|
66
|
+
* - `read-write` — the root is writable under EVERY mode, including a global
|
|
67
|
+
* `read-only` session; it does not widen anything beyond its own directory.
|
|
68
|
+
*/
|
|
69
|
+
export type RootAccess = 'inherit' | 'read-write' | 'read-only';
|
|
47
70
|
/** One registered additional root exactly as it is persisted. */
|
|
48
71
|
export interface RegisteredRoot {
|
|
49
72
|
/** Stable registry identity. */
|
|
@@ -51,13 +74,35 @@ export interface RegisteredRoot {
|
|
|
51
74
|
/** Canonical absolute directory (the `realpath` captured at registration). */
|
|
52
75
|
readonly path: string;
|
|
53
76
|
/**
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* `
|
|
77
|
+
* The write authority this registration carries. In memory every record has
|
|
78
|
+
* exactly one value; on disk it is expressed by WHICH grant field is
|
|
79
|
+
* present — `recordedPath` means `inherit`, `readOnlyRecordedPath` means
|
|
80
|
+
* `read-only`, `readWriteRecordedPath` means `read-write` — never by a
|
|
81
|
+
* separate `access` key (see `registry.ts` for the downgrade safety that
|
|
82
|
+
* split buys).
|
|
83
|
+
*/
|
|
84
|
+
readonly access: RootAccess;
|
|
85
|
+
/**
|
|
86
|
+
* The canonical directory this INHERIT registration was GRANTED for: the
|
|
87
|
+
* `realpath` observed when the operator registered it (or the last time the
|
|
88
|
+
* operator restored it). Authorization is `canonicalPath(path) ===
|
|
89
|
+
* recordedPath` — a root whose current resolution moved somewhere else is
|
|
90
|
+
* reported as `redirected` and withheld, never silently granted elsewhere.
|
|
59
91
|
*/
|
|
60
92
|
readonly recordedPath?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The canonical directory this READ-ONLY registration was granted for. It
|
|
95
|
+
* plays exactly the role `recordedPath` plays for an inherit record, kept
|
|
96
|
+
* under a separate key so the single persisted layout stays downgrade-safe.
|
|
97
|
+
*/
|
|
98
|
+
readonly readOnlyRecordedPath?: string;
|
|
99
|
+
/**
|
|
100
|
+
* The canonical directory this fixed READ-WRITE registration was granted
|
|
101
|
+
* for. Same role as the other two grant fields, under its own key: an older
|
|
102
|
+
* plugin strips the unknown field and classifies the record `invalid`
|
|
103
|
+
* rather than granting it as a legacy `recordedPath` write.
|
|
104
|
+
*/
|
|
105
|
+
readonly readWriteRecordedPath?: string;
|
|
61
106
|
/** Optional display alias; absent when the operator cleared it. */
|
|
62
107
|
readonly alias?: string;
|
|
63
108
|
/** ISO-8601 instant of registration. */
|
|
@@ -73,7 +118,9 @@ export interface RegisteredRoot {
|
|
|
73
118
|
* registered for (it was replaced by a symlink, or a symlink in the chain
|
|
74
119
|
* changed); kept, withheld, and `recheck` restores it once the original
|
|
75
120
|
* directory is back.
|
|
76
|
-
* - `invalid` — the record violates a rule and can only be removed.
|
|
121
|
+
* - `invalid` — the record violates a rule and can only be removed. Whether the
|
|
122
|
+
* violation also voids the record's negative boundary is told apart by
|
|
123
|
+
* {@link RootStatus.invalidReason}.
|
|
77
124
|
*/
|
|
78
125
|
export type RootState = 'available' | 'missing' | 'redirected' | 'invalid';
|
|
79
126
|
/** One registered root as the surfaces report it. */
|
|
@@ -82,6 +129,20 @@ export interface RootStatus extends RegisteredRoot {
|
|
|
82
129
|
readonly state: RootState;
|
|
83
130
|
/** Why the root is not `available`; absent otherwise. */
|
|
84
131
|
readonly detail?: string;
|
|
132
|
+
/**
|
|
133
|
+
* Distinguishes WHY an `invalid` record is unusable. Internal only: it is
|
|
134
|
+
* neither persisted nor sent over the panel wire — `state` and `detail`
|
|
135
|
+
* carry everything a surface needs.
|
|
136
|
+
*
|
|
137
|
+
* `'membership-conflict'` means the record passed every structural rule —
|
|
138
|
+
* including an intact, absolute recorded grant — but its recorded boundary
|
|
139
|
+
* overlaps another record's. Neither membership may be granted while the
|
|
140
|
+
* conflict stands, yet the boundary remains a trustworthy negative
|
|
141
|
+
* restriction, so {@link reservedRootBoundary} keeps it reserved and the
|
|
142
|
+
* registry publishes the root as a withheld, deny-only entry. Every other
|
|
143
|
+
* `invalid` verdict left the grant unverifiable, so it contributes nothing.
|
|
144
|
+
*/
|
|
145
|
+
readonly invalidReason?: 'membership-conflict';
|
|
85
146
|
}
|
|
86
147
|
/** One status together with the 1-based ordinal the surfaces accept for it. */
|
|
87
148
|
export interface IndexedRootStatus {
|
|
@@ -108,7 +169,7 @@ export type RootRef = {
|
|
|
108
169
|
readonly addedAt: string;
|
|
109
170
|
};
|
|
110
171
|
/** Every way root handling can fail, as a stable code the surfaces localize. */
|
|
111
|
-
export type RootValidationCode = 'not-absolute' | 'missing' | 'not-a-directory' | 'equals-primary' | 'primary-overlap' | 'duplicate' | 'nested' | 'invalid-alias' | 'not-found' | 'invalid-ref' | 'storage-unavailable' | 'registry-contended' | 'reveal-unavailable' | 'session-not-found';
|
|
172
|
+
export type RootValidationCode = 'not-absolute' | 'missing' | 'not-a-directory' | 'equals-primary' | 'primary-overlap' | 'duplicate' | 'nested' | 'invalid-alias' | 'not-found' | 'invalid-ref' | 'storage-unavailable' | 'registry-contended' | 'root-limit-reached' | 'config-invalid' | 'reveal-unavailable' | 'session-not-found' | 'unsupported-path' | 'root-unavailable';
|
|
112
173
|
/** A rejected root operation; `code` is the stable contract, `message` the fallback prose. */
|
|
113
174
|
export declare class RootValidationError extends Error {
|
|
114
175
|
readonly code: RootValidationCode;
|
|
@@ -163,10 +224,19 @@ export interface RootCandidateContext {
|
|
|
163
224
|
*/
|
|
164
225
|
readonly home?: string;
|
|
165
226
|
/**
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
|
|
227
|
+
* The platform the rules run for; defaults to `process.platform`. A caller
|
|
228
|
+
* (a test, a probe) may exercise another platform's rules — notably the
|
|
229
|
+
* Windows network/device-path refusal — on any host.
|
|
230
|
+
*/
|
|
231
|
+
readonly platform?: NodeJS.Platform;
|
|
232
|
+
/**
|
|
233
|
+
* The recorded boundaries already reserved: {@link reservedRootBoundary}
|
|
234
|
+
* applied to every other status — available, missing, redirected, and
|
|
235
|
+
* membership-conflict records alike. A withheld root grants nothing, but
|
|
236
|
+
* its recorded boundary is the negative restriction a new writable root
|
|
237
|
+
* must not swallow while the operator has not removed it. Only a
|
|
238
|
+
* structurally invalid record contributes nothing, and the row being
|
|
239
|
+
* revived exempts only itself.
|
|
170
240
|
*/
|
|
171
241
|
readonly existing: readonly string[];
|
|
172
242
|
}
|
|
@@ -188,15 +258,32 @@ export declare function validateRootCandidate(raw: string, context: RootCandidat
|
|
|
188
258
|
* `invalid` (kept, unusable).
|
|
189
259
|
* @param primaryRoot - the session workspace root the records belong to.
|
|
190
260
|
* @param records - the stored records, in registry order.
|
|
261
|
+
* @param platform - the platform the rules run for; defaults to `process.platform`.
|
|
191
262
|
* @returns one status per record, in the same order.
|
|
192
263
|
*/
|
|
193
|
-
export declare function classifyStoredRoots(primaryRoot: string, records: readonly RegisteredRoot[]): RootStatus[];
|
|
264
|
+
export declare function classifyStoredRoots(primaryRoot: string, records: readonly RegisteredRoot[], platform?: NodeJS.Platform): RootStatus[];
|
|
194
265
|
/**
|
|
195
266
|
* The canonical roots a status list grants, in registry order.
|
|
196
267
|
* @param statuses - the statuses to project.
|
|
197
268
|
* @returns the canonical paths of every `available` root.
|
|
198
269
|
*/
|
|
199
270
|
export declare function availableRoots(statuses: readonly RootStatus[]): string[];
|
|
271
|
+
/**
|
|
272
|
+
* The recorded boundary one status still RESERVES: the grant it was
|
|
273
|
+
* registered for, which a new or revived registration may not overlap merely
|
|
274
|
+
* because the root is currently unusable.
|
|
275
|
+
*
|
|
276
|
+
* `available`, `missing`, `redirected`, and membership-conflict statuses all
|
|
277
|
+
* carry an intact, trustworthy recorded grant; every other `invalid` verdict
|
|
278
|
+
* means the grant could not be parsed at all, so it reserves — and denies —
|
|
279
|
+
* nothing. This is not an independent authorization table: it derives the
|
|
280
|
+
* boundary from the status list {@link classifyStoredRoots} produced, with
|
|
281
|
+
* the same grant-extraction semantics, so its answer can never disagree with
|
|
282
|
+
* classification.
|
|
283
|
+
* @param status - one classified registration.
|
|
284
|
+
* @returns the absolute recorded boundary, or `undefined` when there is none.
|
|
285
|
+
*/
|
|
286
|
+
export declare function reservedRootBoundary(status: RootStatus): string | undefined;
|
|
200
287
|
/**
|
|
201
288
|
* Pair every status with the 1-based ordinal the surfaces accept for it. The
|
|
202
289
|
* ordinal belongs to the POSITION, so it stays correct even for records whose
|
package/lib/types/sandbox.d.ts
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
*
|
|
28
28
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/sandbox
|
|
29
29
|
*/
|
|
30
|
-
import type {
|
|
30
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
31
|
+
import type { SandboxExecutionPolicy, SandboxPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
31
32
|
import { LocalSandboxProvider } from '@deepseek-ai/dsh-sandbox-local';
|
|
32
33
|
import type { Config } from '@deepseek-ai/dsh-sandbox-local';
|
|
33
34
|
import type { UpstreamConfined } from './compat/sandbox-confine.ts';
|
|
@@ -39,6 +40,7 @@ export type { Config };
|
|
|
39
40
|
*/
|
|
40
41
|
export declare class MultiRootSandboxProvider extends LocalSandboxProvider {
|
|
41
42
|
static inject: string[];
|
|
43
|
+
constructor(ctx: Context, config: Config);
|
|
42
44
|
/**
|
|
43
45
|
* Wrap `argv` so it executes confined under `policy` on this host, granting
|
|
44
46
|
* the scope's additional roots inside the SAME dialect profile.
|
|
@@ -60,17 +62,71 @@ export declare class MultiRootSandboxProvider extends LocalSandboxProvider {
|
|
|
60
62
|
*/
|
|
61
63
|
confine(argv: readonly string[], policy: SandboxPolicy, signal?: AbortSignal): UpstreamConfined;
|
|
62
64
|
/**
|
|
63
|
-
* Graft the scope's
|
|
65
|
+
* Graft the scope's effective grants and denies onto one upstream `confine`
|
|
66
|
+
* result.
|
|
64
67
|
*
|
|
65
68
|
* Pure with respect to the upstream call: it only ever reads the scope and
|
|
66
69
|
* rewrites `argv`. `enforcement`, `denialSignatures` and `runnerFailureRules`
|
|
67
70
|
* are passed through untouched, because the bash executor derives its denial
|
|
68
71
|
* and enforcement reporting from them.
|
|
72
|
+
*
|
|
73
|
+
* Mode behavior (B9, R2): `workspace-write` widens the grant with the
|
|
74
|
+
* effective writable roots AND denies any restricted root the upstream
|
|
75
|
+
* grants could reach (a `read-only` root registered under the platform
|
|
76
|
+
* temp). `read-only` grafts the fixed `read-write` roots back onto the
|
|
77
|
+
* deny-all baseline — the three-state matrix's standout cell — and then
|
|
78
|
+
* applies the SAME deny pass: a grafted writable parent must not silently
|
|
79
|
+
* swallow a denied child (Seatbelt appends the deny after the allow, bwrap
|
|
80
|
+
* adds a mandatory `--ro-bind`, Landlock / windows-acl refuse the
|
|
81
|
+
* unrepresentable subtraction). `danger-full-access` never reaches
|
|
82
|
+
* here (consumers bypass `confine`); its deny semantics live in
|
|
83
|
+
* {@link restrictedArgvPrefix}.
|
|
69
84
|
* @param confined - the upstream result for this call.
|
|
70
85
|
* @param argv - the caller's argv, needed to re-split the wrapped form.
|
|
71
86
|
* @param policy - the per-call policy.
|
|
72
|
-
* @
|
|
87
|
+
* @param scope - the authorization snapshot taken at dispatch.
|
|
88
|
+
* @returns the upstream result element for element, or one with a rewritten argv.
|
|
73
89
|
*/
|
|
74
90
|
private widenForScope;
|
|
91
|
+
/**
|
|
92
|
+
* Build the runner-prefix that confines an ambient-writable execution to
|
|
93
|
+
* everything EXCEPT `deniedRoots` — the `danger-full-access` + explicit
|
|
94
|
+
* `read-only` cell, which the shell, PTY and PTC consumers never route
|
|
95
|
+
* through `confine()` at all.
|
|
96
|
+
*
|
|
97
|
+
* The restricted profile does not depend on the callee's argv, so the prefix
|
|
98
|
+
* is computed once per call here and the synchronous `ctx.subprocess.spawn`
|
|
99
|
+
* seam only has to prepend it. The upstream profile builders stay private:
|
|
100
|
+
* this probes `confine` under `read-only` for the runner's argument skeleton
|
|
101
|
+
* and rewrites the file-effect part per dialect. Windows selects the
|
|
102
|
+
* standalone ambient deny-token runner. Landlock cannot express
|
|
103
|
+
* "everything except", so this combination selects bubblewrap; unusable
|
|
104
|
+
* runners fail without spawning an unconfined command.
|
|
105
|
+
*
|
|
106
|
+
* The return preserves the installed `confine` shape: a synchronous result
|
|
107
|
+
* on a synchronous release, a promise on an asynchronous one.
|
|
108
|
+
* @param policy - the caller's policy (mode must be `danger-full-access`).
|
|
109
|
+
* @param deniedRoots - canonical roots this execution must not write.
|
|
110
|
+
* @param signal - cancellation, forwarded to releases that accept it.
|
|
111
|
+
* @param protectedHome - diagnostics label: which deny root is the
|
|
112
|
+
* protected `$DSH_HOME`. Forwarded to the Windows runner only when it is
|
|
113
|
+
* a member of `deniedRoots`; it never changes authorization.
|
|
114
|
+
* @returns the argv prefix, ending at the `--` separator.
|
|
115
|
+
*/
|
|
116
|
+
restrictedArgvPrefix(policy: SandboxExecutionPolicy, deniedRoots: readonly string[], signal?: AbortSignal, protectedHome?: string): readonly string[] | Promise<readonly string[]>;
|
|
117
|
+
/** Full-access restriction and the actual selected backend's diagnostics. */
|
|
118
|
+
restrictedExecution(policy: SandboxExecutionPolicy, deniedRoots: readonly string[], signal?: AbortSignal, protectedHome?: string): UpstreamConfined;
|
|
119
|
+
/**
|
|
120
|
+
* Limit an execution with unknown registry authority to the primary root.
|
|
121
|
+
* Shared temp grants are unsafe: another process may have registered a
|
|
122
|
+
* read-only root beneath them. Harvest the upstream read-only skeleton and
|
|
123
|
+
* graft only the primary root, retaining diagnostics and the sync/async shape.
|
|
124
|
+
* Explicit denies still win; an unrepresentable overlap refuses launch.
|
|
125
|
+
* @param policy - the caller's execution policy.
|
|
126
|
+
* @param deniedRoots - known negative boundaries, including protected home.
|
|
127
|
+
* @param signal - cancellation forwarded to upstream.
|
|
128
|
+
* @returns the confined prefix ending at `--`, without the caller's command.
|
|
129
|
+
*/
|
|
130
|
+
clampedExecution(policy: SandboxExecutionPolicy, deniedRoots: readonly string[], signal?: AbortSignal): UpstreamConfined;
|
|
75
131
|
}
|
|
76
132
|
export default MultiRootSandboxProvider;
|