@dsh-electron/dsh-plugin-multi-root-workspace 0.1.5 → 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 (101) hide show
  1. package/CHANGELOG.en.md +56 -1
  2. package/CHANGELOG.md +56 -1
  3. package/README.en.md +157 -92
  4. package/README.md +158 -93
  5. package/cordis.patch.yml +61 -0
  6. package/lib/client.js +2819 -115
  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-82Y27Iom.js → compat-DF_yL1bB.js} +180 -4
  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-Cn1Jj5Xi.js → instructions-D19jRxN0.js} +109 -28
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +43 -0
  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 +240 -25
  28. package/lib/session-directory-BLOstut1.js +9 -0
  29. package/lib/tool-context-CpgIFcCB.js +6 -0
  30. package/lib/types/client/RootSwitcher.d.ts +63 -0
  31. package/lib/types/client/WorkspaceFoldersAction.d.ts +41 -0
  32. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  33. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  34. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  35. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  36. package/lib/types/client/files-tree/face.d.ts +107 -0
  37. package/lib/types/client/files-tree/locales.d.ts +10 -0
  38. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  39. package/lib/types/client/files-tree/store.d.ts +81 -0
  40. package/lib/types/client/index.d.ts +13 -2
  41. package/lib/types/client/locales.d.ts +60 -0
  42. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  43. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  44. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  45. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  46. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  47. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  48. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  49. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  50. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  51. package/lib/types/client/panel-client.d.ts +2 -1
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +16 -3
  54. package/lib/types/compat/client-icons.d.ts +18 -0
  55. package/lib/types/compat/client-session.d.ts +2 -0
  56. package/lib/types/compat/dsh-version.d.ts +5 -3
  57. package/lib/types/compat/instruction-overlap.d.ts +7 -0
  58. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  59. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  60. package/lib/types/compat/process-result.d.ts +14 -0
  61. package/lib/types/compat/pty-marker.d.ts +83 -0
  62. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  63. package/lib/types/compat/session-directory.d.ts +4 -0
  64. package/lib/types/compat/storage-zod.d.ts +65 -0
  65. package/lib/types/compat/tool-context.d.ts +9 -0
  66. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  67. package/lib/types/compat/windows-native.d.ts +68 -0
  68. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  69. package/lib/types/compat/workspace-files.d.ts +7 -0
  70. package/lib/types/contract.d.ts +35 -4
  71. package/lib/types/dialects.d.ts +99 -3
  72. package/lib/types/fs.d.ts +32 -10
  73. package/lib/types/index.d.ts +5 -0
  74. package/lib/types/lsp.d.ts +4 -0
  75. package/lib/types/method-wrapper.d.ts +2 -0
  76. package/lib/types/process-fence.d.ts +54 -0
  77. package/lib/types/profile-isolation.d.ts +178 -0
  78. package/lib/types/providers.d.ts +70 -0
  79. package/lib/types/registry.d.ts +132 -31
  80. package/lib/types/roots.d.ts +100 -13
  81. package/lib/types/sandbox.d.ts +59 -12
  82. package/lib/types/scope.d.ts +203 -14
  83. package/lib/types/windows-broker.d.ts +17 -0
  84. package/lib/types/windows-deny.d.ts +1 -0
  85. package/lib/types/windows-profile.d.ts +10 -0
  86. package/lib/types/windows-runner-contract.d.ts +97 -0
  87. package/lib/types/windows-runner.d.ts +1 -0
  88. package/lib/types/workspace-files.d.ts +4 -0
  89. package/lib/types/workspace-routing.d.ts +11 -0
  90. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  91. package/lib/windows-runner.js +1311 -0
  92. package/lib/workspace-files.js +203 -0
  93. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  94. package/package.json +116 -51
  95. package/scripts/windows-broker/install.ps1 +89 -0
  96. package/scripts/windows-broker/rights.cs +58 -0
  97. package/scripts/windows-broker/service.cs +323 -0
  98. package/scripts/windows-broker/verify-install.ps1 +80 -0
  99. package/lib/command-DyOWujPo.js +0 -690
  100. package/lib/sandbox-D8Fw95FM.js +0 -290
  101. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
