@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.
Files changed (93) hide show
  1. package/CHANGELOG.en.md +44 -1
  2. package/CHANGELOG.md +44 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +57 -0
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +1 -1
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/contract.d.ts +31 -19
  64. package/lib/types/dialects.d.ts +99 -3
  65. package/lib/types/fs.d.ts +32 -10
  66. package/lib/types/index.d.ts +7 -2
  67. package/lib/types/process-fence.d.ts +54 -0
  68. package/lib/types/profile-isolation.d.ts +178 -0
  69. package/lib/types/providers.d.ts +70 -0
  70. package/lib/types/registry.d.ts +132 -31
  71. package/lib/types/roots.d.ts +100 -13
  72. package/lib/types/sandbox.d.ts +59 -3
  73. package/lib/types/scope.d.ts +200 -14
  74. package/lib/types/windows-broker.d.ts +17 -0
  75. package/lib/types/windows-deny.d.ts +1 -0
  76. package/lib/types/windows-profile.d.ts +10 -13
  77. package/lib/types/windows-runner-contract.d.ts +97 -0
  78. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  79. package/lib/windows-runner.js +1256 -13
  80. package/lib/workspace-files.js +138 -3
  81. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  82. package/package.json +84 -62
  83. package/scripts/windows-broker/install.ps1 +89 -0
  84. package/scripts/windows-broker/rights.cs +58 -0
  85. package/scripts/windows-broker/service.cs +323 -0
  86. package/scripts/windows-broker/verify-install.ps1 +80 -0
  87. package/lib/command-CTdNvwt1.js +0 -791
  88. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  89. package/lib/sandbox-BRXl_FpF.js +0 -132
  90. package/lib/types/client/RootFileTree.d.ts +0 -12
  91. package/lib/types/panel-files.d.ts +0 -3
  92. package/lib/windows-profile-CiFsFYIV.js +0 -216
  93. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -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 to
46
- * the primary root, and empty in M1.
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
- /** Extra writable roots granted inside the same sandbox mechanism. */
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
- * Render the workspace topology the model is told about. Only roots are listed
80
- * (never files), and the sentence states the two facts a denied write would
81
- * otherwise have to teach: that the additional roots are part of the SAME
82
- * workspace, and that the session cwd is unchanged (requirement §5, §8).
83
- * @param primaryRoot - the canonical session workspace root.
84
- * @param additionalRoots - canonical additional roots, in scope order.
85
- * @returns the stable topology sentence.
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(primaryRoot: string, additionalRoots: readonly string[]): string;
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
- constructor(ctx: Context);
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
- * The additional roots registered for one canonical primary root.
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
- /** A root-set capability: removing or adding a root changes the token's identity. */
2
- export declare function workspaceScopeSid(roots: readonly string[]): string;
3
- export interface WindowsProfile {
4
- workspace: string;
5
- temp: string;
6
- mode: 'workspace-write';
7
- additionalRoots: string[];
8
- writeSid?: string;
9
- tempWriteSid?: string;
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 };