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/sandbox-core.js
CHANGED
|
@@ -10,20 +10,21 @@
|
|
|
10
10
|
* contract (`code: "SANDBOX_UNAVAILABLE"`), and what CI runs is the code that
|
|
11
11
|
* ships.
|
|
12
12
|
*
|
|
13
|
+
* This is a TypeScript source built to `lib/sandbox-core.js`; edit THIS file
|
|
14
|
+
* and run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
15
|
+
* `pnpm test` fails when it drifts.
|
|
16
|
+
*
|
|
13
17
|
* @module dsh-plugin-wsl/sandbox-core
|
|
14
18
|
*/
|
|
15
|
-
|
|
16
19
|
import { createProbeCache } from "./probe-cache.js";
|
|
17
20
|
import { toLinuxPath } from "./paths.js";
|
|
18
21
|
import { runCapture } from "./wsl.js";
|
|
19
22
|
import { bwrapArgvPrefix } from "./bwrap.js";
|
|
20
|
-
|
|
21
23
|
/**
|
|
22
24
|
* bwrap's denial dialect: a write outside the granted mounts fails with EROFS,
|
|
23
25
|
* whose message is "Read-only file system". Classified case-insensitively.
|
|
24
26
|
*/
|
|
25
27
|
const DENIAL_SIGNATURES = ["read-only file system"];
|
|
26
|
-
|
|
27
28
|
/**
|
|
28
29
|
* Runner-side diagnostics. Bubblewrap reports its own failures with a `bwrap: `
|
|
29
30
|
* prefix and exits 1; exit status alone never proves runner failure (the
|
|
@@ -31,7 +32,6 @@ const DENIAL_SIGNATURES = ["read-only file system"];
|
|
|
31
32
|
* command that merely prints the signature is not misread as "never ran".
|
|
32
33
|
*/
|
|
33
34
|
const RUNNER_FAILURE_RULES = [{ fatalSignatures: ["bwrap: "] }];
|
|
34
|
-
|
|
35
35
|
/**
|
|
36
36
|
* Enforcement is **partial**, not `full` like upstream's Linux rung.
|
|
37
37
|
*
|
|
@@ -44,7 +44,6 @@ const RUNNER_FAILURE_RULES = [{ fatalSignatures: ["bwrap: "] }];
|
|
|
44
44
|
* the honest `partial` and README states where the hole is.
|
|
45
45
|
*/
|
|
46
46
|
const ENFORCEMENT = "partial";
|
|
47
|
-
|
|
48
47
|
/**
|
|
49
48
|
* The one mitigation the profile can offer, and its honest ceiling.
|
|
50
49
|
*
|
|
@@ -60,98 +59,251 @@ const ENFORCEMENT = "partial";
|
|
|
60
59
|
* either way, and the mask is a decision an operator makes to shrink the hole,
|
|
61
60
|
* recorded as the `windowsDrive: "masked"` fact the model can see.
|
|
62
61
|
*/
|
|
63
|
-
|
|
64
62
|
/**
|
|
65
63
|
* Probe verdicts, keyed by `wslPath\0distro`. The policy — a success is durable, a
|
|
66
64
|
* failure is re-probed, concurrent probes are shared — lives in `probe-cache.js`,
|
|
67
65
|
* where it is unit-testable without the peers this module needs.
|
|
68
66
|
*/
|
|
69
67
|
const probeVerdicts = createProbeCache();
|
|
70
|
-
|
|
68
|
+
/** The cache key one (wsl.exe, distro) pair's bwrap verdict lives under. */
|
|
69
|
+
const probeKey = (wslPath, distro) => `${wslPath}\u0000${distro}`;
|
|
71
70
|
/**
|
|
72
|
-
*
|
|
71
|
+
* The direct install command each detected package family gets in the remedy.
|
|
72
|
+
* The bootstrap script remains the first-choice path — it detects the family
|
|
73
|
+
* itself — but a user pasting the message on a dnf or pacman distro must not
|
|
74
|
+
* be handed an `apt-get` line.
|
|
75
|
+
*/
|
|
76
|
+
const FAMILY_INSTALL = {
|
|
77
|
+
apt: "apt-get install -y bubblewrap",
|
|
78
|
+
dnf: "dnf install -y bubblewrap",
|
|
79
|
+
pacman: "pacman -S --noconfirm bubblewrap",
|
|
80
|
+
zypper: "zypper --non-interactive install bubblewrap",
|
|
81
|
+
};
|
|
82
|
+
/** How many characters of the failed probe's output the remedy quotes. */
|
|
83
|
+
const PROBE_OUTPUT_CAP = 300;
|
|
84
|
+
/**
|
|
85
|
+
* Classify one failed probe's output. `bwrap: ` on the output means the binary
|
|
86
|
+
* RAN and refused — a present-but-unusable bwrap (an old build, a kernel
|
|
87
|
+
* without unprivileged user namespaces), which no reinstall fixes. wsl.exe's
|
|
88
|
+
* own relay for a missing `--exec` binary names the exec instead, which is the
|
|
89
|
+
* install case. Anything else is unclassified and gets the install remedy with
|
|
90
|
+
* the raw output quoted, so neither reading is lost.
|
|
73
91
|
*
|
|
74
|
-
* @param
|
|
75
|
-
* @
|
|
76
|
-
* the failure type `confine` throws; production binds the peer's class so
|
|
77
|
-
* `instanceof` consumers keep matching, tests bind a stand-in with the same
|
|
78
|
-
* `code`.
|
|
79
|
-
* @returns {typeof WslSandboxCore} the sandbox class (see `lib/sandbox.js` for
|
|
80
|
-
* the shipped binding).
|
|
92
|
+
* @param output - the failed probe's combined stdout+stderr, NULs stripped.
|
|
93
|
+
* @returns why the probe failed, as the remedy needs it.
|
|
81
94
|
*/
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
function classifyProbeOutput(output) {
|
|
96
|
+
if (/\bbwrap: /i.test(output))
|
|
97
|
+
return "broken";
|
|
98
|
+
if (/no such file|execv|not found|cannot run|could not be started/i.test(output))
|
|
99
|
+
return "missing";
|
|
100
|
+
return "unknown";
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The package manager the distro offers, for the remedy's direct command. Only
|
|
104
|
+
* consulted on a FAILED probe, so a healthy distro never pays for it; a probe
|
|
105
|
+
* that cannot answer leaves the remedy with the package-manager-agnostic line.
|
|
106
|
+
*
|
|
107
|
+
* @param wslPath - the `wsl.exe` path the failed probe used.
|
|
108
|
+
* @param distro - the distro the failed probe ran in.
|
|
109
|
+
* @param signal - optional cancellation shared with the probe.
|
|
110
|
+
* @returns the first family whose manager exists, or undefined.
|
|
111
|
+
*/
|
|
112
|
+
async function detectPackageFamily(wslPath, distro, signal) {
|
|
113
|
+
try {
|
|
114
|
+
const script = "for m in apt-get dnf pacman zypper; do command -v $m && break; done";
|
|
115
|
+
// `command -v` answers with the PATH-resolved path, so matching on the
|
|
116
|
+
// trailing name is exact — a bare manager name never comes back.
|
|
117
|
+
const manager = (await runCapture([wslPath, "-d", distro, "--exec", "sh", "-c", script], signal)).trim().split("\n")[0]?.trim() ?? "";
|
|
118
|
+
if (manager.endsWith("/apt-get"))
|
|
119
|
+
return "apt";
|
|
120
|
+
if (manager.endsWith("/dnf"))
|
|
121
|
+
return "dnf";
|
|
122
|
+
if (manager.endsWith("/pacman"))
|
|
123
|
+
return "pacman";
|
|
124
|
+
if (manager.endsWith("/zypper"))
|
|
125
|
+
return "zypper";
|
|
126
|
+
return undefined;
|
|
94
127
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Compose the remedy one failed probe reports. Pure apart from the optional
|
|
134
|
+
* family probe it may issue (a failed probe only), so the text is unit-testable
|
|
135
|
+
* through a fake `wsl.exe`.
|
|
136
|
+
*
|
|
137
|
+
* @param wslPath - the `wsl.exe` path the probe used.
|
|
138
|
+
* @param distro - the distro the probe ran in.
|
|
139
|
+
* @param cause - the raw probe failure.
|
|
140
|
+
* @param signal - optional cancellation shared with the probe.
|
|
141
|
+
* @returns the operator-facing remedy text.
|
|
142
|
+
*/
|
|
143
|
+
async function composeBwrapFailure(wslPath, distro, cause, signal) {
|
|
144
|
+
const output = String(cause?.message ?? cause ?? "").slice(0, PROBE_OUTPUT_CAP).trim();
|
|
145
|
+
const kind = classifyProbeOutput(output);
|
|
146
|
+
if (kind === "broken") {
|
|
147
|
+
// A broken verdict requires the bwrap banner, so the output is never empty here.
|
|
148
|
+
return (`bwrap is installed inside distro "${distro}" but failed the usability probe: ${output}. ` +
|
|
149
|
+
`This is usually the kernel (unprivileged user namespaces disabled) or an unusable bwrap build rather than a missing package, so reinstalling will not help. ` +
|
|
150
|
+
`Check \`wsl.exe -d ${distro} --exec bwrap --version\` inside the distro, or set \`sandbox: false\` on the wsl-shell and wsl-fs rows to run unconfined.`);
|
|
118
151
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
throw new SandboxUnavailableError(
|
|
140
|
-
policy.mode,
|
|
141
|
-
`bwrap is not usable inside distro "${distro}". Install it with ` +
|
|
142
|
-
`"scripts/bootstrap.sh ${distro} --install" (from the package), or directly: ` +
|
|
143
|
-
`wsl.exe -d ${distro} -u root -- apt-get install -y bubblewrap`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
const workspaceRoot = toLinuxPath(policy.workspaceRoot, { distro });
|
|
147
|
-
const prefix = bwrapArgvPrefix({ ...policy, workspaceRoot }, { maskWindowsDrive: this.maskWindowsDrive });
|
|
148
|
-
return {
|
|
149
|
-
argv: [...prefix, ...argv],
|
|
150
|
-
enforcement: ENFORCEMENT,
|
|
151
|
-
denialSignatures: DENIAL_SIGNATURES,
|
|
152
|
-
runnerFailureRules: RUNNER_FAILURE_RULES,
|
|
153
|
-
windowsDrive: this.maskWindowsDrive ? "masked" : "visible",
|
|
154
|
-
};
|
|
152
|
+
const remedy = missingBwrapRemedy(distro, await detectPackageFamily(wslPath, distro, signal));
|
|
153
|
+
return kind === "unknown" && output.length > 0 ? `${remedy} The probe failed with: ${output}` : remedy;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The canonical remedy for a distro whose `bwrap` is missing — also the
|
|
157
|
+
* fallback for callers that know only "the probe failed", without a classified
|
|
158
|
+
* reason. The direct command is family-aware when the caller knows the family;
|
|
159
|
+
* the bootstrap script is always the first-choice path because it detects the
|
|
160
|
+
* family itself.
|
|
161
|
+
*
|
|
162
|
+
* @param distro - the distro to install into.
|
|
163
|
+
* @param family - the detected package family, when known.
|
|
164
|
+
* @returns the operator-facing remedy text.
|
|
165
|
+
*/
|
|
166
|
+
export function missingBwrapRemedy(distro, family) {
|
|
167
|
+
const direct = family === undefined ? undefined : FAMILY_INSTALL[family];
|
|
168
|
+
if (direct === undefined) {
|
|
169
|
+
return (`bwrap is not usable inside distro "${distro}". Install it with ` +
|
|
170
|
+
`"scripts/bootstrap.sh ${distro} --install" (from the package), or install the ` +
|
|
171
|
+
`"bubblewrap" package with the distro's package manager.`);
|
|
155
172
|
}
|
|
156
|
-
|
|
173
|
+
return (`bwrap is not usable inside distro "${distro}". Install it with ` +
|
|
174
|
+
`"scripts/bootstrap.sh ${distro} --install" (from the package), or directly: ` +
|
|
175
|
+
`wsl.exe -d ${distro} -u root -- ${direct}`);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The remedy for a bwrap that PROBED healthy but failed a run — the runtime
|
|
179
|
+
* twin of {@link missingBwrapRemedy}. No family knowledge exists on this path
|
|
180
|
+
* (the probe succeeded, so no family probe ever ran), and reinstalling is only
|
|
181
|
+
* one of the plausible fixes, so the hint leads with diagnosis.
|
|
182
|
+
*
|
|
183
|
+
* @param distro - the distro whose bwrap failed at run time.
|
|
184
|
+
* @returns the operator-facing hint text.
|
|
185
|
+
*/
|
|
186
|
+
export function bwrapInstallHint(distro) {
|
|
187
|
+
return (`bwrap failed at run time inside distro "${distro}". Check \`bwrap --version\` there, ` +
|
|
188
|
+
`or reinstall it: "scripts/bootstrap.sh ${distro} --install".`);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Append {@link bwrapInstallHint} to a runner-failure detail that names bwrap.
|
|
192
|
+
* A probe-time failure composes its own classified remedy; this covers the
|
|
193
|
+
* OTHER order — bwrap probed healthy, then failed a real command — where no
|
|
194
|
+
* probe failure is on record and the raw `bwrap: …` line is all the model
|
|
195
|
+
* would see. Details that do not name bwrap pass through untouched: the
|
|
196
|
+
* runner-failure rules match the confinement runner, and the confinement
|
|
197
|
+
* runner is always bwrap here, but the wording stays the decider so a future
|
|
198
|
+
* rule change cannot attach a bwrap remedy to a foreign failure.
|
|
199
|
+
*
|
|
200
|
+
* @param detail - the classified runner-failure detail.
|
|
201
|
+
* @param distro - the distro the command ran in.
|
|
202
|
+
* @returns the detail, with the remedy appended when it names bwrap.
|
|
203
|
+
*/
|
|
204
|
+
export function withBwrapHint(detail, distro) {
|
|
205
|
+
return /\bbwrap\b/i.test(detail) ? `${detail} — ${bwrapInstallHint(distro)}` : detail;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Probe one distro's `bwrap` under the shared verdict cache. The probe mirrors
|
|
209
|
+
* upstream's: apply the real read-only profile around `true`, and treat a zero
|
|
210
|
+
* exit as the kernel having accepted and enforced it. A missing `bwrap` fails
|
|
211
|
+
* the spawn and probes unusable, which is what makes the providers fail closed
|
|
212
|
+
* rather than silently run unconfined.
|
|
213
|
+
*
|
|
214
|
+
* A success is remembered for the process lifetime; a failure is not, so a
|
|
215
|
+
* `bubblewrap` installed while the app is running is believed on the next
|
|
216
|
+
* command. The failure's composed remedy stays readable through
|
|
217
|
+
* {@link bwrapFailure}, which is how every refusal and the preflight surface it.
|
|
218
|
+
*
|
|
219
|
+
* @param options - the `wsl.exe` path and the distro to probe.
|
|
220
|
+
* @param options.wslPath - the `wsl.exe` path.
|
|
221
|
+
* @param options.distro - the distro to probe.
|
|
222
|
+
* @param options.signal - optional cancellation.
|
|
223
|
+
* @returns true when a confined command can run there.
|
|
224
|
+
*/
|
|
225
|
+
export async function bwrapUsable(options) {
|
|
226
|
+
const { wslPath, distro, signal } = options;
|
|
227
|
+
return probeVerdicts.run(probeKey(wslPath, distro), async () => {
|
|
228
|
+
const prefix = bwrapArgvPrefix({ mode: "read-only", workspaceRoot: "/" });
|
|
229
|
+
try {
|
|
230
|
+
await runCapture([wslPath, "-d", distro, "--exec", ...prefix, "true"], signal);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
catch (cause) {
|
|
234
|
+
throw new Error(await composeBwrapFailure(wslPath, distro, cause, signal));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The remedy text of a key's last failed probe, as {@link bwrapUsable} composed
|
|
240
|
+
* it — the same string the providers' refusals carry. Undefined while the probe
|
|
241
|
+
* never failed (or last succeeded).
|
|
242
|
+
*
|
|
243
|
+
* @param wslPath - the `wsl.exe` path the probe used.
|
|
244
|
+
* @param distro - the distro the probe ran in.
|
|
245
|
+
* @returns the composed remedy, or undefined.
|
|
246
|
+
*/
|
|
247
|
+
export function bwrapFailure(wslPath, distro) {
|
|
248
|
+
return probeVerdicts.failure(probeKey(wslPath, distro));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Build the sandbox class, bound to one unavailable-error implementation.
|
|
252
|
+
*
|
|
253
|
+
* @param deps - the injected peer surface.
|
|
254
|
+
* @returns the sandbox class (see `lib/sandbox.js` for the shipped binding).
|
|
255
|
+
*/
|
|
256
|
+
export function createSandboxCore({ SandboxUnavailableError }) {
|
|
257
|
+
return class WslSandboxCore {
|
|
258
|
+
/**
|
|
259
|
+
* @param config - the `wsl.exe` path, overridable for a non-standard install.
|
|
260
|
+
*/
|
|
261
|
+
constructor(config = {}) {
|
|
262
|
+
this.wslPath = config.wslPath ?? "wsl.exe";
|
|
263
|
+
this.maskWindowsDrive = config.maskWindowsDrive ?? false;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Whether `bwrap` can create a profile inside this distro. Delegates to the
|
|
267
|
+
* shared free probe — the same cache the preflight service and every other
|
|
268
|
+
* instance instance read, so a verdict paid once is paid once per process.
|
|
269
|
+
*
|
|
270
|
+
* @param distro - the distro to probe.
|
|
271
|
+
* @param signal - optional cancellation.
|
|
272
|
+
* @returns true when a confined command can run there.
|
|
273
|
+
*/
|
|
274
|
+
async usable(distro, signal) {
|
|
275
|
+
return bwrapUsable({ wslPath: this.wslPath, distro, signal });
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Wrap one distro-side argv in the bwrap profile the policy asks for.
|
|
279
|
+
*
|
|
280
|
+
* The policy's `workspaceRoot` arrives in whichever coordinate system the
|
|
281
|
+
* caller speaks — a session workspace is a UNC path — and is translated into the
|
|
282
|
+
* Linux path `bwrap` must bind inside the distro.
|
|
283
|
+
*
|
|
284
|
+
* @param argv - the argv to run INSIDE the distro (shell, its flags, command).
|
|
285
|
+
* @param policy - the resolved confined policy (`mode` is never
|
|
286
|
+
* `danger-full-access`; the caller does not confine that case at all).
|
|
287
|
+
* @param options - the distro to run in and optional cancellation.
|
|
288
|
+
* @returns the wrapped argv plus the settlement-classification facts, in the
|
|
289
|
+
* seam's `ConfinedArgv` shape.
|
|
290
|
+
* @throws SandboxUnavailableError when the distro has no usable `bwrap`, so a
|
|
291
|
+
* requested confinement never degrades into an unconfined run.
|
|
292
|
+
*/
|
|
293
|
+
async confine(argv, policy, options) {
|
|
294
|
+
const { distro, signal } = options;
|
|
295
|
+
if (!(await this.usable(distro, signal))) {
|
|
296
|
+
throw new SandboxUnavailableError(policy.mode, bwrapFailure(this.wslPath, distro) ?? missingBwrapRemedy(distro));
|
|
297
|
+
}
|
|
298
|
+
const workspaceRoot = toLinuxPath(policy.workspaceRoot, { distro });
|
|
299
|
+
const prefix = bwrapArgvPrefix({ ...policy, workspaceRoot }, { maskWindowsDrive: this.maskWindowsDrive });
|
|
300
|
+
return {
|
|
301
|
+
argv: [...prefix, ...argv],
|
|
302
|
+
enforcement: ENFORCEMENT,
|
|
303
|
+
denialSignatures: DENIAL_SIGNATURES,
|
|
304
|
+
runnerFailureRules: RUNNER_FAILURE_RULES,
|
|
305
|
+
windowsDrive: this.maskWindowsDrive ? "masked" : "visible",
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
};
|
|
157
309
|
}
|
package/lib/sandbox.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux-side confinement this plugin applies to commands that run inside a
|
|
3
|
+
* distro.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is not `ctx.sandbox`
|
|
6
|
+
*
|
|
7
|
+
* DSH's local sandbox provider selects its runner chain by `process.platform`.
|
|
8
|
+
* This harness process is Windows, so it selects the windows-acl runner — and
|
|
9
|
+
* that runner's restricted, low-integrity token **cannot reach WSL at all**
|
|
10
|
+
* (`wsl.exe` fails with `Wsl/E_ACCESSDENIED`). The confinement that governs a
|
|
11
|
+
* distro command therefore has to be *built* on the host and *executed* inside
|
|
12
|
+
* the distro, which is what this module produces: an argv whose program is
|
|
13
|
+
* `bwrap`, handed to `wsl.exe --exec` like any other distro command.
|
|
14
|
+
*
|
|
15
|
+
* Consuming the seam is deliberately not part of this: `dsh-tool-bash` reads
|
|
16
|
+
* `ctx.shell.sandboxMode` (the executor's capability fact) plus
|
|
17
|
+
* `ctx.sandboxPolicy`, and never asks `ctx.sandbox` for anything. A WSL
|
|
18
|
+
* executor can therefore own its confinement without shadowing the
|
|
19
|
+
* composition-level provider that host sessions still need.
|
|
20
|
+
*
|
|
21
|
+
* ## Fidelity to the upstream Linux rung
|
|
22
|
+
*
|
|
23
|
+
* The profile arguments, the denial dialect and the runner-failure rules mirror
|
|
24
|
+
* `@deepseek-ai/dsh-sandbox-local`'s `bwrap` rung, so a denial reads the same to
|
|
25
|
+
* the tool layer whether the command ran on a Linux host or inside a distro.
|
|
26
|
+
* The one deliberate difference is the core's `partial` enforcement: see
|
|
27
|
+
* `lib/sandbox-core.js` for what it reports and why.
|
|
28
|
+
*
|
|
29
|
+
* The class itself lives in `lib/sandbox-core.js`, free of DSH peers — this
|
|
30
|
+
* module is the shipped binding that hands it the peer's `SandboxUnavailableError`,
|
|
31
|
+
* so `instanceof` consumers keep matching the class their composition knows.
|
|
32
|
+
*
|
|
33
|
+
* This is a TypeScript source built to `lib/sandbox.js`; edit THIS file and run
|
|
34
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
35
|
+
* fails when it drifts.
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-plugin-wsl/sandbox
|
|
38
|
+
*/
|
|
39
|
+
export { bwrapProfileArgs } from "./bwrap.js";
|
|
40
|
+
/**
|
|
41
|
+
* The confinement over WSL: the probe-then-confine class bound to the peer's
|
|
42
|
+
* failure type. See the core module for the probe policy and the profile.
|
|
43
|
+
*/
|
|
44
|
+
export declare const WslSandbox: new (config?: {
|
|
45
|
+
wslPath?: string;
|
|
46
|
+
maskWindowsDrive?: boolean;
|
|
47
|
+
} | undefined) => import("./sandbox-core.js").WslSandboxCoreInstance;
|
|
48
|
+
export default WslSandbox;
|
package/lib/sandbox.js
CHANGED
|
@@ -30,21 +30,21 @@
|
|
|
30
30
|
* module is the shipped binding that hands it the peer's `SandboxUnavailableError`,
|
|
31
31
|
* so `instanceof` consumers keep matching the class their composition knows.
|
|
32
32
|
*
|
|
33
|
+
* This is a TypeScript source built to `lib/sandbox.js`; edit THIS file and run
|
|
34
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
35
|
+
* fails when it drifts.
|
|
36
|
+
*
|
|
33
37
|
* @module dsh-plugin-wsl/sandbox
|
|
34
38
|
*/
|
|
35
|
-
|
|
36
39
|
import { SandboxUnavailableError } from "@deepseek-ai/dsh-sandbox";
|
|
37
40
|
import { createSandboxCore } from "./sandbox-core.js";
|
|
38
|
-
|
|
39
41
|
// The profile and the argv prefix are also what `lib/agent-confined.js` builds
|
|
40
42
|
// for its residents, so both live in the peer-free `lib/bwrap.js` and are
|
|
41
43
|
// re-exported here, where the sandbox's callers have always found them.
|
|
42
44
|
export { bwrapProfileArgs } from "./bwrap.js";
|
|
43
|
-
|
|
44
45
|
/**
|
|
45
46
|
* The confinement over WSL: the probe-then-confine class bound to the peer's
|
|
46
47
|
* failure type. See the core module for the probe policy and the profile.
|
|
47
48
|
*/
|
|
48
49
|
export const WslSandbox = createSandboxCore({ SandboxUnavailableError });
|
|
49
|
-
|
|
50
50
|
export default WslSandbox;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-backed execution handle for the file-search spawn.
|
|
3
|
+
*
|
|
4
|
+
* `dsh-tool-fs-search` consumes a deliberately small slice of the subprocess
|
|
5
|
+
* handle: it awaits `handle.done` for `{exitCode, signal}`, then reads
|
|
6
|
+
* `handle.collected.stdout/stderr.readFrom(0)` — nothing else. This module
|
|
7
|
+
* builds exactly that surface around one resident-agent `exec`, so a distro
|
|
8
|
+
* search pays one round trip on the warm agent (~10 ms) instead of a one-shot
|
|
9
|
+
* `wsl.exe` spawn (~400 ms of process startup for a few ms of rg).
|
|
10
|
+
*
|
|
11
|
+
* The agent being out is not an error condition here — it is the fallback
|
|
12
|
+
* cue. `spawnFallback` (the shipped one-shot handle the caller builds) is
|
|
13
|
+
* consulted lazily inside `done`; from that point the facade proxies the
|
|
14
|
+
* delegate's `collected` and `done` verbatim. Neither transport ever touches
|
|
15
|
+
* the 9p share.
|
|
16
|
+
*
|
|
17
|
+
* Cancellation shares one channel: the caller's `signal` and `kill()` abort
|
|
18
|
+
* the agent exec, which settles `done` as killed (`{exitCode: null, signal:
|
|
19
|
+
* "SIGTERM"}`) — the same shape the one-shot path reports for a killed search.
|
|
20
|
+
* A genuine agent failure (anything but unavailability) propagates through
|
|
21
|
+
* `done` as a rejection, which the tool classifies as a search failure.
|
|
22
|
+
*
|
|
23
|
+
* Pure shaping over injected collaborators, so the handle semantics are
|
|
24
|
+
* unit-testable without a distro.
|
|
25
|
+
*
|
|
26
|
+
* This is a TypeScript source built to `lib/search-exec.js`; edit THIS file and
|
|
27
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
28
|
+
* fails when it drifts.
|
|
29
|
+
*
|
|
30
|
+
* @module dsh-plugin-wsl/search-exec
|
|
31
|
+
*/
|
|
32
|
+
import type { AgentExecRunner } from "./agent-exec.js";
|
|
33
|
+
/**
|
|
34
|
+
* The output-stream reader the search tool consumes: `handle.collected.stdout`
|
|
35
|
+
* / `stderr` expose `readFrom(offset)` with the seam's standard shape.
|
|
36
|
+
*/
|
|
37
|
+
export interface SearchCollectedReader {
|
|
38
|
+
/** Read the stream from a byte offset. */
|
|
39
|
+
readFrom: (from: number) => {
|
|
40
|
+
text: string;
|
|
41
|
+
lossy: boolean;
|
|
42
|
+
nextOffset: number;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The slice of the subprocess handle the file-search tool consumes
|
|
47
|
+
* (`dsh-tool-fs-search` `runRipgrep`): settle on `done`, then read the
|
|
48
|
+
* collected streams. Nothing else of the handle is contract.
|
|
49
|
+
*/
|
|
50
|
+
export interface SearchExecutionHandle {
|
|
51
|
+
/** Settles with the exit code and signal; rejects on a genuine failure. */
|
|
52
|
+
done: Promise<{
|
|
53
|
+
exitCode: number | null;
|
|
54
|
+
signal: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
/** The collected stream readers; a delegated handle's own on fallback. */
|
|
57
|
+
collected: {
|
|
58
|
+
stdout?: SearchCollectedReader | undefined;
|
|
59
|
+
stderr?: SearchCollectedReader | undefined;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Optional cooperative kill; the tool relies on its `signal` for
|
|
63
|
+
* cancellation instead.
|
|
64
|
+
*/
|
|
65
|
+
kill?: () => boolean | void;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Run one search command through the agent and present it as a search handle.
|
|
69
|
+
*
|
|
70
|
+
* @param object - the handle's collaborators.
|
|
71
|
+
* @returns the search handle (`done`, `collected`, `kill`).
|
|
72
|
+
*/
|
|
73
|
+
export declare function searchExecutionHandle({ agent, cwd, argv, maxOutputBytes, signal, spawnFallback }: {
|
|
74
|
+
/**
|
|
75
|
+
* The {@link WslAgent}-like runner: `exec` accepting
|
|
76
|
+
* `{cwd, argv, maxOutputBytes, signal}` and resolving
|
|
77
|
+
* `{exitCode, stdout, stderr}`; throws `AgentUnavailableError` when out.
|
|
78
|
+
*/
|
|
79
|
+
agent: AgentExecRunner;
|
|
80
|
+
/** The Linux working directory to search from. */
|
|
81
|
+
cwd: string;
|
|
82
|
+
/** The distro-side argv (`rg` and its arguments). */
|
|
83
|
+
argv: string[];
|
|
84
|
+
/**
|
|
85
|
+
* Per-stream capture ceiling the agent enforces; the tool's own raw-output
|
|
86
|
+
* budget rides here.
|
|
87
|
+
*/
|
|
88
|
+
maxOutputBytes?: number;
|
|
89
|
+
/** The caller's cancellation. */
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
/**
|
|
92
|
+
* Builds the one-shot handle the facade delegates to when the agent is out;
|
|
93
|
+
* called at most once, lazily.
|
|
94
|
+
*/
|
|
95
|
+
spawnFallback: () => SearchExecutionHandle;
|
|
96
|
+
}): SearchExecutionHandle;
|