@@ -0,0 +1,68 @@
1
+ export type NativePointer = unknown;
2
+ export type NativeFunction = (...args: readonly unknown[]) => number;
3
+ export interface NativeApi {
4
+ [name: string]: NativeFunction;
5
+ }
6
+ export interface KoffiApi {
7
+ decode(pointer: NativePointer, type: string, length?: number): unknown;
8
+ address(pointer: NativePointer): bigint;
9
+ load(name: string): unknown;
10
+ alloc(type: string, length: number): NativePointer;
11
+ free(pointer: NativePointer): void;
12
+ encode(pointer: NativePointer, type: string, value: unknown, length?: number): void;
13
+ }
14
+ export interface WindowsNative {
15
+ koffi: KoffiApi;
16
+ extendWin32ProcessBindings(create: (context: {
17
+ kernel32: unknown;
18
+ advapi32: unknown;
19
+ bind(library: unknown, name: string, result: string, args: string[]): unknown;
20
+ }) => NativeApi): NativeApi;
21
+ spawnInheritedJobProcess(api: NativeApi, options: {
22
+ token: NativePointer;
23
+ command: string;
24
+ args: readonly string[];
25
+ cwd: string;
26
+ controlFileDescriptor?: 7;
27
+ }): {
28
+ process: NativePointer;
29
+ job: NativePointer;
30
+ };
31
+ waitForProcessExit(api: NativeApi, process: NativePointer): number;
32
+ }
33
+ /** Decode the UTF-16 pointee, not a second string pointer stored at its first
34
+ * code units. `decode(ptr, 'str16')` would dereference those units as an address.
35
+ */
36
+ export declare function readWindowsWideString(koffi: KoffiApi, pointer: NativePointer): string;
37
+ export declare function loadWindowsNative(upstreamRunner: string): Promise<WindowsNative>;
38
+ /** Older public process helpers predate fd-7 CRT forwarding. Adapt the Win32
39
+ * STARTUPINFO structure at the public binding seam, preserving argv and Job
40
+ * ownership. New helpers already populate cbReserved2 and pass through.
41
+ */
42
+ export declare function spawnWindowsDeniedChild(native: WindowsNative, api: NativeApi, options: Parameters<WindowsNative['spawnInheritedJobProcess']>[1], activation?: {
43
+ sourceToken: NativePointer;
44
+ assign(process: NativePointer, thread: NativePointer): void;
45
+ }): ReturnType<WindowsNative['spawnInheritedJobProcess']>;
46
+ /**
47
+ * Terminate a kill-on-close Job and wait until the kernel reports it drained.
48
+ *
49
+ * The public `waitForProcessExit` covers the direct child only: a descendant
50
+ * the child spawned before exiting stays in the Job and could otherwise keep
51
+ * running — with its restricted token — after the runner has released its
52
+ * ancestor pins. `TerminateJobObject(job, 127)` kills every member; the
53
+ * subsequent `QueryInformationJobObject` polling loop (class 1,
54
+ * JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, `ActiveProcesses` at byte 40) only
55
+ * returns once the kernel reports none left. There is deliberately no
56
+ * timeout: releasing pins while a live descendant remains is exactly the gap
57
+ * this wait exists to close. The caller still owns and closes the Job handle
58
+ * afterwards.
59
+ *
60
+ * Abnormal-error boundary: a TerminateJobObject or QueryInformationJobObject
61
+ * API failure throws into the caller's teardown — the Job handle is still
62
+ * closed there and its kill-on-close flag is the residual bound, same as the
63
+ * forced-termination case: if the runner process itself is killed, no
64
+ * in-process guarantee can survive, the Job flag takes the descendants down
65
+ * with it, and the pin handles die with the process. Protection is promised
66
+ * only through this orderly path.
67
+ */
68
+ export declare function terminateWindowsJobAndWait(native: WindowsNative, api: NativeApi, job: NativePointer): void;
@@ -0,0 +1,56 @@
1
+ /** Windows deny-path pinning (branch-review Phase B).
2
+ *
3
+ * The ACL writer's deny ACEs follow the file OBJECT, not the registered
4
+ * spelling: a restricted process that renames a deny root's ancestor moves
5
+ * the protected tree to a NEW path the recorded deny set does not name — the
6
+ * kernel still refuses that token's writes through the moved ACEs, but the
7
+ * host fs fence, which matches the recorded spellings, no longer covers the
8
+ * moved objects (the shell → host new-path gap, R1). This guard pins every
9
+ * ancestor directory of every deny root with an open handle that shares read
10
+ * and write but NOT delete — so a rename or delete of the registered tree
11
+ * fails for the lifetime of the restricted child, without writing a single
12
+ * persistent ACL on `C:\`, user directories, or anywhere else.
13
+ *
14
+ * `windows-deny.ts` owns the lifetime contract: handles are held until the
15
+ * confined child AND its drained Job descendants have exited.
16
+ *
17
+ * Primitives are bound through the public `extendWin32ProcessBindings` seam —
18
+ * no upstream private file, no new native dependency. Every verification uses
19
+ * the same handle that pins the directory, so the checked object and the
20
+ * pinned object are always the same one.
21
+ */
22
+ import type { NativeApi, NativePointer, WindowsNative } from './windows-native.ts';
23
+ /** What `GetFileInformationByHandle` reports for one pinned object. */
24
+ export interface WindowsPathIdentity {
25
+ readonly attributes: number;
26
+ readonly volumeSerial: number;
27
+ readonly fileIndex: bigint;
28
+ readonly links: number;
29
+ }
30
+ /** Held ancestor pins. {@link WindowsPathGuard.close} releases every handle. */
31
+ export interface WindowsPathGuard {
32
+ close(): void;
33
+ }
34
+ /** BY_HANDLE_FILE_INFORMATION is 52 bytes (FILETIME members are DWORD-pairs):
35
+ * attributes @0, volume serial @28, link count @40, file index high @44 /
36
+ * low @48. Read back on the same handle that pins the path, so the identity
37
+ * cannot drift between check and pin.
38
+ */
39
+ export declare function readWindowsPathIdentity(api: NativeApi, handle: NativePointer): WindowsPathIdentity;
40
+ /**
41
+ * Open a pin handle on every ancestor of every deny root — including the deny
42
+ * root itself — and hold them all until {@link WindowsPathGuard.close}. Each
43
+ * handle is opened with GENERIC_READ, read+write sharing, and a null security
44
+ * descriptor so it is non-inheritable; the missing DELETE sharing is what
45
+ * blocks renames, while ordinary reads and writes (and creating sibling files
46
+ * inside a pinned directory) keep working.
47
+ *
48
+ * Every pin is verified on the same handle: the object must be a directory,
49
+ * must not be a reparse point, must not carry the inherit flag (so a confined
50
+ * child cannot simply `CloseHandle` the pin away), and its kernel-reported
51
+ * final DOS path must equal the expected canonical spelling.
52
+ * Any failure — open, verification, or a sharing conflict with a handle some
53
+ * other component already holds — closes every earlier pin in reverse order
54
+ * and refuses, keeping the launch fail-closed before spawn.
55
+ */
56
+ export declare function pinWindowsDenyPaths(native: WindowsNative, roots: readonly string[]): WindowsPathGuard;
@@ -0,0 +1,7 @@
1
+ /** Old releases watch a session; newer ones watch a path. Probe arity, never version. */
2
+ import type { WorkspaceFiles, WorkspaceFileScope, WorkspaceFileWatchFrame } from '@deepseek-ai/dsh-api-workspace-files';
3
+ type PathWatch = (this: WorkspaceFiles, scope: WorkspaceFileScope, path: string, signal: AbortSignal) => AsyncIterable<WorkspaceFileWatchFrame>;
4
+ export declare function adaptPathWatch(service: WorkspaceFiles, wrap: (original: PathWatch) => PathWatch): () => void;
5
+ /** Match both legacy `readRelated` and modern `readBytes({ baseFile })` tail resolution. */
6
+ export declare function resolveRelatedWorkspacePath(absoluteBase: string, tail: string): string;
7
+ export {};
@@ -12,8 +12,9 @@
12
12
  * - **One shape per endpoint.** {@link PanelResponseMap} is the single mapping
