@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/scope.d.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* @module @dsh-electron/dsh-plugin-multi-root-workspace/scope
|
|
17
17
|
*/
|
|
18
18
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
19
|
-
import type { SandboxExecutionPolicy } from '@deepseek-ai/dsh-sandbox';
|
|
19
|
+
import type { SandboxExecutionPolicy, SandboxMode } from '@deepseek-ai/dsh-sandbox';
|
|
20
|
+
import type { RootAccess } from './roots.ts';
|
|
20
21
|
declare module '@deepseek-ai/cordis' {
|
|
21
22
|
interface Context {
|
|
22
23
|
multiRootScope: MultiRootScopeService;
|
|
@@ -37,19 +38,82 @@ export interface AdditionalWorkspaceRoot {
|
|
|
37
38
|
* ever registered.
|
|
38
39
|
*/
|
|
39
40
|
recordedPath: string;
|
|
41
|
+
/**
|
|
42
|
+
* The write authority the registration carries (B9 three-state).
|
|
43
|
+
* `read-only` roots remain workspace members — instructions, LSP,
|
|
44
|
+
* workspace-file routing, and reads all still see them — but they are never
|
|
45
|
+
* part of `writableRoots` and never reach a write grant, under ANY mode.
|
|
46
|
+
* `inherit` follows the session mode; fixed `read-write` stays writable even
|
|
47
|
+
* under a read-only session.
|
|
48
|
+
*/
|
|
49
|
+
access: RootAccess;
|
|
50
|
+
/** Registry withheld the positive grant; retain its recorded deny boundary. */
|
|
51
|
+
withheld?: boolean;
|
|
40
52
|
/** Optional display alias. */
|
|
41
53
|
alias?: string;
|
|
42
54
|
}
|
|
55
|
+
/** One granted workspace member, with the authority fact the enforcement reads. */
|
|
56
|
+
export interface ScopeRoot {
|
|
57
|
+
/** Canonical absolute directory. */
|
|
58
|
+
readonly path: string;
|
|
59
|
+
/** The write authority the registration carries. */
|
|
60
|
+
readonly access: RootAccess;
|
|
61
|
+
/** Optional display alias. */
|
|
62
|
+
readonly alias?: string;
|
|
63
|
+
/** Stable registry identity. */
|
|
64
|
+
readonly id: string;
|
|
65
|
+
}
|
|
43
66
|
/**
|
|
44
67
|
* The resolved scope of one confined call. `primaryRoot` equals the policy's
|
|
45
|
-
* workspace root; `additionalRoots` are canonical, deduplicated, never equal
|
|
46
|
-
* the primary root
|
|
68
|
+
* workspace root; `additionalRoots` are canonical, deduplicated, never equal
|
|
69
|
+
* to the primary root.
|
|
47
70
|
*/
|
|
48
71
|
export interface FilesystemScope {
|
|
49
72
|
/** The session's workspace root (`session.header.cwd`, canonical). */
|
|
50
73
|
primaryRoot: string;
|
|
51
|
-
/**
|
|
74
|
+
/** Every granted member — ANY access level — canonical, in registry order. */
|
|
52
75
|
additionalRoots: readonly string[];
|
|
76
|
+
/** The granted members with their access, in registry order. */
|
|
77
|
+
roots: readonly ScopeRoot[];
|
|
78
|
+
/**
|
|
79
|
+
* The members a write may reach under THIS call — the effective access
|
|
80
|
+
* (registration policy × session mode) narrowed by the tool cap.
|
|
81
|
+
*/
|
|
82
|
+
writableRoots: readonly string[];
|
|
83
|
+
/**
|
|
84
|
+
* Registered boundaries THIS call must not write, including missing and
|
|
85
|
+
* redirected registrations. Negative restrictions never create a grant.
|
|
86
|
+
* It is the deny set enforcement reads: explicit
|
|
87
|
+
* `read-only` roots under `danger-full-access` (and wherever another grant,
|
|
88
|
+
* such as the platform temp area, would otherwise cover the same path), and
|
|
89
|
+
* every member when the call is tool-capped. Read access is unaffected —
|
|
90
|
+
* these are all still workspace members.
|
|
91
|
+
*/
|
|
92
|
+
deniedRoots: readonly string[];
|
|
93
|
+
/**
|
|
94
|
+
* Whether this process holds the registry authority for the store. While it
|
|
95
|
+
* is `unavailable` — contended, storage-failed, config-invalid, or torn
|
|
96
|
+
* down — the empty root table is NOT evidence that nothing is restricted:
|
|
97
|
+
* the registrations this process cannot see may carry explicit `read-only`
|
|
98
|
+
* boundaries, so both writable modes limit writes to the primary root
|
|
99
|
+
* instead of trusting the empty deny set.
|
|
100
|
+
* A composition with no registry at all never reports `unavailable` — it can
|
|
101
|
+
* never have registered roots.
|
|
102
|
+
*/
|
|
103
|
+
authority: 'available' | 'unavailable';
|
|
104
|
+
/**
|
|
105
|
+
* The canonical DSH home directory, present exactly when this scope appended
|
|
106
|
+
* it to `deniedRoots` — i.e. under `danger-full-access` while restrictions
|
|
107
|
+
* apply (a non-empty deny set or an unavailable authority). `$DSH_HOME`
|
|
108
|
+
* holds the store, the lease, the profiles and the user's own config: an
|
|
109
|
+
* ambient Full-access process must not edit them, so once the workspace
|
|
110
|
+
* carries ANY deny boundary the home joins it. Zero additional roots with a
|
|
111
|
+
* healthy authority never set this — the empty scope stays upstream-exact.
|
|
112
|
+
* Never set under `workspace-write` or `read-only`: those modes already
|
|
113
|
+
* confine writes by containment, so denying home adds nothing but the cost
|
|
114
|
+
* of a stale `FsTarget` re-resolution on every write.
|
|
115
|
+
*/
|
|
116
|
+
protectedHome?: string;
|
|
53
117
|
}
|
|
54
118
|
/**
|
|
55
119
|
* Canonicalize and sanitize one root list against a primary root: drop the
|
|
@@ -76,15 +140,65 @@ export declare function sanitizeAdditionalRoots(primaryRoot: string, roots: read
|
|
|
76
140
|
*/
|
|
77
141
|
export declare const MULTI_ROOT_CONTEXT_NAME = "multi-root:scope";
|
|
78
142
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
*
|
|
143
|
+
* The deterministic sentence the model hears while the registry authority is
|
|
144
|
+
* unavailable under either writable mode — enforcement is then the
|
|
145
|
+
* primary-root-only one, which the prompt must not leave implicit.
|
|
146
|
+
*/
|
|
147
|
+
export declare const REGISTRY_UNAVAILABLE_NOTE: string;
|
|
148
|
+
/**
|
|
149
|
+
* Render the workspace topology the model is told about. Pure: the text
|
|
150
|
+
* depends only on its inputs, so two consecutive assemblies of the same
|
|
151
|
+
* session state produce the same contribution byte for byte. Only granted
|
|
152
|
+
* members are ever listed — withheld, missing, or invalid roots never reach
|
|
153
|
+
* `roots` in the first place — and no host internals (ids, SIDs, store or
|
|
154
|
+
* lease paths) are ever named.
|
|
155
|
+
* @param options.platform - the host platform (`process.platform` of the contribution).
|
|
156
|
+
* @param options.mode - the session's sandbox mode.
|
|
157
|
+
* @param options.primaryRoot - the canonical session workspace root.
|
|
158
|
+
* @param options.roots - the granted additional members, in registry order.
|
|
159
|
+
* @param options.cappedTools - sorted tool names that can never write an additional root.
|
|
160
|
+
* @param options.toolAccessInvalid - invalid host config caps every additional-root write.
|
|
161
|
+
* @param options.authority - the registry-authority fact; `unavailable` under
|
|
162
|
+
* `danger-full-access` and `workspace-write` limit writes to the primary root, which the
|
|
163
|
+
* model must hear even when there is no topology to describe.
|
|
164
|
+
* @param options.protectedHome - the canonical DSH home when the scope appended
|
|
165
|
+
* it to the deny set (`danger-full-access` with restrictions); stated by path
|
|
166
|
+
* so the model can place the refusal.
|
|
167
|
+
* @returns the stable topology text; empty when there are no additional roots
|
|
168
|
+
* and no downgrade to report.
|
|
86
169
|
*/
|
|
87
|
-
export declare function renderWorkspaceRootsContext(
|
|
170
|
+
export declare function renderWorkspaceRootsContext(options: {
|
|
171
|
+
readonly platform: string;
|
|
172
|
+
readonly mode: SandboxMode;
|
|
173
|
+
readonly primaryRoot: string;
|
|
174
|
+
readonly roots: readonly ScopeRoot[];
|
|
175
|
+
readonly cappedTools: readonly string[];
|
|
176
|
+
readonly toolAccessInvalid?: boolean;
|
|
177
|
+
readonly authority?: 'available' | 'unavailable';
|
|
178
|
+
readonly protectedHome?: string;
|
|
179
|
+
}): string;
|
|
180
|
+
/**
|
|
181
|
+
* Plugin configuration for the `multi-root-scope` row.
|
|
182
|
+
*/
|
|
183
|
+
export interface Config {
|
|
184
|
+
/**
|
|
185
|
+
* Per-tool write caps: a listed tool may never write ANY additional root
|
|
186
|
+
* (the primary root and upstream behaviour are unchanged). `read-only` is
|
|
187
|
+
* the only legal value — a cap restricts, it can never widen past the
|
|
188
|
+
* root's own access.
|
|
189
|
+
*/
|
|
190
|
+
readonly toolAccess?: Record<string, 'read-only'>;
|
|
191
|
+
/**
|
|
192
|
+
* The resolved DSH home directory (`$DSH_HOME` / `~/.dsh`), which the patch
|
|
193
|
+
* row fills from `dshHomePath()`. Under `danger-full-access` it joins the
|
|
194
|
+
* deny set exactly while restrictions apply — a non-empty derived deny set
|
|
195
|
+
* or an unavailable registry authority — so an ambient agent process cannot
|
|
196
|
+
* overwrite the store, the lease file, profiles, or the user's own config.
|
|
197
|
+
* Absent or invalid (non-string, empty, relative) means the boundary simply
|
|
198
|
+
* does not exist: bare test compositions carry no home.
|
|
199
|
+
*/
|
|
200
|
+
readonly dshHome?: string;
|
|
201
|
+
}
|
|
88
202
|
/**
|
|
89
203
|
* The `ctx.multiRootScope` service. Providers ask it — never the filesystem —
|
|
90
204
|
* what the current scope is, which is what keeps the fs fence and every kernel
|
|
@@ -94,9 +208,37 @@ export declare class MultiRootScopeService extends Service {
|
|
|
94
208
|
/** Additional roots keyed by canonical primary root; empty until roots are registered. */
|
|
95
209
|
private readonly rootsByPrimary;
|
|
96
210
|
private readonly listeners;
|
|
211
|
+
/**
|
|
212
|
+
* The registry-authority fact the registry row publishes. Defaults to
|
|
213
|
+
* `available`: a composition without a registry row can never have
|
|
214
|
+
* registered roots, so nothing is unknown there. The registry sets it false
|
|
215
|
+
* the moment it cannot prove the store is its own — before the lease is
|
|
216
|
+
* acquired, while contended or storage-failed, and at teardown — and true
|
|
217
|
+
* only after the accepted records have been published.
|
|
218
|
+
*/
|
|
219
|
+
private registryAuthority;
|
|
220
|
+
/** Validated `toolAccess` row config; empty when no tool is capped. */
|
|
221
|
+
private readonly caps;
|
|
222
|
+
/** A malformed `toolAccess` refuses every additional-root write (fail closed). */
|
|
223
|
+
private readonly capsInvalid;
|
|
224
|
+
/**
|
|
225
|
+
* The configured DSH home boundary; `undefined` when the row carries no
|
|
226
|
+
* `dshHome` (bare test compositions) or one that is not an absolute string —
|
|
227
|
+
* an invalid value is logged at construction and ignored rather than
|
|
228
|
+
* treated as evidence.
|
|
229
|
+
*/
|
|
230
|
+
private readonly dshHome;
|
|
97
231
|
/** Observe registry publications; consumers still resolve authority from scopeOf(). */
|
|
98
232
|
subscribe(listener: () => void): () => void;
|
|
99
|
-
|
|
233
|
+
/**
|
|
234
|
+
* The sorted names of the tools `toolAccess` caps — read-only for the
|
|
235
|
+
* prompt contribution. An invalid config is not enumerated (it caps every
|
|
236
|
+
* call but is not a listable tool set).
|
|
237
|
+
*/
|
|
238
|
+
toolCaps(): readonly string[];
|
|
239
|
+
/** Whether malformed toolAccess caps every call, including unknown tools. */
|
|
240
|
+
get toolAccessInvalid(): boolean;
|
|
241
|
+
constructor(ctx: Context, config?: Config);
|
|
100
242
|
/**
|
|
101
243
|
* Resolve the scope of one confined call.
|
|
102
244
|
* @param policy - the upstream policy whose workspace root is the primary root.
|
|
@@ -104,11 +246,45 @@ export declare class MultiRootScopeService extends Service {
|
|
|
104
246
|
*/
|
|
105
247
|
resolve(policy: SandboxExecutionPolicy): FilesystemScope;
|
|
106
248
|
/**
|
|
107
|
-
*
|
|
249
|
+
* Whether THIS call's additional-root writes are capped. Inside a tool
|
|
250
|
+
* dispatch the AsyncLocalStorage store answers; outside any dispatch (user
|
|
251
|
+
* PTY, panel, an unknown async boundary) the answer is fail-closed: capped
|
|
252
|
+
* whenever `toolAccess` names any tool, because an operation whose tool
|
|
253
|
+
* identity is unknown gets the most restrictive configured cap. Membership
|
|
254
|
+
* is never affected — only writability.
|
|
255
|
+
*/
|
|
256
|
+
private capped;
|
|
257
|
+
/**
|
|
258
|
+
* The additional roots registered for one canonical primary root — every
|
|
259
|
+
* granted member, ANY access level (a read-only root is still a member of
|
|
260
|
+
* the workspace for instructions, LSP, routing, and reads).
|
|
108
261
|
* @param primaryRoot - canonical primary root (or any spelling thereof).
|
|
109
262
|
* @returns the canonical additional roots; empty when none are registered.
|
|
110
263
|
*/
|
|
111
264
|
scopeOf(primaryRoot: string): readonly string[];
|
|
265
|
+
/**
|
|
266
|
+
* The additional roots a WRITE may reach for one primary root under
|
|
267
|
+
* `workspace-write` — the members whose effective access is writable there
|
|
268
|
+
* (`inherit` and fixed `read-write`). Tool caps are per-call and do not
|
|
269
|
+
* apply to this diagnostic projection.
|
|
270
|
+
* @param primaryRoot - canonical primary root (or any spelling thereof).
|
|
271
|
+
* @returns the canonical writable roots; empty when none qualify.
|
|
272
|
+
*/
|
|
273
|
+
writableOf(primaryRoot: string): readonly string[];
|
|
274
|
+
/**
|
|
275
|
+
* Whether ANY registered primary currently publishes an explicit `read-only`
|
|
276
|
+
* member. The PTY seam resolves its caller through the agent-PTY
|
|
277
|
+
* environment markers; when that lookup fails this flag is the difference
|
|
278
|
+
* between pass-through (nothing to deny anywhere on the store) and fail
|
|
279
|
+
* closed. An unavailable registry authority answers `true` by itself: a
|
|
280
|
+
* terminal whose session cannot be resolved must not inherit a deny set
|
|
281
|
+
* this process cannot see.
|
|
282
|
+
* @returns whether at least one primary holds a restricted root, or the
|
|
283
|
+
* registry cannot prove none does.
|
|
284
|
+
*/
|
|
285
|
+
hasActiveDenyRoots(): boolean;
|
|
286
|
+
/** The sanitized members of one primary root, in registry order. */
|
|
287
|
+
private membersOf;
|
|
112
288
|
/**
|
|
113
289
|
* Replace the roots registered for one primary root.
|
|
114
290
|
*
|
|
@@ -119,5 +295,15 @@ export declare class MultiRootScopeService extends Service {
|
|
|
119
295
|
* @param roots - the additional roots, in display order.
|
|
120
296
|
*/
|
|
121
297
|
setAdditionalRoots(primaryRoot: string, roots: readonly AdditionalWorkspaceRoot[]): void;
|
|
298
|
+
/**
|
|
299
|
+
* Publish the registry-authority fact. This is the REGISTRY's write port —
|
|
300
|
+
* the counterpart of {@link setAdditionalRoots}: the registry marks the
|
|
301
|
+
* scope `unavailable` the moment it stops being the store's authority
|
|
302
|
+
* (before acquisition, on contention or storage failure, at teardown) and
|
|
303
|
+
* `available` only once its stored records have been published. Nothing
|
|
304
|
+
* else may call it, and it never touches the root table.
|
|
305
|
+
* @param available - whether this process holds the registry authority.
|
|
306
|
+
*/
|
|
307
|
+
setRegistryAuthority(available: boolean): void;
|
|
122
308
|
}
|
|
123
309
|
export default MultiRootScopeService;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function brokerPipeName(userSid: string): string;
|
|
2
|
+
/** The broker must acknowledge before any ACL edits or process creation. The
|
|
3
|
+
* caller independently reads the deny-only groups from the token;
|
|
4
|
+
* even an impersonated pipe server cannot fabricate that kernel state.
|
|
5
|
+
*/
|
|
6
|
+
export declare function requestDenyToken(userSid: string, tokenHandle: bigint, deniedIds: readonly string[]): Promise<bigint>;
|
|
7
|
+
/** Synchronous, bounded RPC inside the public suspended-create binding. The
|
|
8
|
+
* caller verifies the child's kernel token before the helper can resume it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function assignDenyToken(userSid: string, processHandle: bigint, threadHandle: bigint, deniedIds: readonly string[], io: {
|
|
11
|
+
open(pipe: string): unknown;
|
|
12
|
+
write(pipe: unknown, bytes: Buffer): void;
|
|
13
|
+
available(pipe: unknown): number;
|
|
14
|
+
read(pipe: unknown, count: number): Buffer;
|
|
15
|
+
pause(): void;
|
|
16
|
+
close(pipe: unknown): void;
|
|
17
|
+
}): void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runWindowsDenied(upstreamRunner: string, roots: readonly string[], argv: readonly string[], protectedHome?: string): Promise<number>;
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/** Strictly recognize the upstream public runner contract, failing closed on new flags. */
|
|
12
|
-
export declare function parseWindowsProfile(args: readonly string[], allowAdditional?: boolean): WindowsProfile;
|
|
13
|
-
export declare function widenWindowsProfile(profile: readonly string[], roots: readonly string[], primaryRoot?: string): string[];
|
|
1
|
+
export { parseWindowsProfile, workspaceScopeSid, type WindowsProfile } from './windows-runner-contract.ts';
|
|
2
|
+
/** Recognize the upstream runner, then select our ambient deny-token path.
|
|
3
|
+
*
|
|
4
|
+
* `protectedHome` is a diagnostics label only: when it names one of the deny
|
|
5
|
+
* roots it is forwarded as `--protected-home` so the runner can word its
|
|
6
|
+
* hardlink/scan remediation for the protected `$DSH_HOME`. It never changes
|
|
7
|
+
* which roots are denied or how.
|
|
8
|
+
*/
|
|
9
|
+
export declare function restrictWindowsProfile(profile: readonly string[], deniedRoots: readonly string[], protectedHome?: string): string[];
|
|
10
|
+
export declare function widenWindowsProfile(profile: readonly string[], roots: readonly string[], deniedRoots: readonly string[], primaryRoot?: string): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The standalone Windows runner's contract — shared between the widened
|
|
3
|
+
* profile (`windows-profile.ts`, in-process) and `windows-runner.ts` (a
|
|
4
|
+
* separate Node process).
|
|
5
|
+
*
|
|
6
|
+
* ZERO bare-specifier imports: `lib/windows-runner.js` executes outside the
|
|
7
|
+
* host's plugin package resolution, so every module it statically reaches may
|
|
8
|
+
* import `node:` builtins only. tests/windows-multi-root.spec.ts and
|
|
9
|
+
* scripts/check-pack.mjs both walk the built runner's module graph and fail on
|
|
10
|
+
* any non-`node:` specifier.
|
|
11
|
+
*
|
|
12
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/windows-runner-contract
|
|
13
|
+
*/
|
|
14
|
+
/** IPC budget, independent of the registry's configurable add limit. Historical
|
|
15
|
+
* over-limit stores retain their records. Beyond this native launch budget the
|
|
16
|
+
* whole ambient launch is refused before ACL edits; denies are never truncated.
|
|
17
|
+
* Keep the broker service's MaxDenyRoots in sync (covered by contract tests).
|
|
18
|
+
*/
|
|
19
|
+
export declare const MAX_WINDOWS_DENY_ROOTS = 256;
|
|
20
|
+
export declare function assertWindowsDenyCapacity(count: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Raised when a confined argv or a runner profile cannot be recognized or
|
|
23
|
+
* widened. The provider converts this into the fail-closed
|
|
24
|
+
* `SandboxUnavailableError`: an unrecognized dialect must never degrade into
|
|
25
|
+
* "the primary root was granted after all". Re-exported from `dialects.ts`.
|
|
26
|
+
*/
|
|
27
|
+
export declare class DialectUnrecognizedError extends Error {
|
|
28
|
+
constructor(message: string);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The canonical spelling of `path`, or `path` itself when it does not exist.
|
|
32
|
+
*
|
|
33
|
+
* Mirrors `canonicalPath` from `@deepseek-ai/dsh-sandbox`: the published
|
|
34
|
+
* package ships only its entry point, and this module may not import it at
|
|
35
|
+
* all (the runner resolves upstream packages from the upstream runner's own
|
|
36
|
+
* location, never from its own directory). Same precedent and justification
|
|
37
|
+
* as `src/containment.ts` (ADR-0002).
|
|
38
|
+
*/
|
|
39
|
+
export declare function canonicalPath(path: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether a path names a Windows network or device location: two leading
|
|
42
|
+
* slashes or backslashes in any mix (`\\server\share`, `//server/share`,
|
|
43
|
+
* `\\?\…`, `\\.\…`). Windows ACL confinement cannot be shown to cover these —
|
|
44
|
+
* the restricted token's capability SID and Low label are not what an SMB
|
|
45
|
+
* server evaluates, and merely resolving an attacker-chosen UNC path triggers
|
|
46
|
+
* SMB authentication — so they are refused lexically, before any filesystem
|
|
47
|
+
* access. Because `canonicalPath` resolves a mapped drive letter to its
|
|
48
|
+
* `\\server\share` form, checking the canonical spelling also catches mapped
|
|
49
|
+
* network drives.
|
|
50
|
+
* @param path - the path spelling to test.
|
|
51
|
+
* @returns whether it is a network or device path.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isWindowsNetworkOrDevicePath(path: string): boolean;
|
|
54
|
+
/** A root-set capability: removing or adding a root changes the token's identity. */
|
|
55
|
+
export declare function workspaceScopeSid(roots: readonly string[]): string;
|
|
56
|
+
/** A standing deny identity, deliberately disjoint from all write capabilities. */
|
|
57
|
+
export declare function rootDenySid(root: string): string;
|
|
58
|
+
export interface WindowsProfile {
|
|
59
|
+
workspace: string;
|
|
60
|
+
temp: string;
|
|
61
|
+
/**
|
|
62
|
+
* `workspace-write` grants the workspace, the private temp, and every
|
|
63
|
+
* `--additional-root`. `read-only` is legal only on the widened form (the
|
|
64
|
+
* standalone runner): the runner then grants the additional roots ALONE — a
|
|
65
|
+
* workspace-write token over just those directories — which is how a fixed
|
|
66
|
+
* `read-write` additional root stays writable inside a read-only session
|
|
67
|
+
* (B9). An upstream `read-only` profile carries no `--additional-root`, so
|
|
68
|
+
* the mode only ever reaches the runner with grants attached.
|
|
69
|
+
*/
|
|
70
|
+
mode: 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
71
|
+
additionalRoots: string[];
|
|
72
|
+
deniedRoots: string[];
|
|
73
|
+
/**
|
|
74
|
+
* The upstream runner's own file (`--upstream-runner`). Required when
|
|
75
|
+
* `allowAdditional` parses — our standalone runner resolves
|
|
76
|
+
* `@deepseek-ai/dsh-sandbox-windows-acl` by package self-reference FROM this
|
|
77
|
+
* location, the only place that package is guaranteed importable.
|
|
78
|
+
*/
|
|
79
|
+
upstreamRunner?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Diagnostics label only (`--protected-home`): which `--deny-root` is the
|
|
82
|
+
* protected `$DSH_HOME`. It changes no authorization — every deny root's
|
|
83
|
+
* policy is identical — it only selects the remediation wording in
|
|
84
|
+
* hardlink/scan diagnostics. Valid only on the standalone
|
|
85
|
+
* `danger-full-access` form.
|
|
86
|
+
*/
|
|
87
|
+
protectedHome?: string;
|
|
88
|
+
writeSid?: string;
|
|
89
|
+
tempWriteSid?: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Strictly recognize the upstream public runner contract, failing closed on
|
|
93
|
+
* new flags. `allowAdditional` is the standalone runner's own mode: it accepts
|
|
94
|
+
* repeated `--additional-root` and requires `--upstream-runner` exactly once
|
|
95
|
+
* naming an absolute, existing file — anything else exits 127 without a child.
|
|
96
|
+
*/
|
|
97
|
+
export declare function parseWindowsProfile(args: readonly string[], allowAdditional?: boolean): WindowsProfile;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, win32 } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
function assertWindowsDenyCapacity(count) {
|
|
5
|
+
if (!Number.isInteger(count) || count < 1 || count > 256) throw new DialectUnrecognizedError(`Windows ambient restriction requires 1–256 deny roots including the protected DSH home; got ${count}. Reduce the registered restrictions before launching; no deny roots were dropped`);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Raised when a confined argv or a runner profile cannot be recognized or
|
|
9
|
+
* widened. The provider converts this into the fail-closed
|
|
10
|
+
* `SandboxUnavailableError`: an unrecognized dialect must never degrade into
|
|
11
|
+
* "the primary root was granted after all". Re-exported from `dialects.ts`.
|
|
12
|
+
*/
|
|
13
|
+
var DialectUnrecognizedError = class extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "DialectUnrecognizedError";
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* The canonical spelling of `path`, or `path` itself when it does not exist.
|
|
21
|
+
*
|
|
22
|
+
* Mirrors `canonicalPath` from `@deepseek-ai/dsh-sandbox`: the published
|
|
23
|
+
* package ships only its entry point, and this module may not import it at
|
|
24
|
+
* all (the runner resolves upstream packages from the upstream runner's own
|
|
25
|
+
* location, never from its own directory). Same precedent and justification
|
|
26
|
+
* as `src/containment.ts` (ADR-0002).
|
|
27
|
+
*/
|
|
28
|
+
function canonicalPath(path) {
|
|
29
|
+
try {
|
|
30
|
+
return realpathSync.native(path);
|
|
31
|
+
} catch {
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a path names a Windows network or device location: two leading
|
|
37
|
+
* slashes or backslashes in any mix (`\\server\share`, `//server/share`,
|
|
38
|
+
* `\\?\…`, `\\.\…`). Windows ACL confinement cannot be shown to cover these —
|
|
39
|
+
* the restricted token's capability SID and Low label are not what an SMB
|
|
40
|
+
* server evaluates, and merely resolving an attacker-chosen UNC path triggers
|
|
41
|
+
* SMB authentication — so they are refused lexically, before any filesystem
|
|
42
|
+
* access. Because `canonicalPath` resolves a mapped drive letter to its
|
|
43
|
+
* `\\server\share` form, checking the canonical spelling also catches mapped
|
|
44
|
+
* network drives.
|
|
45
|
+
* @param path - the path spelling to test.
|
|
46
|
+
* @returns whether it is a network or device path.
|
|
47
|
+
*/
|
|
48
|
+
function isWindowsNetworkOrDevicePath(path) {
|
|
49
|
+
return /^[\\/]{2}/.test(path);
|
|
50
|
+
}
|
|
51
|
+
/** A root-set capability: removing or adding a root changes the token's identity. */
|
|
52
|
+
function workspaceScopeSid(roots) {
|
|
53
|
+
const paths = [...new Set(roots.map((root) => root.toLowerCase()))].sort();
|
|
54
|
+
const hash = createHash("sha256").update("dsh-multi-root-workspace\0").update(JSON.stringify(paths)).digest();
|
|
55
|
+
return `S-1-4-${[
|
|
56
|
+
0,
|
|
57
|
+
4,
|
|
58
|
+
8,
|
|
59
|
+
12
|
|
60
|
+
].map((offset) => hash.readUInt32LE(offset) % (2 ** 30 - 1) + 1).join("-")}-2`;
|
|
61
|
+
}
|
|
62
|
+
/** A standing deny identity, deliberately disjoint from all write capabilities. */
|
|
63
|
+
function rootDenySid(root) {
|
|
64
|
+
const hash = createHash("sha256").update("dsh-multi-root-deny\0").update(root.toLowerCase()).digest();
|
|
65
|
+
return `S-1-4-${[
|
|
66
|
+
0,
|
|
67
|
+
4,
|
|
68
|
+
8,
|
|
69
|
+
12
|
|
70
|
+
].map((offset) => hash.readUInt32LE(offset) % (2 ** 30 - 1) + 1).join("-")}-3`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Strictly recognize the upstream public runner contract, failing closed on
|
|
74
|
+
* new flags. `allowAdditional` is the standalone runner's own mode: it accepts
|
|
75
|
+
* repeated `--additional-root` and requires `--upstream-runner` exactly once
|
|
76
|
+
* naming an absolute, existing file — anything else exits 127 without a child.
|
|
77
|
+
*/
|
|
78
|
+
function parseWindowsProfile(args, allowAdditional = false) {
|
|
79
|
+
const fields = /* @__PURE__ */ new Map();
|
|
80
|
+
const additionalRoots = [];
|
|
81
|
+
const deniedRoots = [];
|
|
82
|
+
let upstreamRunner;
|
|
83
|
+
let protectedHome;
|
|
84
|
+
const known = /* @__PURE__ */ new Set([
|
|
85
|
+
"--workspace",
|
|
86
|
+
"--temp",
|
|
87
|
+
"--mode",
|
|
88
|
+
"--write-sid",
|
|
89
|
+
"--temp-write-sid"
|
|
90
|
+
]);
|
|
91
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
92
|
+
const key = args[index];
|
|
93
|
+
const value = args[index + 1];
|
|
94
|
+
if (key === void 0 || value === void 0 || value === "") throw new DialectUnrecognizedError("Windows ACL profile has a missing value");
|
|
95
|
+
if (allowAdditional && key === "--additional-root") {
|
|
96
|
+
additionalRoots.push(value);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (allowAdditional && key === "--deny-root") {
|
|
100
|
+
deniedRoots.push(value);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (allowAdditional && key === "--upstream-runner") {
|
|
104
|
+
if (upstreamRunner !== void 0) throw new DialectUnrecognizedError("Windows ACL profile repeats --upstream-runner");
|
|
105
|
+
upstreamRunner = value;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (allowAdditional && key === "--protected-home") {
|
|
109
|
+
if (protectedHome !== void 0) throw new DialectUnrecognizedError("Windows ACL profile repeats --protected-home");
|
|
110
|
+
protectedHome = value;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!known.has(key) || fields.has(key)) throw new DialectUnrecognizedError(`Windows ACL profile has unknown or duplicate flag ${key}`);
|
|
114
|
+
fields.set(key, value);
|
|
115
|
+
}
|
|
116
|
+
if (allowAdditional && upstreamRunner === void 0) throw new DialectUnrecognizedError("Windows multi-root runner requires --upstream-runner");
|
|
117
|
+
if (upstreamRunner !== void 0 && (!isAbsolute(upstreamRunner) || !existsSync(upstreamRunner) || !statSync(upstreamRunner).isFile())) throw new DialectUnrecognizedError(`--upstream-runner is not an absolute existing file: ${upstreamRunner}`);
|
|
118
|
+
const workspace = fields.get("--workspace");
|
|
119
|
+
const temp = fields.get("--temp");
|
|
120
|
+
const mode = fields.get("--mode");
|
|
121
|
+
if (workspace === void 0 || temp === void 0 || mode !== "workspace-write" && mode !== "read-only" && !(allowAdditional && mode === "danger-full-access")) throw new DialectUnrecognizedError("Windows multi-root requires --workspace, --temp, and --mode read-only or workspace-write");
|
|
122
|
+
if (mode === "danger-full-access" ? deniedRoots.length === 0 || additionalRoots.length > 0 || fields.has("--write-sid") || fields.has("--temp-write-sid") : deniedRoots.length > 0) throw new DialectUnrecognizedError("Windows ambient restriction requires deny roots only, without write grants or capability SIDs");
|
|
123
|
+
if (allowAdditional && mode === "read-only" && additionalRoots.length === 0) throw new DialectUnrecognizedError("Windows multi-root read-only requires at least one --additional-root");
|
|
124
|
+
const writeSid = fields.get("--write-sid");
|
|
125
|
+
const tempWriteSid = fields.get("--temp-write-sid");
|
|
126
|
+
if (writeSid === void 0 !== (tempWriteSid === void 0)) throw new DialectUnrecognizedError("Windows ACL profile requires both capability SIDs or neither");
|
|
127
|
+
if (protectedHome !== void 0 && (mode !== "danger-full-access" || !win32.isAbsolute(protectedHome) || !deniedRoots.some((root) => root.toLowerCase() === protectedHome.toLowerCase()))) throw new DialectUnrecognizedError("--protected-home must be an absolute member of the deny roots on a standalone danger-full-access profile");
|
|
128
|
+
return {
|
|
129
|
+
workspace,
|
|
130
|
+
temp,
|
|
131
|
+
mode,
|
|
132
|
+
additionalRoots,
|
|
133
|
+
deniedRoots,
|
|
134
|
+
...upstreamRunner === void 0 ? {} : { upstreamRunner },
|
|
135
|
+
...protectedHome === void 0 ? {} : { protectedHome },
|
|
136
|
+
...writeSid === void 0 ? {} : { writeSid },
|
|
137
|
+
...tempWriteSid === void 0 ? {} : { tempWriteSid }
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { parseWindowsProfile as a, isWindowsNetworkOrDevicePath as i, assertWindowsDenyCapacity as n, rootDenySid as o, canonicalPath as r, workspaceScopeSid as s, DialectUnrecognizedError as t };
|