dsh-plugin-wsl-env 0.7.5 → 0.9.0
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.md +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-wsl — run a DeepSeek Harness session against a WSL distro.
|
|
3
|
+
*
|
|
4
|
+
* DeepSeek Harness is a Cordis application whose capabilities are exposed as
|
|
5
|
+
* service seams: `ctx.fs` (filesystem), `ctx.shell` (command execution),
|
|
6
|
+
* `ctx.sandbox` (confinement), `ctx.directoryPicker`, `ctx.workspaceRegistry`.
|
|
7
|
+
* The model-facing tools (`dsh-tool-fs`, `dsh-tool-fs-search`, `dsh-tool-bash`)
|
|
8
|
+
* consume those seams and never talk to a filesystem or a shell directly. A WSL
|
|
9
|
+
* integration is therefore a seam provider, not a new tool.
|
|
10
|
+
*
|
|
11
|
+
* This package provides two of them:
|
|
12
|
+
*
|
|
13
|
+
* - {@link WslShellExecutor} registers `ctx.shell` and runs every command
|
|
14
|
+
* inside the distro as `wsl.exe -d <distro> --cd <linux dir> --exec <shell> -lc <cmd>`,
|
|
15
|
+
* where `<shell>` is the distro user's login shell (not a hardcoded bash).
|
|
16
|
+
* - {@link WslFileSystem} registers `ctx.fs` and maps Linux paths onto the
|
|
17
|
+
* distro's UNC share so the host's own fs stack (including the packaged
|
|
18
|
+
* ripgrep used by `dsh-tool-fs-search`) reads and writes real distro files.
|
|
19
|
+
*
|
|
20
|
+
* ## The one constraint that shapes everything
|
|
21
|
+
*
|
|
22
|
+
* On Windows, DSH confines commands with `dsh-sandbox-windows-acl`: a
|
|
23
|
+
* restricted, low-integrity token plus a capability-SID write allowlist. That
|
|
24
|
+
* token cannot reach WSL at all — `wsl.exe` fails with `Wsl/E_ACCESSDENIED` and
|
|
25
|
+
* the UNC share `\\wsl.localhost\<distro>` reports access denied. Both work
|
|
26
|
+
* normally outside the sandbox.
|
|
27
|
+
*
|
|
28
|
+
* The confinement is applied by the *sandboxing* executor (`pwsh-sandbox`),
|
|
29
|
+
* which wraps the argv through `ctx.sandbox.confine`. This plugin therefore
|
|
30
|
+
* subclasses the NON-sandboxing `LocalBashExecutor`, and inherits
|
|
31
|
+
* `sandboxMode === undefined` from the seam — which is the honest capability
|
|
32
|
+
* fact: the tool layer reads it and advertises that these commands are not
|
|
33
|
+
* confined. WSL access is inherently outside the harness's file sandbox; do not
|
|
34
|
+
* paper over that by reporting a mode this provider does not enforce.
|
|
35
|
+
*
|
|
36
|
+
* This is a TypeScript source built to `lib/index.js`; edit THIS file and run
|
|
37
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
38
|
+
* fails when it drifts.
|
|
39
|
+
*
|
|
40
|
+
* @module dsh-plugin-wsl
|
|
41
|
+
*/
|
|
42
|
+
import z from "@deepseek-ai/schemastery";
|
|
43
|
+
import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
|
|
44
|
+
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
45
|
+
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
46
|
+
import { WslSandbox } from "./sandbox.js";
|
|
47
|
+
import type { WslAgent } from "./agent.js";
|
|
48
|
+
import { AgentSubstrate } from "./fs-substrate.js";
|
|
49
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
50
|
+
import type { FsDirEntry, FsEditOutcome, FsEditRequest, FsInfo, FsPathInfo, FsTarget, FsVersion, FsWriteIntent, FsWriteOutcome } from "@deepseek-ai/dsh-fs";
|
|
51
|
+
import type { SandboxExecutionPolicy, SandboxMode } from "@deepseek-ai/dsh-sandbox";
|
|
52
|
+
import type { SubprocessSpawnSpec } from "@deepseek-ai/dsh-subprocess";
|
|
53
|
+
/**
|
|
54
|
+
* Upstream shell vocabulary, derived through `@deepseek-ai/dsh-bash-local` —
|
|
55
|
+
* this package does not depend on `@deepseek-ai/dsh-shell` directly, so the
|
|
56
|
+
* execution types ride in through the executor's own signatures.
|
|
57
|
+
*/
|
|
58
|
+
export type ShellExecSpec = Parameters<LocalBashExecutor["execute"]>[0];
|
|
59
|
+
export type ShellExecRequest = Parameters<LocalBashExecutor["resolve"]>[0];
|
|
60
|
+
export type ShellExecution = Awaited<ReturnType<LocalBashExecutor["execute"]>>;
|
|
61
|
+
/**
|
|
62
|
+
* One confined command's distro-side profile, as the confinement returns it:
|
|
63
|
+
* the wrapped argv, the enforcement completeness, the denial dialect, the
|
|
64
|
+
* runner-failure rules and the Windows-drive masking fact.
|
|
65
|
+
*/
|
|
66
|
+
export type ConfinedProfile = Awaited<ReturnType<InstanceType<typeof WslSandbox>["confine"]>>;
|
|
67
|
+
/**
|
|
68
|
+
* The resolved spec this executor stamps with its home-marker fields: an empty
|
|
69
|
+
* `workdir` plus the relative tail that {@link WslShellExecutor.withDefaultWorkdir}
|
|
70
|
+
* joins against the distro home. The extra keys are this provider's own; the
|
|
71
|
+
* seam's `ShellExecSpec` does not know them.
|
|
72
|
+
*/
|
|
73
|
+
export type WslExecSpec = ShellExecSpec & {
|
|
74
|
+
relativeWorkdir?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The WSL shell executor's validated config — the keys of
|
|
78
|
+
* {@link WslShellExecutor.Config}, which the upstream `LocalBashExecutor`
|
|
79
|
+
* config type does not know. The seven volatile fields arrive as live
|
|
80
|
+
* wrappers, so they are spelled as their `.get()` shape (the reading
|
|
81
|
+
* {@link configuredCwd} and `hostCwd` perform).
|
|
82
|
+
*/
|
|
83
|
+
export interface WslShellExecutorConfig {
|
|
84
|
+
/** The pinned distro; empty means WSL's default. */
|
|
85
|
+
distro: string;
|
|
86
|
+
/** The `wsl.exe` path. */
|
|
87
|
+
wslPath: string;
|
|
88
|
+
/** The distro shell; empty resolves the login shell. */
|
|
89
|
+
shell: string;
|
|
90
|
+
/** Whether `<shell> -lc` semantics apply. */
|
|
91
|
+
loginShell: boolean;
|
|
92
|
+
/** Whether commands confine inside the distro. */
|
|
93
|
+
sandbox: boolean;
|
|
94
|
+
/** Whether confined profiles mask `/mnt`. */
|
|
95
|
+
maskWindowsDrive: boolean;
|
|
96
|
+
/** The volatile default workdir. */
|
|
97
|
+
cwd: {
|
|
98
|
+
get(): string | undefined;
|
|
99
|
+
};
|
|
100
|
+
/** The volatile Windows spawn directory. */
|
|
101
|
+
hostCwd: {
|
|
102
|
+
get(): string | undefined;
|
|
103
|
+
};
|
|
104
|
+
/** Extra env names forwarded through WSLENV. */
|
|
105
|
+
forwardEnv: string[];
|
|
106
|
+
/** The volatile default timeout. */
|
|
107
|
+
timeoutMs: {
|
|
108
|
+
get(): number;
|
|
109
|
+
};
|
|
110
|
+
/** The volatile timeout cap. */
|
|
111
|
+
maxTimeoutMs: {
|
|
112
|
+
get(): number;
|
|
113
|
+
};
|
|
114
|
+
/** The volatile per-stream output cap. */
|
|
115
|
+
maxOutputBytes: {
|
|
116
|
+
get(): number;
|
|
117
|
+
};
|
|
118
|
+
/** The volatile spill-file cap. */
|
|
119
|
+
maxSpillBytes: {
|
|
120
|
+
get(): number;
|
|
121
|
+
};
|
|
122
|
+
/** The volatile kill-escalation grace. */
|
|
123
|
+
graceMs: {
|
|
124
|
+
get(): number;
|
|
125
|
+
};
|
|
126
|
+
/** Whether execution prefers the resident agent. */
|
|
127
|
+
agent: boolean;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The upstream executor's spawn-spec builder, re-opened for this subclass —
|
|
131
|
+
* see docs/UPSTREAM-SPAWN-SEAM.md. Type-level bridge only; the runtime class
|
|
132
|
+
* is untouched.
|
|
133
|
+
*/
|
|
134
|
+
export type SpawnSeamExecutor = Omit<LocalBashExecutor, "spawnSpec"> & {
|
|
135
|
+
spawnSpec: (spec: ShellExecSpec, argv: string[], stdoutMaxBytes: number, signal?: AbortSignal) => SubprocessSpawnSpec;
|
|
136
|
+
};
|
|
137
|
+
/** The per-process confinement facts retained until settlement. */
|
|
138
|
+
interface ProcessFacts {
|
|
139
|
+
/** The mode the process ran under. */
|
|
140
|
+
mode: SandboxMode | undefined;
|
|
141
|
+
/** The seam's enforcement completeness. */
|
|
142
|
+
enforcement: "full" | "partial";
|
|
143
|
+
/** The case-insensitive stderr substrings a denial produces. */
|
|
144
|
+
denialSignatures: string[];
|
|
145
|
+
/** Structured runner-failure evidence rules. */
|
|
146
|
+
runnerFailureRules: Array<{
|
|
147
|
+
fatalSignatures: string[];
|
|
148
|
+
allowedExitCodes?: number[];
|
|
149
|
+
informationalLines?: string[];
|
|
150
|
+
}>;
|
|
151
|
+
/** The confinement runner program (`argv[0]`), for spawn-failure attribution. */
|
|
152
|
+
runnerProgram: string;
|
|
153
|
+
/** The Linux workdir the process was given. */
|
|
154
|
+
workdir: string;
|
|
155
|
+
/** Whether the profile masks the Windows drive. */
|
|
156
|
+
windowsDrive: "masked" | "visible";
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Bash executor that runs every command inside a WSL distro.
|
|
160
|
+
*
|
|
161
|
+
* Extends the unconfined `LocalBashExecutor`, so command defaulting, deadline
|
|
162
|
+
* handling, output caps, spill files and background-process management are the
|
|
163
|
+
* shipped implementation's; this subclass replaces only the argv (prefix with
|
|
164
|
+
* `wsl.exe`) and the process working directory (wsl.exe must be started from a
|
|
165
|
+
* Windows directory, while the Linux directory rides on `--cd`).
|
|
166
|
+
*/
|
|
167
|
+
export declare class WslShellExecutor extends LocalBashExecutor {
|
|
168
|
+
static inject: string[];
|
|
169
|
+
static Config: z<Schemastery.ObjectS<NoInfer<{
|
|
170
|
+
/** Distro name; empty means WSL's own default distro. */
|
|
171
|
+
distro: z<string, string, "defined">;
|
|
172
|
+
/** Path to `wsl.exe`; overridable for a non-standard install. */
|
|
173
|
+
wslPath: z<string, string, "defined">;
|
|
174
|
+
/**
|
|
175
|
+
* The shell commands run in. Empty means "resolve the login shell of the
|
|
176
|
+
* distro's user" — hardcoding bash silently ignores a user whose passwd
|
|
177
|
+
* entry points at zsh, fish, dash, or anything else, losing their PATH and
|
|
178
|
+
* rc setup. Set an absolute path inside the distro to pin one.
|
|
179
|
+
*/
|
|
180
|
+
shell: z<string, string, "defined">;
|
|
181
|
+
/**
|
|
182
|
+
* Login-shell semantics (`<shell> -lc`) source the profile scripts, which is
|
|
183
|
+
* what makes an interactive WSL setup's PATH (nvm, cargo, pyenv) visible.
|
|
184
|
+
* Set false for a fast, fully deterministic `<shell> -c`. Applies only to the
|
|
185
|
+
* POSIX family — see `shellArgs` for why other shells get a bare `-c`.
|
|
186
|
+
*/
|
|
187
|
+
loginShell: z<boolean, boolean, "defined">;
|
|
188
|
+
/**
|
|
189
|
+
* Confine every command inside the distro with `bwrap`, and report the
|
|
190
|
+
* policy's mode through {@link sandboxMode}. Fails closed: a distro without
|
|
191
|
+
* a usable `bwrap` produces `SANDBOX_UNAVAILABLE` rather than an unconfined
|
|
192
|
+
* run. `false` restores the pre-sandbox behaviour exactly — commands run
|
|
193
|
+
* unconfined and `sandboxMode` is `undefined`, so the model is told so.
|
|
194
|
+
*/
|
|
195
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
196
|
+
/**
|
|
197
|
+
* Shadow `/mnt` with an empty tmpfs in every confined profile, hiding the
|
|
198
|
+
* Windows drive from confined commands: its files cannot be read or
|
|
199
|
+
* exfiltrated and its executables cannot be launched through interop —
|
|
200
|
+
* the hole that lets a "sandboxed" command reach your full Windows token.
|
|
201
|
+
* A narrowing, not a closure (a command can still write an executable
|
|
202
|
+
* into the workspace and run it — binfmt dispatches on content, not
|
|
203
|
+
* location), so enforcement stays `partial` and the sandbox fact reports
|
|
204
|
+
* `windowsDrive: "masked"`. Costs: confined commands cannot read or touch
|
|
205
|
+
* anything under `/mnt`, and tools that probe the drive degrade. The
|
|
206
|
+
* complete closure is distro-level: `[interop] enabled=false` in
|
|
207
|
+
* `wsl.conf`.
|
|
208
|
+
*/
|
|
209
|
+
maskWindowsDrive: z<boolean, boolean, "defined">;
|
|
210
|
+
/**
|
|
211
|
+
* Default workdir for a request that names none; accepts `/home/andy/proj`,
|
|
212
|
+
* a UNC path, or `C:\...`. Empty — the default — means the distro user's
|
|
213
|
+
* home, resolved inside the distro.
|
|
214
|
+
*/
|
|
215
|
+
cwd: z<string, string, "volatile">;
|
|
216
|
+
/** Windows directory the `wsl.exe` process itself starts in. */
|
|
217
|
+
hostCwd: z<string, string, "volatile">;
|
|
218
|
+
/** Extra env names to forward into the distro through WSLENV. */
|
|
219
|
+
forwardEnv: z<string[], string[], "defined">;
|
|
220
|
+
timeoutMs: z<number, number, "volatile-defined">;
|
|
221
|
+
maxTimeoutMs: z<number, number, "volatile-defined">;
|
|
222
|
+
maxOutputBytes: z<number, number, "volatile-defined">;
|
|
223
|
+
maxSpillBytes: z<number, number, "volatile-defined">;
|
|
224
|
+
graceMs: z<number, number, "volatile-defined">;
|
|
225
|
+
/**
|
|
226
|
+
* Run commands through the resident agent when it is up (one long-lived
|
|
227
|
+
* in-distro process; in-distro timeout semantics), falling back to the
|
|
228
|
+
* one-shot `wsl.exe` path whenever the agent is out. `false` never uses
|
|
229
|
+
* the agent. The confinement, the output budgets' shapes and the result
|
|
230
|
+
* decoration are identical on both paths.
|
|
231
|
+
*/
|
|
232
|
+
agent: z<boolean, boolean, "defined">;
|
|
233
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
234
|
+
/** Distro name; empty means WSL's own default distro. */
|
|
235
|
+
distro: z<string, string, "defined">;
|
|
236
|
+
/** Path to `wsl.exe`; overridable for a non-standard install. */
|
|
237
|
+
wslPath: z<string, string, "defined">;
|
|
238
|
+
/**
|
|
239
|
+
* The shell commands run in. Empty means "resolve the login shell of the
|
|
240
|
+
* distro's user" — hardcoding bash silently ignores a user whose passwd
|
|
241
|
+
* entry points at zsh, fish, dash, or anything else, losing their PATH and
|
|
242
|
+
* rc setup. Set an absolute path inside the distro to pin one.
|
|
243
|
+
*/
|
|
244
|
+
shell: z<string, string, "defined">;
|
|
245
|
+
/**
|
|
246
|
+
* Login-shell semantics (`<shell> -lc`) source the profile scripts, which is
|
|
247
|
+
* what makes an interactive WSL setup's PATH (nvm, cargo, pyenv) visible.
|
|
248
|
+
* Set false for a fast, fully deterministic `<shell> -c`. Applies only to the
|
|
249
|
+
* POSIX family — see `shellArgs` for why other shells get a bare `-c`.
|
|
250
|
+
*/
|
|
251
|
+
loginShell: z<boolean, boolean, "defined">;
|
|
252
|
+
/**
|
|
253
|
+
* Confine every command inside the distro with `bwrap`, and report the
|
|
254
|
+
* policy's mode through {@link sandboxMode}. Fails closed: a distro without
|
|
255
|
+
* a usable `bwrap` produces `SANDBOX_UNAVAILABLE` rather than an unconfined
|
|
256
|
+
* run. `false` restores the pre-sandbox behaviour exactly — commands run
|
|
257
|
+
* unconfined and `sandboxMode` is `undefined`, so the model is told so.
|
|
258
|
+
*/
|
|
259
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
260
|
+
/**
|
|
261
|
+
* Shadow `/mnt` with an empty tmpfs in every confined profile, hiding the
|
|
262
|
+
* Windows drive from confined commands: its files cannot be read or
|
|
263
|
+
* exfiltrated and its executables cannot be launched through interop —
|
|
264
|
+
* the hole that lets a "sandboxed" command reach your full Windows token.
|
|
265
|
+
* A narrowing, not a closure (a command can still write an executable
|
|
266
|
+
* into the workspace and run it — binfmt dispatches on content, not
|
|
267
|
+
* location), so enforcement stays `partial` and the sandbox fact reports
|
|
268
|
+
* `windowsDrive: "masked"`. Costs: confined commands cannot read or touch
|
|
269
|
+
* anything under `/mnt`, and tools that probe the drive degrade. The
|
|
270
|
+
* complete closure is distro-level: `[interop] enabled=false` in
|
|
271
|
+
* `wsl.conf`.
|
|
272
|
+
*/
|
|
273
|
+
maskWindowsDrive: z<boolean, boolean, "defined">;
|
|
274
|
+
/**
|
|
275
|
+
* Default workdir for a request that names none; accepts `/home/andy/proj`,
|
|
276
|
+
* a UNC path, or `C:\...`. Empty — the default — means the distro user's
|
|
277
|
+
* home, resolved inside the distro.
|
|
278
|
+
*/
|
|
279
|
+
cwd: z<string, string, "volatile">;
|
|
280
|
+
/** Windows directory the `wsl.exe` process itself starts in. */
|
|
281
|
+
hostCwd: z<string, string, "volatile">;
|
|
282
|
+
/** Extra env names to forward into the distro through WSLENV. */
|
|
283
|
+
forwardEnv: z<string[], string[], "defined">;
|
|
284
|
+
timeoutMs: z<number, number, "volatile-defined">;
|
|
285
|
+
maxTimeoutMs: z<number, number, "volatile-defined">;
|
|
286
|
+
maxOutputBytes: z<number, number, "volatile-defined">;
|
|
287
|
+
maxSpillBytes: z<number, number, "volatile-defined">;
|
|
288
|
+
graceMs: z<number, number, "volatile-defined">;
|
|
289
|
+
/**
|
|
290
|
+
* Run commands through the resident agent when it is up (one long-lived
|
|
291
|
+
* in-distro process; in-distro timeout semantics), falling back to the
|
|
292
|
+
* one-shot `wsl.exe` path whenever the agent is out. `false` never uses
|
|
293
|
+
* the agent. The confinement, the output budgets' shapes and the result
|
|
294
|
+
* decoration are identical on both paths.
|
|
295
|
+
*/
|
|
296
|
+
agent: z<boolean, boolean, "defined">;
|
|
297
|
+
}>>, "plain">;
|
|
298
|
+
/** Cached default distro, resolved once from `wsl.exe -l -q`. */
|
|
299
|
+
resolvedDistro: string;
|
|
300
|
+
/** Cached distro-user login shell, resolved once. */
|
|
301
|
+
resolvedShell: string;
|
|
302
|
+
/** Cached distro-user home, the workdir when `cwd` is empty. */
|
|
303
|
+
resolvedHome: string | undefined;
|
|
304
|
+
/** The default mode — the capability fact the tool layer reads. */
|
|
305
|
+
mode: SandboxMode | undefined;
|
|
306
|
+
/** The distro-side confinement, or nothing when the operator opted out. */
|
|
307
|
+
sandbox: InstanceType<typeof WslSandbox> | undefined;
|
|
308
|
+
/**
|
|
309
|
+
* Per-process confinement facts retained until settlement. Overlapping calls
|
|
310
|
+
* can carry different policies, so a shared latest-wrap value would classify
|
|
311
|
+
* a process against the wrong facts. Unconfined processes have no entry.
|
|
312
|
+
*/
|
|
313
|
+
processFacts: Map<ShellExecution, ProcessFacts>;
|
|
314
|
+
/**
|
|
315
|
+
* @param ctx - the composition context.
|
|
316
|
+
* @param config - the validated plugin config.
|
|
317
|
+
*/
|
|
318
|
+
constructor(ctx: Context, config: WslShellExecutorConfig);
|
|
319
|
+
/**
|
|
320
|
+
* The mode this executor confines with, or `undefined` when it does not
|
|
321
|
+
* confine at all. `dsh-tool-bash` reads exactly this to decide whether to
|
|
322
|
+
* advertise the escalation field, so it must disappear together with the
|
|
323
|
+
* enforcement — never claim a boundary that is not applied.
|
|
324
|
+
*
|
|
325
|
+
* @returns the deployment default mode, or `undefined` when unconfined.
|
|
326
|
+
*/
|
|
327
|
+
get sandboxMode(): SandboxMode | undefined;
|
|
328
|
+
/**
|
|
329
|
+
* The distro every command runs in, resolving WSL's default on first use.
|
|
330
|
+
*
|
|
331
|
+
* @returns the distro name.
|
|
332
|
+
*/
|
|
333
|
+
distro(): Promise<string>;
|
|
334
|
+
/**
|
|
335
|
+
* The shell every command runs in: the configured one, else the login shell of
|
|
336
|
+
* the distro user, resolved once and cached.
|
|
337
|
+
*
|
|
338
|
+
* @returns an absolute path to the shell inside the distro.
|
|
339
|
+
*/
|
|
340
|
+
shellName(): Promise<string>;
|
|
341
|
+
/**
|
|
342
|
+
* The shell every command runs in, resolving the distro login shell on
|
|
343
|
+
* first use.
|
|
344
|
+
*
|
|
345
|
+
* @returns an absolute path to the shell inside the distro.
|
|
346
|
+
*/
|
|
347
|
+
shell(): Promise<string>;
|
|
348
|
+
/**
|
|
349
|
+
* Windows directory the `wsl.exe` process is started from.
|
|
350
|
+
*
|
|
351
|
+
* @returns the host directory to spawn in.
|
|
352
|
+
*/
|
|
353
|
+
hostCwd(): string;
|
|
354
|
+
/**
|
|
355
|
+
* The shared resident agent serving this executor's distro.
|
|
356
|
+
*
|
|
357
|
+
* @param distro - the resolved distro name.
|
|
358
|
+
* @returns the shared agent.
|
|
359
|
+
*/
|
|
360
|
+
executionAgent(distro: string): WslAgent;
|
|
361
|
+
/**
|
|
362
|
+
* The argv that runs INSIDE the distro for one spec: the resolved shell, its
|
|
363
|
+
* login/command flags, and the command string. This is the argv a
|
|
364
|
+
* distro-side sandbox wraps, which is why it is built separately from the
|
|
365
|
+
* `wsl.exe` invocation around it.
|
|
366
|
+
*
|
|
367
|
+
* @param spec - the resolved spec from {@link LocalBashExecutor.resolve}.
|
|
368
|
+
* @param shell - the resolved distro shell.
|
|
369
|
+
* @returns the distro-side argv.
|
|
370
|
+
*/
|
|
371
|
+
distroArgv(spec: ShellExecSpec, shell: string): string[];
|
|
372
|
+
/**
|
|
373
|
+
* Build the `wsl.exe` argv for one resolved spec. The command string is
|
|
374
|
+
* passed as a single argv element to the resolved shell, so there is no
|
|
375
|
+
* quoting layer of our own; Node's argv array reaches `wsl.exe` verbatim.
|
|
376
|
+
*
|
|
377
|
+
* `--exec` is load-bearing — see the comment inside.
|
|
378
|
+
*
|
|
379
|
+
* @param spec - the resolved spec from {@link LocalBashExecutor.resolve}.
|
|
380
|
+
* @param distro - the resolved distro name.
|
|
381
|
+
* @param shell - the resolved distro shell.
|
|
382
|
+
* @param inner - the already-confined distro argv, when confinement applies.
|
|
383
|
+
* @returns the argv to spawn.
|
|
384
|
+
*/
|
|
385
|
+
argv(spec: ShellExecSpec, distro: string, shell: string, inner?: string[]): string[];
|
|
386
|
+
/**
|
|
387
|
+
* Override only the spawn's working directory and environment. `spec.workdir`
|
|
388
|
+
* is a Linux path, which a Windows CreateProcess call cannot use as `cwd`.
|
|
389
|
+
*
|
|
390
|
+
* The inherited builder is private, so the call goes through the
|
|
391
|
+
* {@link SpawnSeamExecutor} bridge (`inheritedSpawnSpec`) instead of `super`
|
|
392
|
+
* — see docs/UPSTREAM-SPAWN-SEAM.md.
|
|
393
|
+
*
|
|
394
|
+
* @param spec - resolved execution settings.
|
|
395
|
+
* @param argv - the argv from {@link WslShellExecutor.argv}.
|
|
396
|
+
* @param stdoutMaxBytes - per-call stdout cap.
|
|
397
|
+
* @param signal - the spawn cancellation signal.
|
|
398
|
+
* @returns the subprocess spawn spec.
|
|
399
|
+
*/
|
|
400
|
+
spawnSpec(spec: ShellExecSpec, argv: string[], stdoutMaxBytes: number, signal?: AbortSignal): SubprocessSpawnSpec;
|
|
401
|
+
/**
|
|
402
|
+
* Resolve a request into a spec, replacing the parent's `process.cwd()`
|
|
403
|
+
* fallback with this provider's distro-home marker and stamping the per-call
|
|
404
|
+
* sandbox policy.
|
|
405
|
+
*
|
|
406
|
+
* The seam requires this step to be synchronous — the tool layer calls
|
|
407
|
+
* `ctx.shell.execute(ctx.shell.resolve(…))` with no `await` between them — so
|
|
408
|
+
* neither the home nor a confinement decision can be made here. Two request
|
|
409
|
+
* shapes default to the home when no `cwd` is configured: one that names no
|
|
410
|
+
* `workdir` at all, and one that names it only RELATIVELY — the parent
|
|
411
|
+
* resolves the first against `process.cwd()` (a Windows directory) and has
|
|
412
|
+
* nothing of its own to join the second onto. Both are stamped as the empty
|
|
413
|
+
* marker, with a relative tail carried verbatim in `relativeWorkdir`;
|
|
414
|
+
* {@link withDefaultWorkdir} resolves the pair against the home. The policy
|
|
415
|
+
* travels on the spec to {@link execute}, which awaits the wrap.
|
|
416
|
+
*
|
|
417
|
+
* @param request - the caller's request.
|
|
418
|
+
* @returns the fully-resolved spec.
|
|
419
|
+
*/
|
|
420
|
+
resolve(request: ShellExecRequest): WslExecSpec;
|
|
421
|
+
/**
|
|
422
|
+
* The spec to execute, with the home marker resolved against the distro
|
|
423
|
+
* user's home: `workdir: ""` becomes the home itself, and a relative tail
|
|
424
|
+
* carried by {@link WslShellExecutor.resolve} is joined under it. That is one
|
|
425
|
+
* `sh -c 'printf %s "$HOME"'` per provider instance, cached, and it happens
|
|
426
|
+
* only for a request that defaulted — an absolute workdir never reaches here.
|
|
427
|
+
*
|
|
428
|
+
* @param spec - the resolved spec.
|
|
429
|
+
* @returns the spec to execute.
|
|
430
|
+
*/
|
|
431
|
+
withDefaultWorkdir(spec: WslExecSpec): Promise<WslExecSpec>;
|
|
432
|
+
/**
|
|
433
|
+
* Decorate a handle's foreground projection in place, memoized once. The
|
|
434
|
+
* handle keeps its identity — never wrapped in a second object — because the
|
|
435
|
+
* per-process facts and `onProcessDone` key on the exact instance.
|
|
436
|
+
*
|
|
437
|
+
* @template H - the concrete handle type, preserved so the decorated handle
|
|
438
|
+
* keeps its identity in the callers' types; the settled result row the maps
|
|
439
|
+
* are spelled in is derived from its `result()`.
|
|
440
|
+
* @param ex - the execution handle to decorate.
|
|
441
|
+
* @param map - maps the settled result.
|
|
442
|
+
* @param mapError - maps a rejection, when the caller has one.
|
|
443
|
+
* @returns the same handle.
|
|
444
|
+
*/
|
|
445
|
+
static decorateResult<H extends {
|
|
446
|
+
result(): Promise<unknown>;
|
|
447
|
+
}>(ex: H, map: (result: Awaited<ReturnType<H["result"]>>) => Awaited<ReturnType<H["result"]>>, mapError: (error: unknown) => Awaited<ReturnType<H["result"]>>): H;
|
|
448
|
+
/**
|
|
449
|
+
* Stamp the per-process sandbox facts before `done` settles, so a background
|
|
450
|
+
* process reports the same mode, denial and enforcement a foreground run
|
|
451
|
+
* would. Signal deaths are not denials.
|
|
452
|
+
*
|
|
453
|
+
* @param proc - the settled process handle.
|
|
454
|
+
* @param stderr - the process's retained stderr tail used by subclasses for settlement classification.
|
|
455
|
+
* @param providerRejected - whether the subprocess promise rejected without a direct outcome.
|
|
456
|
+
* @param providerError - the provider rejection reason, which may itself be undefined.
|
|
457
|
+
*/
|
|
458
|
+
onProcessDone(proc: ShellExecution, stderr: string, providerRejected: boolean, providerError?: unknown): void;
|
|
459
|
+
/**
|
|
460
|
+
* Reject a settled command that failed because the distro itself is missing,
|
|
461
|
+
* stopped or unregistered.
|
|
462
|
+
*
|
|
463
|
+
* `wsl.exe` exits non-zero for that with an EMPTY stderr and its diagnostic on
|
|
464
|
+
* stdout, so nothing else here can see it: the denial signatures read stderr, and
|
|
465
|
+
* the result would otherwise reach the model as a bare non-zero code with no
|
|
466
|
+
* cause. The message names the remedy and says plainly that a wider permission
|
|
467
|
+
* cannot help — a sandbox flag does not create a distro.
|
|
468
|
+
*
|
|
469
|
+
* @param result - the settled shell result.
|
|
470
|
+
* @param distro - the distro the command asked for.
|
|
471
|
+
* @throws {Error} carrying the `WSL_E_*` code and the remedy.
|
|
472
|
+
*/
|
|
473
|
+
throwIfDistroUnavailable(result: {
|
|
474
|
+
exitCode?: number | null;
|
|
475
|
+
stdout?: {
|
|
476
|
+
text?: string;
|
|
477
|
+
};
|
|
478
|
+
stderr?: {
|
|
479
|
+
text?: string;
|
|
480
|
+
};
|
|
481
|
+
} | undefined, distro: string): void;
|
|
482
|
+
/**
|
|
483
|
+
* Reject a command that ran in the wrong directory.
|
|
484
|
+
*
|
|
485
|
+
* `wsl.exe --cd <path>` does not fail on a missing directory: it warns on stderr,
|
|
486
|
+
* runs the command in `/`, and exits 0. Reporting that as success would let a write
|
|
487
|
+
* or a build act on the wrong tree while every signal says it worked, so it becomes
|
|
488
|
+
* an error naming the directory that could not be entered.
|
|
489
|
+
*
|
|
490
|
+
* @param result - the settled shell result.
|
|
491
|
+
* @param workdir - the Linux directory the request asked for.
|
|
492
|
+
* @throws {Error} naming the directory and the fallback that happened.
|
|
493
|
+
*/
|
|
494
|
+
throwIfWorkdirMissing(result: {
|
|
495
|
+
exitCode?: number | null;
|
|
496
|
+
stdout?: {
|
|
497
|
+
text?: string;
|
|
498
|
+
};
|
|
499
|
+
stderr?: {
|
|
500
|
+
text?: string;
|
|
501
|
+
};
|
|
502
|
+
} | undefined, workdir: string): void;
|
|
503
|
+
/**
|
|
504
|
+
* Execute one resolved spec inside the distro, confined by the per-call policy
|
|
505
|
+
* when there is one.
|
|
506
|
+
*
|
|
507
|
+
* Confinement wraps the DISTRO-side argv (`<shell> -lc <command>`) in `bwrap`
|
|
508
|
+
* and hands the result to `wsl.exe --exec`, so the sandbox is built inside the
|
|
509
|
+
* distro while the process that spawns it stays the ordinary Windows one. The
|
|
510
|
+
* `danger-full-access` escalation — and an operator's `sandbox: false` — skip
|
|
511
|
+
* the wrap entirely and run the command as before.
|
|
512
|
+
*
|
|
513
|
+
* @param spec - resolved execution settings.
|
|
514
|
+
* @returns the live shell process handle.
|
|
515
|
+
*/
|
|
516
|
+
execute(spec: ShellExecSpec): Promise<ShellExecution>;
|
|
517
|
+
/**
|
|
518
|
+
* Refuse a workdir that names another WSL distro — the shell-side twin of
|
|
519
|
+
* the filesystem's `FS_OUTSIDE_DISTRO` fence. Left unguarded, `toLinuxPath`
|
|
520
|
+
* strips the UNC's distro name and the command runs inside THIS distro
|
|
521
|
+
* against whatever tree happens to share the path, silently, while the fs
|
|
522
|
+
* side refuses the same workspace outright. The wording mirrors the fs
|
|
523
|
+
* refusal (minus `restrictToDistro`, which is an fs-only key).
|
|
524
|
+
*
|
|
525
|
+
* @param workdir - the resolved workdir (UNC, Linux path, or Windows path).
|
|
526
|
+
* @param distro - the pinned distro name.
|
|
527
|
+
* @throws {Error} naming both distros and both ways out.
|
|
528
|
+
*/
|
|
529
|
+
throwIfWorkdirInAnotherDistro(workdir: string, distro: string): void;
|
|
530
|
+
/**
|
|
531
|
+
* The agent-backed execution path: one long-lived in-distro process, the
|
|
532
|
+
* confinement argv unchanged, the handle shape unchanged. Any
|
|
533
|
+
* {@link AgentUnavailableError} propagates for the caller's fallback.
|
|
534
|
+
*
|
|
535
|
+
* @param resolved - the resolved spec from {@link WslShellExecutor.execute}.
|
|
536
|
+
* @param distro - the resolved distro name.
|
|
537
|
+
* @param shell - the resolved distro shell.
|
|
538
|
+
* @returns the decorated execution result.
|
|
539
|
+
*/
|
|
540
|
+
executeViaAgent(resolved: WslExecSpec, distro: string, shell: string): Promise<ShellExecution>;
|
|
541
|
+
/**
|
|
542
|
+
* The pre-agent execution path, verbatim: one `wsl.exe` per command through
|
|
543
|
+
* the inherited `executeArgv` machinery. Kept as the permanent fallback for
|
|
544
|
+
* an out-of-service agent and as the `agent: false` behaviour.
|
|
545
|
+
*
|
|
546
|
+
* @param resolved - the resolved spec.
|
|
547
|
+
* @param distro - the resolved distro name.
|
|
548
|
+
* @param shell - the resolved distro shell.
|
|
549
|
+
* @returns the decorated execution result.
|
|
550
|
+
*/
|
|
551
|
+
executeOneShot(resolved: WslExecSpec, distro: string, shell: string): Promise<ShellExecution>;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Filesystem backend over a WSL distro's UNC share.
|
|
555
|
+
*
|
|
556
|
+
* Reads, listings, stats and searches reuse the host backend verbatim: the
|
|
557
|
+
* share is a real filesystem to Node, and the packaged ripgrep walks it too.
|
|
558
|
+
* Two things are deliberately replaced.
|
|
559
|
+
*
|
|
560
|
+
* **Resolution** accepts Linux paths (`/home/andy/proj/src/a.ts`) and shows them
|
|
561
|
+
* back as Linux paths, while `targetKey` stays the UNC path the host I/O uses —
|
|
562
|
+
* the seam treats `targetKey` as opaque and reserves `displayPath` for model and
|
|
563
|
+
* UI output, so this rewrite is contract-legal.
|
|
564
|
+
*
|
|
565
|
+
* **Mutation guards are hoisted.** The host backend publishes a guarded
|
|
566
|
+
* *creation* by hard-linking a staged file into place, and the WSL UNC share
|
|
567
|
+
* rejects hard links with `ENOTSUP`; it also refuses `symlink` with `EPERM`.
|
|
568
|
+
* Guarded creation through the share would therefore fail with `FS_IO_ERROR` on
|
|
569
|
+
* the most common write there is — creating a new file. This backend checks the
|
|
570
|
+
* caller's guard itself and then delegates the unguarded publication, which the
|
|
571
|
+
* share does support. The residual window is the same check-then-act race
|
|
572
|
+
* `dsh-fs-sandbox` already documents and accepts.
|
|
573
|
+
*
|
|
574
|
+
* **Publication is replaced.** Unguarded is not enough on its own: the host
|
|
575
|
+
* backend's Windows branch also copies the replaced file's security descriptor
|
|
576
|
+
* and publishes with `ReplaceFileW`, and neither exists on a 9p share — the
|
|
577
|
+
* descriptor read fails every write to an existing file outright. See the
|
|
578
|
+
* constructor for what runs instead.
|
|
579
|
+
*/
|
|
580
|
+
/**
|
|
581
|
+
* The WSL filesystem provider's validated config — the keys of
|
|
582
|
+
* {@link WslFileSystem.Config}, which the upstream `LocalFileSystem` config
|
|
583
|
+
* type does not know. This provider has no volatile fields: every key is read
|
|
584
|
+
* as its plain value.
|
|
585
|
+
*/
|
|
586
|
+
export interface WslFileSystemConfig {
|
|
587
|
+
/** The pinned distro; empty resolves WSL's default. */
|
|
588
|
+
distro: string;
|
|
589
|
+
/** The base directory for relative input. */
|
|
590
|
+
cwd: string;
|
|
591
|
+
/** The `wsl.exe` path, for default-distro resolution. */
|
|
592
|
+
wslPath: string;
|
|
593
|
+
/**
|
|
594
|
+
* The overwrite-diff byte bound per side; schema-defaulted when mounted
|
|
595
|
+
* through cordis, and defaulted again by the substrate when absent.
|
|
596
|
+
*/
|
|
597
|
+
diffBasisMaxBytes?: number;
|
|
598
|
+
/** Whether mutations are fenced by the policy. */
|
|
599
|
+
sandbox: boolean;
|
|
600
|
+
/** Whether confined profiles mask `/mnt`. */
|
|
601
|
+
maskWindowsDrive: boolean;
|
|
602
|
+
/** Whether paths leaving the distro are refused. */
|
|
603
|
+
restrictToDistro: boolean;
|
|
604
|
+
/** The in-distro watch scan depth bound. */
|
|
605
|
+
watchMaxDepth: number;
|
|
606
|
+
/**
|
|
607
|
+
* Which I/O substrate serves the file tools; schema-defaulted when mounted
|
|
608
|
+
* through cordis, absent on direct construction (the router), which the
|
|
609
|
+
* constructor's refusal treats as `"agent"`.
|
|
610
|
+
*/
|
|
611
|
+
substrate?: string;
|
|
612
|
+
}
|
|
613
|
+
export declare class WslFileSystem extends LocalFileSystem {
|
|
614
|
+
static inject: string[];
|
|
615
|
+
static Config: z<Schemastery.ObjectS<NoInfer<{
|
|
616
|
+
/** Distro name; empty resolves WSL's default on first use. */
|
|
617
|
+
distro: z<string, string, "defined">;
|
|
618
|
+
/**
|
|
619
|
+
* Default workdir for relative input; accepts a Linux path, a UNC path, or
|
|
620
|
+
* `C:\...`. Empty — the default — means the distro user's home, resolved
|
|
621
|
+
* inside the distro.
|
|
622
|
+
*/
|
|
623
|
+
cwd: z<string, string, "defined">;
|
|
624
|
+
/** Path to `wsl.exe`, used only to resolve the default distro. */
|
|
625
|
+
wslPath: z<string, string, "defined">;
|
|
626
|
+
/** UTF-8 byte limit per overwrite-diff side, mirroring the host backend. */
|
|
627
|
+
diffBasisMaxBytes: z<number, number, "defined">;
|
|
628
|
+
/**
|
|
629
|
+
* Fence mutations by `ctx.sandboxPolicy`, and report the mode through
|
|
630
|
+
* {@link sandboxMode}. `false` restores the pre-sandbox behaviour exactly —
|
|
631
|
+
* an unfenced backend that reports `undefined` — for an operator who wants
|
|
632
|
+
* the old posture and accepts what it means: the model is told these
|
|
633
|
+
* operations are unconfined.
|
|
634
|
+
*/
|
|
635
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
636
|
+
/**
|
|
637
|
+
* Shadow `/mnt` with an empty tmpfs in every confined profile, hiding the
|
|
638
|
+
* Windows drive from confined commands — same meaning and same honest
|
|
639
|
+
* limits as the `wsl-shell` key of the same name (its doc carries the
|
|
640
|
+
* full story). Confined fs mutations publish through the masked resident;
|
|
641
|
+
* reads and the share substrate are unaffected.
|
|
642
|
+
*/
|
|
643
|
+
maskWindowsDrive: z<boolean, boolean, "defined">;
|
|
644
|
+
/**
|
|
645
|
+
* Refuse any path that leaves the pinned distro. A WSL filesystem is a
|
|
646
|
+
* whole-Linux view whose `/mnt/c` also reaches the Windows disk, so without
|
|
647
|
+
* this the backend would silently widen the session far beyond the
|
|
648
|
+
* workspace the operator selected.
|
|
649
|
+
*/
|
|
650
|
+
restrictToDistro: z<boolean, boolean, "defined">;
|
|
651
|
+
/**
|
|
652
|
+
* How deep the in-distro watch scan walks, `1` = the watched directory and
|
|
653
|
+
* its direct children. `0` — the default — walks the whole tree, which is
|
|
654
|
+
* what the invalidation contract asks for but a real cost every poll over
|
|
655
|
+
* a tree containing a `node_modules`; a bound trades that cost for a
|
|
656
|
+
* documented blind spot: a change below the bound is not seen until
|
|
657
|
+
* another change above it fires the same callback.
|
|
658
|
+
*/
|
|
659
|
+
watchMaxDepth: z<number, number, "defined">;
|
|
660
|
+
/**
|
|
661
|
+
* Which I/O substrate serves the file tools. Only `"agent"` exists — the
|
|
662
|
+
* resident in-distro agent (`lib/fs-substrate.js`): reads, writes and
|
|
663
|
+
* identities run where the files live, symlinks and mode bits are native,
|
|
664
|
+
* and the mutation guard survives to the write's no-replace publication,
|
|
665
|
+
* kernel-enforced by the confined resident when the policy is confined.
|
|
666
|
+
* The former `"share"` opt-out — the Windows-side host filesystem stack
|
|
667
|
+
* over the distro's 9p share — is retired: it was the one model-facing
|
|
668
|
+
* I/O path that still crossed the share, and the agent has no fallback
|
|
669
|
+
* that needs it. A profile carrying `"share"` is refused at construction
|
|
670
|
+
* with the migration (`shareSubstrateRefusal`). `watch()` rides the
|
|
671
|
+
* in-distro poll loop for the substrate regardless (`lib/watcher.js`) —
|
|
672
|
+
* it never ran through either substrate. The substrate never changes
|
|
673
|
+
* `sandboxMode`'s honesty.
|
|
674
|
+
*/
|
|
675
|
+
substrate: z<"agent" | "share", "agent" | "share", "defined">;
|
|
676
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
677
|
+
/** Distro name; empty resolves WSL's default on first use. */
|
|
678
|
+
distro: z<string, string, "defined">;
|
|
679
|
+
/**
|
|
680
|
+
* Default workdir for relative input; accepts a Linux path, a UNC path, or
|
|
681
|
+
* `C:\...`. Empty — the default — means the distro user's home, resolved
|
|
682
|
+
* inside the distro.
|
|
683
|
+
*/
|
|
684
|
+
cwd: z<string, string, "defined">;
|
|
685
|
+
/** Path to `wsl.exe`, used only to resolve the default distro. */
|
|
686
|
+
wslPath: z<string, string, "defined">;
|
|
687
|
+
/** UTF-8 byte limit per overwrite-diff side, mirroring the host backend. */
|
|
688
|
+
diffBasisMaxBytes: z<number, number, "defined">;
|
|
689
|
+
/**
|
|
690
|
+
* Fence mutations by `ctx.sandboxPolicy`, and report the mode through
|
|
691
|
+
* {@link sandboxMode}. `false` restores the pre-sandbox behaviour exactly —
|
|
692
|
+
* an unfenced backend that reports `undefined` — for an operator who wants
|
|
693
|
+
* the old posture and accepts what it means: the model is told these
|
|
694
|
+
* operations are unconfined.
|
|
695
|
+
*/
|
|
696
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
697
|
+
/**
|
|
698
|
+
* Shadow `/mnt` with an empty tmpfs in every confined profile, hiding the
|
|
699
|
+
* Windows drive from confined commands — same meaning and same honest
|
|
700
|
+
* limits as the `wsl-shell` key of the same name (its doc carries the
|
|
701
|
+
* full story). Confined fs mutations publish through the masked resident;
|
|
702
|
+
* reads and the share substrate are unaffected.
|
|
703
|
+
*/
|
|
704
|
+
maskWindowsDrive: z<boolean, boolean, "defined">;
|
|
705
|
+
/**
|
|
706
|
+
* Refuse any path that leaves the pinned distro. A WSL filesystem is a
|
|
707
|
+
* whole-Linux view whose `/mnt/c` also reaches the Windows disk, so without
|
|
708
|
+
* this the backend would silently widen the session far beyond the
|
|
709
|
+
* workspace the operator selected.
|
|
710
|
+
*/
|
|
711
|
+
restrictToDistro: z<boolean, boolean, "defined">;
|
|
712
|
+
/**
|
|
713
|
+
* How deep the in-distro watch scan walks, `1` = the watched directory and
|
|
714
|
+
* its direct children. `0` — the default — walks the whole tree, which is
|
|
715
|
+
* what the invalidation contract asks for but a real cost every poll over
|
|
716
|
+
* a tree containing a `node_modules`; a bound trades that cost for a
|
|
717
|
+
* documented blind spot: a change below the bound is not seen until
|
|
718
|
+
* another change above it fires the same callback.
|
|
719
|
+
*/
|
|
720
|
+
watchMaxDepth: z<number, number, "defined">;
|
|
721
|
+
/**
|
|
722
|
+
* Which I/O substrate serves the file tools. Only `"agent"` exists — the
|
|
723
|
+
* resident in-distro agent (`lib/fs-substrate.js`): reads, writes and
|
|
724
|
+
* identities run where the files live, symlinks and mode bits are native,
|
|
725
|
+
* and the mutation guard survives to the write's no-replace publication,
|
|
726
|
+
* kernel-enforced by the confined resident when the policy is confined.
|
|
727
|
+
* The former `"share"` opt-out — the Windows-side host filesystem stack
|
|
728
|
+
* over the distro's 9p share — is retired: it was the one model-facing
|
|
729
|
+
* I/O path that still crossed the share, and the agent has no fallback
|
|
730
|
+
* that needs it. A profile carrying `"share"` is refused at construction
|
|
731
|
+
* with the migration (`shareSubstrateRefusal`). `watch()` rides the
|
|
732
|
+
* in-distro poll loop for the substrate regardless (`lib/watcher.js`) —
|
|
733
|
+
* it never ran through either substrate. The substrate never changes
|
|
734
|
+
* `sandboxMode`'s honesty.
|
|
735
|
+
*/
|
|
736
|
+
substrate: z<"agent" | "share", "agent" | "share", "defined">;
|
|
737
|
+
}>>, "plain">;
|
|
738
|
+
/** Cached default distro, resolved once from `wsl.exe -l -q`. */
|
|
739
|
+
resolvedDistro: string;
|
|
740
|
+
/** Cached distro-user home, the base for relative input when `cwd` is empty. */
|
|
741
|
+
resolvedHome: string | undefined;
|
|
742
|
+
/** The deployment default mode — the capability fact the tool layer reads. */
|
|
743
|
+
defaultMode: SandboxMode;
|
|
744
|
+
/** The lazy agent-backed substrate, built on first use (see `agentSubstrate`). */
|
|
745
|
+
agentSubstrateInstance: AgentSubstrate | undefined;
|
|
746
|
+
/** The distro-side bwrap usability probe, shared with the confined agents. */
|
|
747
|
+
sandbox: InstanceType<typeof WslSandbox>;
|
|
748
|
+
/** The resident agent serving publication (see `publicationAgent`). */
|
|
749
|
+
agentInstance: WslAgent | undefined;
|
|
750
|
+
/**
|
|
751
|
+
* @param ctx - the composition context.
|
|
752
|
+
* @param config - the validated plugin config.
|
|
753
|
+
*/
|
|
754
|
+
constructor(ctx: Context, config: WslFileSystemConfig);
|
|
755
|
+
/**
|
|
756
|
+
* The mode this backend fences mutations with, or `undefined` when the
|
|
757
|
+
* operator turned the fence off. This is the capability fact `dsh-tool-fs`
|
|
758
|
+
* reads to advertise the escalation fields honestly, so it must disappear
|
|
759
|
+
* together with the enforcement — never claim a boundary that is not applied.
|
|
760
|
+
*
|
|
761
|
+
* @returns the deployment default mode, or `undefined` when unfenced.
|
|
762
|
+
*/
|
|
763
|
+
get sandboxMode(): SandboxMode | undefined;
|
|
764
|
+
/**
|
|
765
|
+
* The resident agent serving this filesystem's in-distro side work, created
|
|
766
|
+
* on first use and kept for the provider's lifetime (its own idle timer
|
|
767
|
+
* retires the process; a later call starts a fresh one).
|
|
768
|
+
*
|
|
769
|
+
* @returns the agent for the pinned distro.
|
|
770
|
+
*/
|
|
771
|
+
publicationAgent(): Promise<WslAgent>;
|
|
772
|
+
/**
|
|
773
|
+
* The agent-backed substrate, built on first use over the same resident
|
|
774
|
+
* agent every other distro side work shares.
|
|
775
|
+
*
|
|
776
|
+
* @returns the substrate.
|
|
777
|
+
*/
|
|
778
|
+
agentSubstrate(): Promise<AgentSubstrate>;
|
|
779
|
+
/**
|
|
780
|
+
* The agent a MUTATION runs on under this resolved policy — stage-two
|
|
781
|
+
* routing. A confined policy addresses the confined resident whose bwrap
|
|
782
|
+
* profile binds exactly what the mode grants, so the kernel enforces what
|
|
783
|
+
* the check decided; an escalated write (`danger-full-access`) or a disabled
|
|
784
|
+
* sandbox addresses the plain resident, which is what those mean. A distro
|
|
785
|
+
* without a usable bwrap refuses confined mutations instead of downgrading
|
|
786
|
+
* them — the same closed failure the command path has.
|
|
787
|
+
*
|
|
788
|
+
* @param policy - the resolved file-effect policy, or undefined when the
|
|
789
|
+
* caller pre-checked.
|
|
790
|
+
* @returns the agent to publish through.
|
|
791
|
+
*/
|
|
792
|
+
mutationAgent(policy: {
|
|
793
|
+
mode: string;
|
|
794
|
+
workspaceRoot: string;
|
|
795
|
+
} | undefined): Promise<WslAgent>;
|
|
796
|
+
/**
|
|
797
|
+
* Map a substrate error onto the peer's `FsError`, so the tool layer reads
|
|
798
|
+
* the same refusal dialect on both substrates. An agent that is out becomes
|
|
799
|
+
* an I/O error naming the cause — the share backend's "distro is gone" case
|
|
800
|
+
* is the same availability, but the message says which substrate failed.
|
|
801
|
+
*
|
|
802
|
+
* @param error - what the substrate raised.
|
|
803
|
+
* @returns the error to surface.
|
|
804
|
+
*/
|
|
805
|
+
peerError(error: unknown): FsError;
|
|
806
|
+
/**
|
|
807
|
+
* One resolved target's Linux path — the substrate's coordinate — falling
|
|
808
|
+
* back to the display spelling when the identity is not a distro share path.
|
|
809
|
+
*
|
|
810
|
+
* @param target - the resolved target.
|
|
811
|
+
* @returns the absolute Linux path.
|
|
812
|
+
*/
|
|
813
|
+
linuxOf(target: FsTarget): string;
|
|
814
|
+
/**
|
|
815
|
+
* The pinned distro, resolving WSL's default on first use.
|
|
816
|
+
*
|
|
817
|
+
* @returns the distro name.
|
|
818
|
+
*/
|
|
819
|
+
distro(): Promise<string>;
|
|
820
|
+
/**
|
|
821
|
+
* Translate one caller path into the world path, enforcing the distro fence.
|
|
822
|
+
*
|
|
823
|
+
* @param path - a Linux, UNC or Windows path.
|
|
824
|
+
* @param cwd - the caller's override of the configured default workdir.
|
|
825
|
+
* @returns the host path to operate on.
|
|
826
|
+
*/
|
|
827
|
+
worldPath(path: string, cwd?: string): Promise<string>;
|
|
828
|
+
/**
|
|
829
|
+
* Resolve a path to a target whose `targetKey` is the UNC identity the host
|
|
830
|
+
* I/O stack uses and whose `displayPath` is the Linux path the model sees.
|
|
831
|
+
*
|
|
832
|
+
* @param path - a Linux, UNC or Windows path.
|
|
833
|
+
* @param opts - optional `cwd` override and `signal`.
|
|
834
|
+
* @returns the resolved `FsTarget`.
|
|
835
|
+
*/
|
|
836
|
+
resolve(path: string, opts?: {
|
|
837
|
+
cwd?: string;
|
|
838
|
+
signal?: AbortSignal;
|
|
839
|
+
}): Promise<FsTarget>;
|
|
840
|
+
/**
|
|
841
|
+
* Enforce the per-call file-effect policy against one target and return the
|
|
842
|
+
* EXACT target the mutation must use, so the checked identity is the mutated
|
|
843
|
+
* one (no check-here-write-there window).
|
|
844
|
+
*
|
|
845
|
+
* `read-only` denies outright. `workspace-write` re-resolves the target NOW
|
|
846
|
+
* (reflecting a symlink ancestor swapped since the tool resolved it), requires
|
|
847
|
+
* containment under a writable root — the workspace plus the temp area of the
|
|
848
|
+
* world the workspace lives in — and hands back that fresh target.
|
|
849
|
+
* `danger-full-access` is the approved escalation and is not fenced. Refusal
|
|
850
|
+
* is the structured `FS_SANDBOX_DENIED` the tool layer maps to its
|
|
851
|
+
* model-facing `[sandbox: …]` marker and escalation hint.
|
|
852
|
+
*
|
|
853
|
+
* @param target - the resolved target to mutate.
|
|
854
|
+
* @param sandboxPolicy - the per-call mode and workspace root; omit for the
|
|
855
|
+
* deployment policy.
|
|
856
|
+
* @returns the target the mutation must use.
|
|
857
|
+
* @throws {FsError} with `FS_SANDBOX_DENIED` when the policy refuses it.
|
|
858
|
+
*/
|
|
859
|
+
checkedTarget(target: FsTarget, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsTarget>;
|
|
860
|
+
/**
|
|
861
|
+
* One target's metadata, answered from the distro.
|
|
862
|
+
*
|
|
863
|
+
* @param target - the resolved target.
|
|
864
|
+
* @param signal - optional cancellation.
|
|
865
|
+
* @returns the metadata, or undefined when absent.
|
|
866
|
+
*/
|
|
867
|
+
stat(target: FsTarget, signal?: AbortSignal): Promise<FsInfo | undefined>;
|
|
868
|
+
/**
|
|
869
|
+
* No-follow stat one path. The agent substrate answers from the distro,
|
|
870
|
+
* where a symlink is a first-class entry.
|
|
871
|
+
*
|
|
872
|
+
* @param path - a Linux, UNC or Windows path.
|
|
873
|
+
* @param opts - optional `cwd` override.
|
|
874
|
+
* @param signal - optional cancellation.
|
|
875
|
+
* @returns the path info, or undefined when absent.
|
|
876
|
+
*/
|
|
877
|
+
lstat(path: string, opts?: {
|
|
878
|
+
cwd?: string;
|
|
879
|
+
}, signal?: AbortSignal): Promise<FsPathInfo | undefined>;
|
|
880
|
+
/**
|
|
881
|
+
* Read one whole regular UTF-8 file from the distro.
|
|
882
|
+
*
|
|
883
|
+
* @param target - the resolved target.
|
|
884
|
+
* @param signal - optional cancellation.
|
|
885
|
+
* @returns the file text.
|
|
886
|
+
*/
|
|
887
|
+
readText(target: FsTarget, signal?: AbortSignal): Promise<string>;
|
|
888
|
+
/**
|
|
889
|
+
* Stream one whole regular UTF-8 file from the distro.
|
|
890
|
+
*
|
|
891
|
+
* @param target - the resolved target.
|
|
892
|
+
* @param signal - optional cancellation.
|
|
893
|
+
* @returns the text chunks.
|
|
894
|
+
*/
|
|
895
|
+
streamText(target: FsTarget, signal?: AbortSignal): Promise<AsyncIterable<string>>;
|
|
896
|
+
/**
|
|
897
|
+
* Read one whole regular file as raw bytes, bounded.
|
|
898
|
+
*
|
|
899
|
+
* @param target - the resolved target.
|
|
900
|
+
* @param signal - optional cancellation.
|
|
901
|
+
* @param maxBytes - inclusive byte cap on the content.
|
|
902
|
+
* @returns the bytes.
|
|
903
|
+
*/
|
|
904
|
+
readBytes(target: FsTarget, signal: AbortSignal | undefined, maxBytes: number): Promise<Uint8Array>;
|
|
905
|
+
/**
|
|
906
|
+
* Read one byte window of a regular file.
|
|
907
|
+
*
|
|
908
|
+
* @param target - the resolved target.
|
|
909
|
+
* @param range - the window.
|
|
910
|
+
* @param signal - optional cancellation.
|
|
911
|
+
* @returns the window's bytes.
|
|
912
|
+
*/
|
|
913
|
+
readByteRange(target: FsTarget, range: {
|
|
914
|
+
offset: number;
|
|
915
|
+
length: number;
|
|
916
|
+
}, signal?: AbortSignal): Promise<Uint8Array>;
|
|
917
|
+
/**
|
|
918
|
+
* List one directory level. The substrate's rows already carry Linux display
|
|
919
|
+
* paths and canonical identities — nothing to rewrite.
|
|
920
|
+
*
|
|
921
|
+
* @param target - the resolved directory.
|
|
922
|
+
* @param signal - optional cancellation.
|
|
923
|
+
* @returns the directory rows.
|
|
924
|
+
*/
|
|
925
|
+
listDir(target: FsTarget, signal?: AbortSignal): Promise<FsDirEntry[]>;
|
|
926
|
+
/**
|
|
927
|
+
* Map an absolute host path into this execution world, SYNCHRONOUSLY — the
|
|
928
|
+
* upstream contract (`FileSystem.processPathFromHostPath`) and what its
|
|
929
|
+
* consumers rely on: `packages/spill/spill-policy` passes this method as a
|
|
930
|
+
* plain callback over image attachments, and `packages/api/session-controller`
|
|
931
|
+
* plus the UI's open/reveal path compare the return value against `undefined`
|
|
932
|
+
* without awaiting. The mapping is therefore built from facts that are already
|
|
933
|
+
* known — the distro, once `distro()` has resolved it (the pinned config, or
|
|
934
|
+
* the default `wsl.exe` answered earlier: the first filesystem operation
|
|
935
|
+
* resolves it), and the configured `cwd`, or the resolved home for a relative
|
|
936
|
+
* path. A call that would have to ask `wsl.exe` — a cold default distro, or a
|
|
937
|
+
* relative path before the home is known — answers `undefined`, which every
|
|
938
|
+
* consumer reads as "no mapping", the same answer the ssh provider gives for
|
|
939
|
+
* a path it cannot map.
|
|
940
|
+
*
|
|
941
|
+
* The VALUE is the world spelling the backend keys on (the UNC identity for a
|
|
942
|
+
* distro path), so the consumers' round trip — map, `resolve`, `processPath`,
|
|
943
|
+
* compare with the original host path — closes exactly as it does for the
|
|
944
|
+
* host backend. Real operations are unaffected: they take the async
|
|
945
|
+
* {@link WslFileSystem.worldPath} and its loud refusals.
|
|
946
|
+
*
|
|
947
|
+
* @param hostPath - an absolute host path (a relative one maps only when the
|
|
948
|
+
* configured `cwd` or a known home gives it a base).
|
|
949
|
+
* @returns the world path for the same file, or undefined when it cannot be
|
|
950
|
+
* mapped with what is already known.
|
|
951
|
+
*/
|
|
952
|
+
processPathFromHostPath(hostPath: string): string | undefined;
|
|
953
|
+
/**
|
|
954
|
+
* Watch from inside the distro, where the kernel can actually report change:
|
|
955
|
+
* a long-lived `wsl.exe` poll loop (`lib/watcher.js`) fires the seam's
|
|
956
|
+
* invalidation callback. The seam asks for coarse invalidation, not events,
|
|
957
|
+
* so a poll is honest — and creations and deletions are both seen, because
|
|
958
|
+
* they bump the parent directory's mtime. Replaces the previous flat
|
|
959
|
+
* refusal (`FS_IO_ERROR`), which was the correct answer over 9p only.
|
|
960
|
+
*
|
|
961
|
+
* A target that disappears mid-watch ends the loop with a report through
|
|
962
|
+
* `changed` — never the armed-but-never-fires silence a missing directory
|
|
963
|
+
* would otherwise produce. The scan's depth is `watchMaxDepth`.
|
|
964
|
+
*
|
|
965
|
+
* @param target - the resolved file or directory to observe.
|
|
966
|
+
* @param changed - the invalidation callback.
|
|
967
|
+
* @param signal - cancels initialization; the caller closes an armed watcher.
|
|
968
|
+
* @returns resolves once active, with the async close function.
|
|
969
|
+
*/
|
|
970
|
+
watch(target: FsTarget, changed: (error?: Error) => void, signal: AbortSignal): Promise<() => Promise<void>>;
|
|
971
|
+
/**
|
|
972
|
+
* Fence the write by the per-call policy, then publish through the
|
|
973
|
+
* substrate. The fence's fresh target is the one checked and written, so a
|
|
974
|
+
* refusal costs no I/O and cannot be raced by a swapped symlink; the guard
|
|
975
|
+
* rides the write op, where the agent re-verifies it one syscall before the
|
|
976
|
+
* rename and the confined resident enforces it in the kernel.
|
|
977
|
+
*
|
|
978
|
+
* @param target - the resolved target.
|
|
979
|
+
* @param content - the full new file content.
|
|
980
|
+
* @param expected - the write intent.
|
|
981
|
+
* @param signal - optional cancellation.
|
|
982
|
+
* @param sandboxPolicy - the per-call mode and workspace root; omit for the
|
|
983
|
+
* deployment policy.
|
|
984
|
+
* @returns the write outcome.
|
|
985
|
+
*/
|
|
986
|
+
writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsWriteOutcome>;
|
|
987
|
+
/**
|
|
988
|
+
* Fence the edit by the same policy, keeping the guard's observable
|
|
989
|
+
* behaviour.
|
|
990
|
+
*
|
|
991
|
+
* @param target - the resolved target.
|
|
992
|
+
* @param edit - the literal search/replace request.
|
|
993
|
+
* @param expected - the version guard.
|
|
994
|
+
* @param signal - optional cancellation.
|
|
995
|
+
* @param sandboxPolicy - the per-call mode and workspace root; omit for the
|
|
996
|
+
* deployment policy.
|
|
997
|
+
* @returns the edit outcome.
|
|
998
|
+
*/
|
|
999
|
+
editText(target: FsTarget, edit: FsEditRequest, expected?: {
|
|
1000
|
+
version: FsVersion;
|
|
1001
|
+
}, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
|
|
1002
|
+
}
|
|
1003
|
+
export {};
|