13
13
  * from endpoint to response type, so the panel cannot silently decode one
14
14
  * endpoint's answer as another's. `reveal` answers with
15
- * {@link RevealedView}; every other endpoint answers with a whole
16
- * {@link RootsView}. The host builds those answers in `command.ts` and the
15
+ * {@link RevealedView}; root-management endpoints answer with
16
+ * {@link RootsView}.
17
+ * The host builds those answers in `command.ts` and the
17
18
  * panel parses them here — one definition, two consumers.
18
19
  * - **Runtime validation at the boundary.** The payload arrives from a browser
19
20
  * and the answer arrives from the host; neither is trusted to match the
@@ -30,7 +31,7 @@
30
31
  /** The absolute logical channel both halves speak over. */
31
32
  export declare const PANEL_CHANNEL = "/multi-root-workspace";
32
33
  /** Endpoint names, relative to {@link PANEL_CHANNEL}. */
33
- export type PanelEndpoint = 'list' | 'add' | 'remove' | 'alias' | 'move' | 'reveal';
34
+ export type PanelEndpoint = 'list' | 'add' | 'remove' | 'alias' | 'move' | 'setAccess' | 'reveal';
34
35
  /** Every endpoint name, for exhaustive iteration in tests and callers. */
35
36
  export declare const PANEL_ENDPOINTS: readonly PanelEndpoint[];
36
37
  /**
@@ -40,6 +41,17 @@ export declare const PANEL_ENDPOINTS: readonly PanelEndpoint[];
40
41
  * value the two sides share is spelled out rather than imported.
41
42
  */
42
43
  export type RootState = 'available' | 'missing' | 'redirected' | 'invalid';
44
+ /**
45
+ * The write authority one registration carries (B9 three-state), spelled out
46
+ * rather than imported: this module is inlined into the browser bundle and
47
+ * must not pull in `node:*`, so the one value the two sides share is
48
+ * duplicated here.
49
+ */
50
+ export type RootAccess = 'inherit' | 'read-write' | 'read-only';
51
+ export type WorkspaceMode = 'read-only' | 'workspace-write' | 'danger-full-access';
52
+ export type EffectiveRootAccess = 'read-only' | 'read-write' | 'full-access' | 'unavailable';
53
+ /** The shared nine-cell rule; tool dispatch may subsequently narrow writes. */
54
+ export declare function effectiveRootAccess(access: RootAccess, mode: WorkspaceMode): EffectiveRootAccess;
43
55
  /** One root as the panel renders it. */
44
56
  export interface RootView {
45
57
  /** 1-based position in the returned snapshot. */
@@ -54,6 +66,10 @@ export interface RootView {
54
66
  readonly addedAt: string;
55
67
  /** Whether the directory is writable right now, withheld, or unusable. */
56
68
  readonly state: RootState;
69
+ /** The write authority the registration carries. */
70
+ readonly access: RootAccess;
71
+ /** Host-derived session permission, before any per-tool cap. */
72
+ readonly effectiveAccess?: EffectiveRootAccess;
57
73
  /** Why the root is not `available`; a code the panel localizes. */
58
74
  readonly detail?: string;
59
75
  }
@@ -64,7 +80,7 @@ export interface RootEntryView {
64
80
  readonly path: string;
65
81
  readonly addedAt: string;
66
82
  }
67
- /** The panel's whole view of one workspace root; every endpoint but `reveal` answers with this. */
83
+ /** The panel's whole view, returned by root-management endpoints. */
68
84
  export interface RootsView {
69
85
  /** The canonical workspace root these registrations belong to. */
70
86
  readonly primaryRoot: string;
@@ -74,8 +90,20 @@ export interface RootsView {
74
90
  * basename.
75
91
  */
76
92
  readonly primaryName?: string;
93
+ /** Execution directory only; never a registry key or an authorization grant. */
94
+ readonly effectiveCwd?: string;
95
+ readonly mode?: WorkspaceMode;
96
+ readonly cappedTools?: readonly string[];
97
+ readonly toolAccessInvalid?: boolean;
77
98
  /** The additional roots, in registry order. */
78
99
  readonly roots: readonly RootView[];
100
+ /** The host cap; MAX_SAFE_INTEGER means a legacy host reported no limit. */
101
+ readonly limit: number;
102
+ /**
103
+ * How many records are stored, in every state. `count > limit` is a
104
+ * historical over-limit store: everything stays granted, only adds refuse.
105
+ */
106
+ readonly count: number;
79
107
  /** Set when the store itself could not be read; the panel shows this verbatim. */
80
108
  readonly unavailable?: string;
81
109
  }
@@ -91,6 +119,7 @@ export interface PanelResponseMap {
91
119
  readonly remove: RootsView;
92
120
  readonly alias: RootsView;
93
121
  readonly move: RootsView;
122
+ readonly setAccess: RootsView;
94
123
  readonly reveal: RevealedView;
95
124
  }
96
125
  /**
@@ -110,6 +139,8 @@ export interface PanelRequest {
110
139
  readonly path?: string;
111
140
  /** Display alias (add/alias); empty clears it. */
112
141
  readonly alias?: string;
142
+ /** Write authority (add — defaults to `inherit` — and setAccess). */
143
+ readonly access?: RootAccess;
113
144
  /** Exact anchor row (move): the target is placed in front of it; absent moves it last. */
114
145
  readonly beforeEntry?: RootEntryView;
115
146
  /** Compatibility anchor; accepted only when the id is unique. */
@@ -35,16 +35,20 @@
35
35
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/dialects
36
36
  */
37
37
  import type { SandboxPolicy } from '@deepseek-ai/dsh-sandbox';
38
+ import { DialectUnrecognizedError } from './windows-runner-contract.ts';
38
39
  /** The kernel runner dialects a confined argv can speak. */
39
40
  export type RunnerDialect = 'seatbelt' | 'bwrap' | 'landlock' | 'windows-acl';
40
41
  /**
41
42
  * Raised when a confined argv cannot be recognized or widened. The provider
42
43
  * converts this into the fail-closed `SandboxUnavailableError`: an unrecognized
43
44
  * dialect must never degrade into "the primary root was granted after all".
45
+ *
46
+ * The class itself lives in `windows-runner-contract.ts`: the standalone
47
+ * Windows runner's module graph may import node builtins only, so the shared
48
+ * error type cannot sit in a module that (even type-only) references upstream
49
+ * packages. Re-exported here unchanged — `instanceof` identity is preserved.
44
50
  */
45
- export declare class DialectUnrecognizedError extends Error {
46
- constructor(message: string);
47
- }
51
+ export { DialectUnrecognizedError };
48
52
  /** One wrapped argv split into its runner/profile part and the caller's argv. */
49
53
  export interface ConfinedShape {
50
54
  /** The recognized dialect. */
@@ -90,3 +94,95 @@ export declare function detectDialect(profileArgs: readonly string[]): RunnerDia
90
94
  * @returns the widened profile arguments.
91
95
  */
92
96
  export declare function widenProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], policy: SandboxPolicy, additionalRoots: readonly string[]): string[];
97
+ /**
98
+ * Graft writable grants onto a `read-only` profile — the B9 cell where a fixed
99
+ * `read-write` additional root stays writable while the session mode denies
100
+ * every other write. Each dialect appends its own grant spelling AFTER
101
+ * upstream's deny-all baseline, so the appended form wins:
102
+ *
103
+ * - seatbelt: SBPL is last-match-wins — a trailing `(allow file-write*
104
+ * (subpath …))` overrides the blanket `(deny file-write*)` for those roots;
105
+ * - bwrap: mounts apply in order — a `--bind W W` after `--ro-bind / /`
106
+ * re-mounts that subtree writable inside the read-only namespace;
107
+ * - landlock: grants union — an extra `--rw W` widens exactly that subtree.
108
+ *
109
+ * `windows-acl` cannot be spelled this way and goes through
110
+ * `widenWindowsProfile` (the standalone runner grants only the listed roots).
111
+ * @param dialect - the recognized dialect.
112
+ * @param profileArgs - the upstream read-only profile arguments.
113
+ * @param additionalRoots - canonical roots the effective scope keeps writable.
114
+ * @returns the grafted profile arguments.
115
+ */
116
+ export declare function graftProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], additionalRoots: readonly string[]): string[];
117
+ /**
118
+ * Deny `deniedRoots` inside an upstream `workspace-write` profile — the B9 edge
119
+ * where an explicit `read-only` (or tool-capped) additional root sits under a
120
+ * directory the profile grants writable (the platform temp area, which an
121
+ * additional root may legitimately be registered beneath).
122
+ *
123
+ * A denied root outside every grant needs no profile work at all: the mode
124
+ * already denies it. Per dialect:
125
+ *
126
+ * - seatbelt appends a trailing `(deny file-write* (subpath …) (literal …))`
127
+ * form — last match wins over every earlier allow, and a redundant deny on
128
+ * an ungranted root is harmless. The literals pin every ancestor of a denied
129
+ * root: Seatbelt checks the operated path only, so `rename`ing an ancestor
130
+ * would otherwise move the whole tree out from under its `subpath` deny;
131
+ * - bwrap re-mounts the root `--ro-bind` AFTER the writable mounts (a later
132
+ * mount wins inside the namespace). The source is mandatory: if a denied
133
+ * root disappears, namespace setup fails before the command can recreate it.
134
+ * This also restores read access to denied roots under a `--tmpfs` target.
135
+ * Ancestors of a denied root that lie strictly inside a writable BIND target
136
+ * are self-bound (`--bind A A`) BEFORE the ro-binds: the kernel refuses to
137
+ * rename a mountpoint, so the deny can no longer be walked off by renaming
138
+ * the tree's parent. Ancestors inside a `--tmpfs` target are never bound —
139
+ * a rename there only moves namespace-private mounts, while the self-bind
140
+ * would expose and make writable the host content the tmpfs hides — and
141
+ * ancestors outside every writable target are not renamable anyway;
142
+ * - landlock cannot subtract: a denied root under an `--rw` grant throws —
143
+ * union semantics have no deny primitive;
144
+ * - windows-acl goes through `widenWindowsProfile`, whose runner-side ACL
145
+ * grant cannot subtract either — its caller checks the same overlap. The
146
+ * deny ACE does follow its directory object through a parent rename, but
147
+ * the moved tree's NEW path no longer matches the recorded deny set — so
148
+ * the ambient deny runner pins every ancestor of each deny root with a
149
+ * non-delete-sharing handle for the child lifetime (`windows-path-guard.ts`,
150
+ * `windows-deny.ts`).
151
+ * @param dialect - the recognized dialect.
152
+ * @param profileArgs - the upstream workspace-write profile arguments.
153
+ * @param deniedRoots - canonical roots this call must not write.
154
+ * @returns the profile arguments with the in-profile denies applied.
155
+ */
156
+ export declare function denyProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], deniedRoots: readonly string[]): string[];
157
+ /**
158
+ * Restrict an ambient-writable execution to everything EXCEPT `deniedRoots` —
159
+ * the B9 cell `danger-full-access` + explicit `read-only`. The consumers of
160
+ * this profile (shell, PTY, PTC) bypass `confine()` entirely under that mode,
161
+ * so the process fence builds this profile from an upstream skeleton instead:
162
+ * the caller probes `confine` once under `read-only` to obtain the runner's
163
+ * argument prefix, and this function rewrites the file-effect part.
164
+ *
165
+ * - seatbelt: `(version 1) (allow default)` kept from the profile head, plus a
166
+ * trailing `(deny file-write* (subpath …) (literal …))` — allow-default with
167
+ * subpath carve-outs, the inverse of the confined profiles. The literals
168
+ * pin every ancestor of a denied root, so `rename`ing the tree's parent
169
+ * cannot walk it out from under the deny (Seatbelt checks the operated
170
+ * path only — verified natively on macOS);
171
+ * - bwrap: `--ro-bind / /` becomes `--bind / /` (ambient writable namespace),
172
+ * each denied root is re-mounted `--ro-bind` after it, and every ancestor
173
+ * of a denied root is self-bound `--bind A A` between the root mount and the
174
+ * ro-binds — a mountpoint cannot be renamed (EBUSY), so the deny follows no
175
+ * rename of the tree's ancestors. A missing source fails the launch,
176
+ * retaining the registered negative authorization;
177
+ * - landlock cannot express "everything except" — its launcher only unions
178
+ * allowlists, so it throws. windows-acl expresses the cell through the
179
+ * standalone ambient-deny runner instead of this profile rewrite: standing
180
+ * deny ACEs on every denied object, plus ancestor pin handles held through
181
+ * the child and its drained Job descendants so no rename can vacate the
182
+ * registered spelling (`windows-deny.ts`, `windows-path-guard.ts`).
183
+ * @param dialect - the recognized dialect.
184
+ * @param profileArgs - the upstream read-only skeleton's profile arguments.
185
+ * @param deniedRoots - canonical roots the execution must not write.
186
+ * @returns the restricted profile arguments.
187
+ */
188
+ export declare function restrictProfileArgs(dialect: RunnerDialect, profileArgs: readonly string[], deniedRoots: readonly string[]): string[];
package/lib/types/fs.d.ts CHANGED
@@ -65,22 +65,44 @@ export declare class MultiRootFileSystem extends LocalFileSystem {
65
65
  version: FsVersion;
66
66
  }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
67
67
  /**
68
- * The writable roots of one workspace-write call: the upstream derivation
69
- * (primary root plus the platform temp areas, canonical and deduplicated)
70
- * widened by the scope's additional roots, in scope order.
68
+ * The writable roots of one confined call: the upstream derivation (primary
69
+ * root plus the platform temp areas, canonical and deduplicated — empty
70
+ * under `read-only`) widened by the scope's EFFECTIVE writable members.
71
+ * Under `read-only` that is exactly the fixed `read-write` roots (the tool
72
+ * cap may already have emptied the scope's list), so an explicit grant
73
+ * survives a globally read-only session. An unavailable registry authority
74
+ * never contributes members: a process that cannot read the store grants
75
+ * only the primary root (and no shared temp grant).
71
76
  * @param policy - the per-call policy.
72
77
  * @param scope - the scope resolved once for this call; both the fence and
73
78
  * the denial message below share it instead of re-resolving.
74
- * @returns the canonical writable roots; empty under `read-only`.
79
+ * @returns the canonical writable roots; possibly empty.
75
80
  */
76
81
  private rootsFor;
77
82
  /**
78
- * Enforce the per-call policy against `target` and return the EXACT target the
79
- * mutation must use, so the checked identity is the mutated one (no
80
- * check-here-write-there TOCTOU). `read-only` denies; `workspace-write`
81
- * re-canonicalizes NOW (reflecting a concurrently swapped symlink ancestor),
82
- * requires containment under one of the scope's writable roots, and returns
83
- * THAT fresh target; `danger-full-access` returns the caller's target unfenced.
83
+ * Enforce the per-call policy against `target` and return the EXACT target
84
+ * the mutation must use, so the checked identity is the mutated one (no
85
+ * check-here-write-there TOCTOU). Deny precedes containment: a root in
86
+ * `scope.deniedRoots` — an explicit `read-only` root, or any member when the
87
+ * call is tool-capped — refuses writes under EVERY mode, including
88
+ * `danger-full-access` and where a broader grant (the platform temp area)
89
+ * would otherwise cover the path. `read-only` then grants only the fixed
90
+ * `read-write` members; `workspace-write` requires containment under the
91
+ * widened writable set.
92
+ *
93
+ * `danger-full-access` is the only mode with a fast path, and only while
94
+ * the deny set is empty: it then returns the caller's target untouched,
95
+ * upstream-exact. Once ANY deny boundary exists the caller's `FsTarget` may
96
+ * be stale — the directory it resolved through can since have been replaced
97
+ * by a symlink into a denied root — so the fence re-resolves
98
+ * `target.displayPath`, checks the FRESH target key, and mutates that fresh
99
+ * target rather than the stale one the caller resolved earlier.
100
+ *
101
+ * One more Full-access rule: while the registry authority is `unavailable`
102
+ * the empty deny set is not trustworthy — the store this process cannot
103
+ * read may hold explicit `read-only` registrations — so the mode degrades
104
+ * to primary-root-only writes (no shared temp or additional-root grants)
105
+ * until the authority returns.
84
106
  * @param target - the resolved target about to be mutated.
85
107
  * @param sandboxPolicy - the per-call policy; omit for the deployment fallback.
86
108
  * @returns the fresh canonical target the mutation must use.
@@ -25,12 +25,16 @@ export { classifyInstallation, compatEnforcement, CORE_PACKAGES, OPTIONAL_CORE_P
25
25
  export type { CompatEnforcement, CompatReport, CompatVerdict, InspectedPackage, SupportedDshRelease, VersionReader, } from './compat/dsh-version.ts';
26
26
  export { instructionsApi, renderInstructions } from './compat/agent-instructions.ts';
27
27
  export type { InstructionsApi, RenderedInstructions } from './compat/agent-instructions.ts';
28
+ export { AGENT_PTY_MARKER, agentPtyMarkerVerdict, assertAgentPtyMarker, PtyMarkerContractError } from './compat/pty-marker.ts';
29
+ export type { AgentPtyMarkerIo, AgentPtyMarkerVerdict } from './compat/pty-marker.ts';
28
30
  export { createInstructionMessage } from './compat/llm-message.ts';
29
31
  export type { InstructionMessageInput } from './compat/llm-message.ts';
30
32
  export { widenConfined } from './compat/sandbox-confine.ts';
31
33
  export type { ConfineCall, UpstreamConfined } from './compat/sandbox-confine.ts';
32
34
  export { composeInstructionMessage, DEFAULT_MAX_BYTES, PLUGIN_SOURCE } from './instructions.ts';
33
35
  export type { Config as InstructionsConfig } from './instructions.ts';
36
+ export { MultiRootProviders, brandMultiRootProvider, isMultiRootProvider, MULTI_ROOT_PROVIDER_BRAND } from './providers.ts';
37
+ export type { ProviderKind } from './providers.ts';
34
38
  export { MultiRootFileSystem } from './fs.ts';
35
39
  export type { Config as FileSystemConfig } from './fs.ts';
36
40
  export { MultiRootSandboxProvider } from './sandbox.ts';
@@ -38,6 +42,7 @@ export type { Config as SandboxConfig } from './sandbox.ts';
38
42
  export { MultiRootScopeService, sanitizeAdditionalRoots, renderWorkspaceRootsContext } from './scope.ts';
39
43
  export type { AdditionalWorkspaceRoot, FilesystemScope } from './scope.ts';
40
44
  export { MultiRootRegistry, DOMAIN_NAME, MAX_ALIAS_LENGTH, REGISTRY_CONTENDED_MESSAGE, multiRootDomainSpec } from './registry.ts';
45
+ export { DEFAULT_MAX_ADDITIONAL_ROOTS, MAX_ADDITIONAL_ROOTS_CEILING } from './registry.ts';
41
46
  export type { AddRootInput, Config as RegistryConfig, PersistedPrimaryRoot, RegistryAuthorityState } from './registry.ts';
42
47
  export { RegistryAuthorityLease, RegistryLeaseContendedError } from './registry-lease.ts';
43
48
  export { availableRoots, canonicalRoot, classifyStoredRoots, expandRootInput, indexedStatuses, isCanonicallyUnder, additionalRootId, removeStatusAt, resolveRootRef, RootValidationError, validateRootCandidate, } from './roots.ts';
@@ -0,0 +1,4 @@
1
+ /** Add root routing at the public LSP query seam; upstream owns servers and disposal. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export declare const inject: string[];
4
+ export declare function apply(ctx: Context): void;
@@ -0,0 +1,2 @@
1
+ /** Wrap a public method on a Cordis service without capturing a caller-context proxy. */
2
+ export declare function wrapMethod<T extends object, K extends keyof T>(target: T, key: K, wrap: (original: T[K]) => T[K]): () => void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `multi-root-process-fence`: the B9 seam for the one cell `ctx.sandbox.confine`
3
+ * never sees — `danger-full-access` with explicit `read-only` additional roots.
4
+ *
5
+ * Upstream, every confined consumer routes through `ctx.sandbox.confine`, which
6
+ * this bundle's provider already widens (grants, the read-only fixed-write
7
+ * graft, and the workspace-write deny edge). But the shell executor, the PTC
8
+ * runtime and the PTY backend all short-circuit that mode: `policy.mode ===
9
+ * 'danger-full-access'` returns the bare argv, so an explicit `read-only` root
10
+ * would silently stay writable. This row closes the gap without touching any
11
+ * upstream executor:
12
+ *
13
+ * - `ctx.subprocess.spawn` is wrapped to prepend a precomputed restricted
14
+ * profile whenever the AsyncLocalStorage below carries one. `spawn` is
15
+ * synchronous, so the profile — which never depends on the callee's argv —
16
+ * is built by the wrapping consumer, not here.
17
+ * - `ctx.shell.execute` (bash and pwsh alike, and `start` where an older
18
+ * release still exposes it — `compat/process-entrypoints.ts` owns which
19
+ * entries a mounted executor actually carries) resolves the per-call policy
20
+ * exactly the way the executor itself does, and — under Full-access with
21
+ * deny roots — asks the sandbox provider for the restricted prefix and runs
22
+ * the call inside the store, so every subprocess the executor launches is
23
+ * confined.
24
+ * - `ctx.ptcRuntime.run` does the same for `run_code`.
25
+ * - `ctx.subprocess.spawnTerminal` keys on the `DSH_PTY_SESSION_ID` marker:
26
+ * every supported `dsh-terminal-bash` backend stamps it (alongside
27
+ * `DSH_SESSION_ID`) when it launches an agent's PTY, and no other caller —
28
+ * above all the panel's terminal controller — ever sets it. A spec without
29
+ * the marker is a USER terminal, passed through untouched in every mode:
30
+ * the sandbox protects agent behavior, and upstream never confines the
31
+ * user's own terminal. Because that marker is the ONLY discriminator, the
32
+ * `multi-root-compat` gate asserts it at boot (`compat/pty-marker.ts`):
33
+ * an installed backend whose emitted code no longer stamps it fails the
34
+ * plugin load rather than silently reclassifying agent PTYs as the user's.
35
+ * `ctx.terminals` itself is deliberately NOT wrapped:
36
+ * upstream mounts it inside `optional-persistent-terminals` with
37
+ * `isolate: { terminals: true }`, so the registry entry is unreachable from
38
+ * this row — the shared `spawnTerminal` seam plus the env discriminator
39
+ * covers both the un-isolated SDK mount and the isolated web mount.
40
+ *
41
+ * The same seam carries the unknown-authority downgrade: while the scope
42
+ * reports `authority: 'unavailable'` — a contended, storage-failed or
43
+ * config-invalid registry publishes an empty deny set that is NOT evidence —
44
+ * `danger-full-access` executions run inside a primary-root-only
45
+ * profile (with the deny set applied) instead of ambient.
46
+ *
47
+ * Windows uses the standalone ambient deny-token runner. Landlock cannot subtract a subtree, so that combination selects bubblewrap.
48
+ * An unusable mount runner fails without starting an unconfined process.
49
+ *
50
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/process-fence
51
+ */
52
+ import type { Context } from '@deepseek-ai/cordis';
53
+ export declare const inject: string[];
54
+ export declare function apply(ctx: Context): void;