dsh-win-multi-bash 0.1.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/LICENSE +21 -0
- package/README.i18n.yaml +8 -0
- package/README.md +162 -0
- package/README.zh.md +162 -0
- package/THIRD_PARTY_NOTICES +43 -0
- package/cordis.patch.yml +64 -0
- package/lib/bash-git/index.js +382 -0
- package/lib/bash-git/invariant.js +23 -0
- package/lib/bash-wsl/index.js +335 -0
- package/lib/bash-wsl/invariant.js +23 -0
- package/lib/index.js +20 -0
- package/lib/shell-select/index.js +169 -0
- package/lib/shell-select/invariant.js +23 -0
- package/lib/tool-bash/index.js +526 -0
- package/lib/tool-bash/invariant.js +23 -0
- package/lib/tool-bash/types/background.js +25 -0
- package/lib/tool-bash/types/factory.js +390 -0
- package/lib/tool-bash/types/git-bash.js +13 -0
- package/lib/tool-bash/types/index.js +16 -0
- package/lib/tool-bash/types/invariant.js +22 -0
- package/lib/tool-bash/types/render.js +96 -0
- package/lib/tool-bash/types/wsl-bash.js +13 -0
- package/lib/vendor/bwrap-profiles.js +32 -0
- package/lib/vendor/helpers.js +111 -0
- package/package.json +95 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
|
|
4
|
+
import { classifyDenial, classifyRunnerFailure, matchesSignature } from "../vendor/helpers.js";
|
|
5
|
+
import { BWRAP_RUNNER_FAILURE_RULES, bwrapProfileArgs } from "../vendor/bwrap-profiles.js";
|
|
6
|
+
import { lstatSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
/**
|
|
9
|
+
* The bwrap denial dialect: a denied file effect prints this signature on
|
|
10
|
+
* stderr (mirrors DENIAL_SIGNATURES.bwrap in the base sandbox-local), so the
|
|
11
|
+
* sandbox facts the tool renders can report `denied` instead of always false.
|
|
12
|
+
*/
|
|
13
|
+
const BWRAP_DENIAL_SIGNATURES = ["read-only file system"];
|
|
14
|
+
/**
|
|
15
|
+
* Derive a config schema from a base object schema WITHOUT mutating it.
|
|
16
|
+
* schemastery's Schema#set() mutates the shared `dict` in place, so chaining
|
|
17
|
+
* .set() on LocalBashExecutor.Config lets whichever subclass module loads
|
|
18
|
+
* last overwrite the earlier one's fields — git-bash's probeTimeoutMs default
|
|
19
|
+
* silently became wsl-bash's 30000 and its sandbox union gained the invalid
|
|
20
|
+
* 'bwrap' value. Spreading the base dict into a fresh z.object makes every
|
|
21
|
+
* derivation independent of both the base and other subclasses.
|
|
22
|
+
*/
|
|
23
|
+
function deriveConfig(base, fields) {
|
|
24
|
+
const derived = z.object({ ...base.dict });
|
|
25
|
+
if (base.meta !== void 0) derived.meta = { ...base.meta };
|
|
26
|
+
for (const key of Object.keys(fields)) derived.set(key, fields[key]);
|
|
27
|
+
return derived;
|
|
28
|
+
}
|
|
29
|
+
//#region lib/types/resolve.js
|
|
30
|
+
/**
|
|
31
|
+
* WSL executable and distro resolution for the bash-wsl executor, dependency-free
|
|
32
|
+
* and parameterized (env/platform) so resolution is a pure function of its
|
|
33
|
+
* inputs on every platform — the twin of dsh-bash-git's resolveBashPath and
|
|
34
|
+
* dsh-pwsh-local's resolvePwshPath.
|
|
35
|
+
* @module @deepseek-ai/dsh-bash-wsl/resolve
|
|
36
|
+
*/
|
|
37
|
+
/**
|
|
38
|
+
* Well-known WSL executable locations: System32 (the shipped wsl.exe) plus
|
|
39
|
+
* PATH entries.
|
|
40
|
+
* @param env - the environment to probe; defaults to the process environment.
|
|
41
|
+
* @returns candidate `wsl.exe` paths in resolution order.
|
|
42
|
+
*/
|
|
43
|
+
function candidateWslPaths(env = process.env) {
|
|
44
|
+
const candidates = [join(env.SystemRoot ?? "C:\\Windows", "System32", "wsl.exe")];
|
|
45
|
+
for (const entry of (env.PATH ?? "").split(";")) {
|
|
46
|
+
const trimmed = entry.trim().replace(/^"|"$/g, "");
|
|
47
|
+
if (trimmed.length === 0) continue;
|
|
48
|
+
candidates.push(join(trimmed, "wsl.exe"));
|
|
49
|
+
}
|
|
50
|
+
return candidates;
|
|
51
|
+
}
|
|
52
|
+
function candidateExists(candidate) {
|
|
53
|
+
try {
|
|
54
|
+
const stat = lstatSync(candidate);
|
|
55
|
+
return stat.isFile() || stat.isSymbolicLink();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve the wsl.exe executable this executor spawns. Returns undefined on
|
|
62
|
+
* win32 when no candidate exists (the executor turns that into a loud
|
|
63
|
+
* route-time error naming the probes); non-win32 hosts resolve a bare
|
|
64
|
+
* `wsl` for PATH resolution, mirroring resolvePwshPath.
|
|
65
|
+
* @param configured - an explicit `wslPath` config value, trusted as-is.
|
|
66
|
+
* @param env - the environment to probe on Windows; defaults to the process environment.
|
|
67
|
+
* @param platform - the platform to resolve for; defaults to the process platform.
|
|
68
|
+
* @returns the first existing well-known location on win32, else `wsl`, else undefined.
|
|
69
|
+
*/
|
|
70
|
+
function resolveWslPath(configured, env = process.env, platform = process.platform) {
|
|
71
|
+
if (configured !== void 0 && configured.length > 0) return configured;
|
|
72
|
+
if (platform === "win32") {
|
|
73
|
+
for (const candidate of candidateWslPaths(env)) if (candidateExists(candidate)) return candidate;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
return "wsl";
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse `wsl -l -q` output into distro names. wsl.exe writes UTF-16LE with
|
|
80
|
+
* null-byte interleaving when stdout is redirected, so NUL bytes are stripped
|
|
81
|
+
* before splitting; surrounding quotes and blank lines are dropped.
|
|
82
|
+
* @param output - raw `-l -q` stdout.
|
|
83
|
+
* @returns distro names in output order.
|
|
84
|
+
*/
|
|
85
|
+
function parseWslDistroList(output) {
|
|
86
|
+
const distros = [];
|
|
87
|
+
for (const raw of output.replace(/\0/g, "").split(/\r?\n/)) {
|
|
88
|
+
const line = raw.trim().replace(/^"|"$/g, "");
|
|
89
|
+
if (line.length === 0) continue;
|
|
90
|
+
distros.push(line);
|
|
91
|
+
}
|
|
92
|
+
return distros;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Convert a Windows drive-letter path to the WSL /mnt/<drive>/... form used
|
|
96
|
+
* as the bwrap workspace root inside the distro. Anything without a
|
|
97
|
+
* drive-letter form fails loud: wsl.exe's own `--cd` translation covers
|
|
98
|
+
* workdirs, but the bwrap profile needs an explicit Linux-side root.
|
|
99
|
+
* @param winPath - the Windows path to convert.
|
|
100
|
+
* @returns the Linux-side path.
|
|
101
|
+
* @throws Error when the path has no drive-letter form.
|
|
102
|
+
*/
|
|
103
|
+
function toWslPath(winPath) {
|
|
104
|
+
const match = /^([A-Za-z]):[\\/](.+)$/.exec(winPath);
|
|
105
|
+
if (match === null) throw new Error(`unsupported Windows path: ${winPath}`);
|
|
106
|
+
const drive = match[1];
|
|
107
|
+
const rest = match[2];
|
|
108
|
+
/* v8 ignore next -- the regex guarantees both captures whenever it matches */
|
|
109
|
+
if (drive === void 0 || rest === void 0) throw new Error(`unsupported Windows path: ${winPath}`);
|
|
110
|
+
return `/mnt/${drive.toLowerCase()}/${rest.replaceAll("\\", "/")}`;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region lib/types/index.js
|
|
114
|
+
/**
|
|
115
|
+
* WSL Service Provider for the bash capability seam: a LocalBashExecutor
|
|
116
|
+
* whose argv is `[wslPath, --cd workdir?, -d distro?, --, bash, -c, payload]`.
|
|
117
|
+
* The command rides as a base64 payload through `echo <b64> | base64 -d | bash`
|
|
118
|
+
* so wsl.exe's Windows→Linux argument serialization can never damage quotes.
|
|
119
|
+
* Sandbox: `auto` probes bubblewrap inside the distro (once, lazily, at the
|
|
120
|
+
* first sandboxMode read or the first command) and runs unconfined without
|
|
121
|
+
* sandbox facts when absent; `bwrap` fails loud at first use when the probe
|
|
122
|
+
* fails; `none` never probes or confines.
|
|
123
|
+
* @module @deepseek-ai/dsh-bash-wsl
|
|
124
|
+
*/
|
|
125
|
+
/**
|
|
126
|
+
* WSL executor over the local bash mechanics: argv is the resolved wsl.exe
|
|
127
|
+
* followed by `--cd`/`-d`/`--` `bash -c` with the base64 payload, and the
|
|
128
|
+
* sandbox stance (`auto`/`bwrap`/`none`) is applied lazily per the module
|
|
129
|
+
* doc's probe contract.
|
|
130
|
+
*/
|
|
131
|
+
var WslBashExecutor = class extends LocalBashExecutor {
|
|
132
|
+
static Config = deriveConfig(LocalBashExecutor.Config, {
|
|
133
|
+
wslPath: z.string(),
|
|
134
|
+
wslDistro: z.string(),
|
|
135
|
+
sandbox: z.union([
|
|
136
|
+
z.const("auto"),
|
|
137
|
+
z.const("none"),
|
|
138
|
+
z.const("bwrap")
|
|
139
|
+
]).default("auto"),
|
|
140
|
+
probeTimeoutMs: z.natural().default(3e4),
|
|
141
|
+
requireSandbox: z.boolean().default(false)
|
|
142
|
+
});
|
|
143
|
+
/** Test hook mirroring the sandbox-local internals pattern. */
|
|
144
|
+
internals = {};
|
|
145
|
+
sandboxStance;
|
|
146
|
+
probeTimeoutMs;
|
|
147
|
+
bwrapVerdict;
|
|
148
|
+
distroProbed = false;
|
|
149
|
+
distroVerdict;
|
|
150
|
+
constructor(ctx, config) {
|
|
151
|
+
super(ctx, config);
|
|
152
|
+
const entry = config;
|
|
153
|
+
this.sandboxStance = entry.sandbox;
|
|
154
|
+
this.probeTimeoutMs = entry.probeTimeoutMs;
|
|
155
|
+
this.requireSandbox = entry.requireSandbox;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The capability fact: the policy default mode while bwrap is usable. With
|
|
159
|
+
* `requireSandbox`, the mode is declared even when the probe fails, so the
|
|
160
|
+
* tool layer advertises escalation (`sandbox_permissions`) and the executor
|
|
161
|
+
* can refuse unconfined runs outside danger-full-access.
|
|
162
|
+
*/
|
|
163
|
+
get sandboxMode() {
|
|
164
|
+
if (this.sandboxStance === "none") return void 0;
|
|
165
|
+
if (this.requireSandbox) return this.ctx.sandboxPolicy.defaultMode;
|
|
166
|
+
return this.requireBwrapUsable() ? this.ctx.sandboxPolicy.defaultMode : void 0;
|
|
167
|
+
}
|
|
168
|
+
/** Auto: false means run unconfined. Explicit bwrap: a failed probe throws loud. */
|
|
169
|
+
requireBwrapUsable() {
|
|
170
|
+
if (this.probeBwrapOnce()) return true;
|
|
171
|
+
if (this.sandboxStance === "bwrap") throw new Error("bash-wsl: bwrap was not found in the WSL distro (explicit sandbox \"bwrap\" is configured); install bubblewrap or set sandbox: auto/none");
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
tryWslPath() {
|
|
175
|
+
return (this.internals.resolveWslPath ?? ((configured) => resolveWslPath(configured)))(this.config.wslPath);
|
|
176
|
+
}
|
|
177
|
+
wslPath() {
|
|
178
|
+
const path = this.tryWslPath();
|
|
179
|
+
if (path !== void 0) return path;
|
|
180
|
+
throw new Error(`bash-wsl: WSL was not found (probed ${candidateWslPaths().join(", ")}). Enable Windows Subsystem for Linux or set wslBash.wslPath.`);
|
|
181
|
+
}
|
|
182
|
+
probeDistroOnce() {
|
|
183
|
+
if (!this.distroProbed) {
|
|
184
|
+
this.distroProbed = true;
|
|
185
|
+
const probe = this.internals.probeDistro;
|
|
186
|
+
if (probe !== void 0) return this.distroVerdict = probe();
|
|
187
|
+
const wsl = this.tryWslPath();
|
|
188
|
+
if (wsl === void 0) {
|
|
189
|
+
this.distroVerdict = void 0;
|
|
190
|
+
/* v8 ignore next 2 -- every executor path resolves wslPath before this probe, so the guard is defensive only. */
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const run = spawnSync(wsl, ["-l", "-q"], {
|
|
194
|
+
timeout: this.probeTimeoutMs,
|
|
195
|
+
encoding: "utf8"
|
|
196
|
+
});
|
|
197
|
+
this.distroVerdict = run.status === 0 ? parseWslDistroList(run.stdout)[0] : void 0;
|
|
198
|
+
}
|
|
199
|
+
return this.distroVerdict;
|
|
200
|
+
}
|
|
201
|
+
distro() {
|
|
202
|
+
return this.config.wslDistro ?? this.probeDistroOnce();
|
|
203
|
+
}
|
|
204
|
+
probeBwrapOnce() {
|
|
205
|
+
if (this.bwrapVerdict !== void 0) return this.bwrapVerdict;
|
|
206
|
+
const probe = this.internals.probeBwrap;
|
|
207
|
+
if (probe !== void 0) return this.bwrapVerdict = probe();
|
|
208
|
+
const wsl = this.tryWslPath();
|
|
209
|
+
if (wsl === void 0) return this.bwrapVerdict = false;
|
|
210
|
+
const distro = this.distro();
|
|
211
|
+
const run = spawnSync(wsl, [
|
|
212
|
+
...distro !== void 0 ? ["-d", distro] : [],
|
|
213
|
+
"-e",
|
|
214
|
+
"bash",
|
|
215
|
+
"-c",
|
|
216
|
+
"command -v bwrap"
|
|
217
|
+
], {
|
|
218
|
+
timeout: this.probeTimeoutMs,
|
|
219
|
+
stdio: "ignore"
|
|
220
|
+
});
|
|
221
|
+
return this.bwrapVerdict = run.status === 0;
|
|
222
|
+
}
|
|
223
|
+
payload(command) {
|
|
224
|
+
return `echo ${Buffer.from(command, "utf8").toString("base64")} | base64 -d | bash`;
|
|
225
|
+
}
|
|
226
|
+
/** The workdir `resolve()` fills when a request omits one. */
|
|
227
|
+
defaultWorkdir() {
|
|
228
|
+
return this.config.cwd ?? process.cwd();
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The plain WSL argv for one spec (no sandbox). `--cd` is passed only when
|
|
232
|
+
* the workdir differs from the default, so the subprocess cwd and the WSL
|
|
233
|
+
* start directory agree without a redundant wsl.exe translation; a Linux
|
|
234
|
+
* workdir passes through verbatim.
|
|
235
|
+
*/
|
|
236
|
+
argv(spec) {
|
|
237
|
+
const head = [this.wslPath()];
|
|
238
|
+
if (spec.workdir !== this.defaultWorkdir()) head.push("--cd", spec.workdir);
|
|
239
|
+
const distro = this.distro();
|
|
240
|
+
if (distro !== void 0) head.push("-d", distro);
|
|
241
|
+
head.push("--", "bash", "-c", this.payload(spec.command));
|
|
242
|
+
return head;
|
|
243
|
+
}
|
|
244
|
+
confinedPolicy(spec) {
|
|
245
|
+
return spec.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
246
|
+
}
|
|
247
|
+
/** The bwrap-wrapped argv; the workspace root is translated to its /mnt/<drive> form. */
|
|
248
|
+
bwrapArgv(spec) {
|
|
249
|
+
const policy = this.confinedPolicy(spec);
|
|
250
|
+
const linuxRoot = toWslPath(policy.workspaceRoot);
|
|
251
|
+
const head = [this.wslPath()];
|
|
252
|
+
if (spec.workdir !== this.defaultWorkdir()) head.push("--cd", spec.workdir);
|
|
253
|
+
const distro = this.distro();
|
|
254
|
+
if (distro !== void 0) head.push("-d", distro);
|
|
255
|
+
const mode = policy.mode;
|
|
256
|
+
head.push("--", "bwrap", ...bwrapProfileArgs({
|
|
257
|
+
mode,
|
|
258
|
+
workspaceRoot: linuxRoot
|
|
259
|
+
}), "bash", "-c", this.payload(spec.command));
|
|
260
|
+
return head;
|
|
261
|
+
}
|
|
262
|
+
async run(spec) {
|
|
263
|
+
const policy = this.confinedPolicy(spec);
|
|
264
|
+
if (policy.mode === "danger-full-access") return {
|
|
265
|
+
...await this.runArgv(spec, this.argv(spec)),
|
|
266
|
+
sandbox: {
|
|
267
|
+
mode: "danger-full-access",
|
|
268
|
+
denied: false
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
if (this.sandboxStance === "none") return this.runArgv(spec, this.argv(spec));
|
|
272
|
+
if (!this.requireBwrapUsable()) {
|
|
273
|
+
if (this.requireSandbox) throw new Error("bash-wsl: sandbox unavailable — bwrap was not found in the WSL distro; refusing to run unconfined under " + policy.mode + " mode. Install bubblewrap or retry the command with sandbox_permissions: \"danger-full-access\" and a justification.");
|
|
274
|
+
return this.runArgv(spec, this.argv(spec));
|
|
275
|
+
}
|
|
276
|
+
const result = await this.runArgv(spec, this.bwrapArgv(spec));
|
|
277
|
+
if (classifyRunnerFailure(result.exitCode, result.stderr.text, BWRAP_RUNNER_FAILURE_RULES) !== void 0) return {
|
|
278
|
+
...result,
|
|
279
|
+
sandbox: {
|
|
280
|
+
mode: policy.mode,
|
|
281
|
+
denied: false,
|
|
282
|
+
enforcement: "full",
|
|
283
|
+
runnerFailed: true
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
...result,
|
|
288
|
+
sandbox: {
|
|
289
|
+
mode: policy.mode,
|
|
290
|
+
denied: classifyDenial(result, BWRAP_DENIAL_SIGNATURES),
|
|
291
|
+
enforcement: "full"
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** Per-process confinement facts retained until settlement (bash-sandbox pattern). */
|
|
296
|
+
processFacts = /* @__PURE__ */ new Map();
|
|
297
|
+
start(spec) {
|
|
298
|
+
const policy = this.confinedPolicy(spec);
|
|
299
|
+
if (policy.mode === "danger-full-access") return this.startArgv(spec, this.argv(spec));
|
|
300
|
+
if (this.sandboxStance === "none") return this.startArgv(spec, this.argv(spec));
|
|
301
|
+
if (!this.requireBwrapUsable()) {
|
|
302
|
+
if (this.requireSandbox) throw new Error("bash-wsl: sandbox unavailable — bwrap was not found in the WSL distro; refusing to run unconfined under " + policy.mode + " mode. Install bubblewrap or retry the command with sandbox_permissions: \"danger-full-access\" and a justification.");
|
|
303
|
+
return this.startArgv(spec, this.argv(spec));
|
|
304
|
+
}
|
|
305
|
+
const proc = this.startArgv(spec, this.bwrapArgv(spec));
|
|
306
|
+
this.processFacts.set(proc, { mode: policy.mode });
|
|
307
|
+
return proc;
|
|
308
|
+
}
|
|
309
|
+
/** Stamp per-process sandbox facts before `done` settles (bash-sandbox semantics). */
|
|
310
|
+
onProcessDone(proc, stderr, spawnFailed, spawnError) {
|
|
311
|
+
const facts = this.processFacts.get(proc);
|
|
312
|
+
if (facts !== void 0) {
|
|
313
|
+
this.processFacts.delete(proc);
|
|
314
|
+
proc.sandbox = classifyRunnerFailure(proc.exitCode, stderr, BWRAP_RUNNER_FAILURE_RULES) !== void 0 ? {
|
|
315
|
+
mode: facts.mode,
|
|
316
|
+
denied: false,
|
|
317
|
+
enforcement: "full",
|
|
318
|
+
runnerFailed: true
|
|
319
|
+
} : {
|
|
320
|
+
mode: facts.mode,
|
|
321
|
+
denied: matchesSignature(proc.exitCode, stderr, BWRAP_DENIAL_SIGNATURES),
|
|
322
|
+
enforcement: "full"
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
super.onProcessDone(proc, stderr, spawnFailed, spawnError);
|
|
326
|
+
}
|
|
327
|
+
resolve(request) {
|
|
328
|
+
return {
|
|
329
|
+
...super.resolve(request),
|
|
330
|
+
sandboxPolicy: request.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve()
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
//#endregion
|
|
335
|
+
export { WslBashExecutor, WslBashExecutor as default };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-bash-wsl`.
|
|
4
|
+
* @module @deepseek-ai/dsh-bash-wsl/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-bash-wsl";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "bash-wsl-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or mutable data relation
|
|
13
|
+
* beyond contracts enforced at its owning seam.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// dsh-win-multi-bash — self-contained Windows multi-bash plugin.
|
|
2
|
+
//
|
|
3
|
+
// This package carries the whole feature implementation, bundled from the
|
|
4
|
+
// DeepSeek Harness project's shell packages (see THIRD_PARTY_NOTICES),
|
|
5
|
+
// compiled to plain ESM JS:
|
|
6
|
+
//
|
|
7
|
+
// lib/shell-select/ — ShellSelectExecutor (the ctx.shell selector)
|
|
8
|
+
// lib/bash-git/ — GitBashExecutor (MSYS / Git for Windows)
|
|
9
|
+
// lib/bash-wsl/ — WslBashExecutor (WSL distros, base64 payloads)
|
|
10
|
+
// lib/tool-bash/ — defineShellTool factory + git_bash / wsl_bash instances
|
|
11
|
+
// lib/vendor/ — helpers.js (bash-sandbox classification) and
|
|
12
|
+
// bwrap-profiles.js (bwrap rules/args) — the two
|
|
13
|
+
// helper modules not exported by the base runtime.
|
|
14
|
+
//
|
|
15
|
+
// Only published @deepseek-ai base packages are imported (dsh-shell,
|
|
16
|
+
// dsh-sandbox, dsh-pwsh-sandbox, dsh-tools, dsh-llm, ...), so the plugin runs
|
|
17
|
+
// on any standard deployment — no additional runtime packages required.
|
|
18
|
+
//
|
|
19
|
+
// The composition wiring lives in cordis.patch.yml.
|
|
20
|
+
export {}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { HarnessError } from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { SHELL_SETTINGS_NAMESPACE, ShellExecutor } from "@deepseek-ai/dsh-shell";
|
|
4
|
+
import { installSettingsSection } from "@deepseek-ai/dsh-settings";
|
|
5
|
+
import { GitBashExecutor } from "../bash-git/index.js";
|
|
6
|
+
import { WslBashExecutor } from "../bash-wsl/index.js";
|
|
7
|
+
import { SandboxPwshExecutor } from "@deepseek-ai/dsh-pwsh-sandbox";
|
|
8
|
+
import { assertServiceableBashConfig } from "@deepseek-ai/dsh-bash-local";
|
|
9
|
+
import { assertServiceablePwshConfig } from "@deepseek-ai/dsh-pwsh-local";
|
|
10
|
+
//#region lib/types/index.js
|
|
11
|
+
/**
|
|
12
|
+
* Selector Service Provider for the bash capability seam: occupies the single
|
|
13
|
+
* `ctx.shell` seat and routes each request to one of several backends held as
|
|
14
|
+
* plain instances. The tool name selects the backend (`request.shell`); a
|
|
15
|
+
* request without one goes to the configured `default`. Backends are
|
|
16
|
+
* constructed on their own child fibers with isolated `shell`/`settings`
|
|
17
|
+
* scopes so their inherited Service registration and settings wiring cannot
|
|
18
|
+
* collide with this executor's own seat. win32 compositions only; POSIX
|
|
19
|
+
* compositions keep a single backend directly on the seat.
|
|
20
|
+
* @module @deepseek-ai/dsh-shell-select
|
|
21
|
+
*/
|
|
22
|
+
/** Error code for routing to an unknown or disabled backend. */
|
|
23
|
+
const SHELL_BACKEND_UNAVAILABLE = "SHELL_BACKEND_UNAVAILABLE";
|
|
24
|
+
const DEFAULT_FACTORIES = {
|
|
25
|
+
"git-bash": (ctx, config) => new GitBashExecutor(ctx, GitBashExecutor.Config(config)),
|
|
26
|
+
"wsl-bash": (ctx, config) => new WslBashExecutor(ctx, WslBashExecutor.Config(config)),
|
|
27
|
+
pwsh: (ctx, config) => new SandboxPwshExecutor(ctx, SandboxPwshExecutor.Config(config))
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Selector executor over the bash capability seam. Registers as `ctx.shell`
|
|
31
|
+
* (the single seat), routes every request to one enabled backend by name, and
|
|
32
|
+
* stamps the chosen name on the resolved spec as `shellBackend`. Enabled
|
|
33
|
+
* names without a factory fail loud at the first rebuild; routing to an
|
|
34
|
+
* unbuilt name throws `SHELL_BACKEND_UNAVAILABLE`.
|
|
35
|
+
*/
|
|
36
|
+
var ShellSelectExecutor = class ShellSelectExecutor extends ShellExecutor {
|
|
37
|
+
static inject = [
|
|
38
|
+
"subprocess",
|
|
39
|
+
"sandbox",
|
|
40
|
+
"sandboxPolicy"
|
|
41
|
+
];
|
|
42
|
+
static Config = z.object({
|
|
43
|
+
backends: z.array(z.string()).default([
|
|
44
|
+
"git-bash",
|
|
45
|
+
"wsl-bash",
|
|
46
|
+
"pwsh"
|
|
47
|
+
]),
|
|
48
|
+
default: z.string().default("pwsh"),
|
|
49
|
+
gitBash: GitBashExecutor.Config.default({}),
|
|
50
|
+
wslBash: WslBashExecutor.Config.default({}),
|
|
51
|
+
pwsh: SandboxPwshExecutor.Config.default({})
|
|
52
|
+
});
|
|
53
|
+
/** Test hook: replace backend construction wholesale (mirrors the sandbox-local internals pattern). */
|
|
54
|
+
internals = {};
|
|
55
|
+
/** The currently authoritative config: the settings section, or the composition entry. */
|
|
56
|
+
source;
|
|
57
|
+
backends = /* @__PURE__ */ new Map();
|
|
58
|
+
fibers = [];
|
|
59
|
+
constructor(ctx, config) {
|
|
60
|
+
super(ctx);
|
|
61
|
+
const entry = config;
|
|
62
|
+
assertServiceableBashConfig(entry.gitBash);
|
|
63
|
+
assertServiceableBashConfig(entry.wslBash);
|
|
64
|
+
assertServiceablePwshConfig(entry.pwsh);
|
|
65
|
+
this.source = () => entry;
|
|
66
|
+
installSettingsSection(ctx, SHELL_SETTINGS_NAMESPACE, ShellSelectExecutor.Config, entry, {
|
|
67
|
+
validate: (value) => {
|
|
68
|
+
const v = value;
|
|
69
|
+
assertServiceableBashConfig(v.gitBash);
|
|
70
|
+
assertServiceableBashConfig(v.wslBash);
|
|
71
|
+
assertServiceablePwshConfig(v.pwsh);
|
|
72
|
+
},
|
|
73
|
+
setSource: (current) => {
|
|
74
|
+
this.source = current;
|
|
75
|
+
},
|
|
76
|
+
onChange: () => {
|
|
77
|
+
this.rebuildBackends(this.config);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Validated config (schemastery applied the defaults before construction). */
|
|
82
|
+
get config() {
|
|
83
|
+
return this.source();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The first sandbox mode any enabled backend declares (tool-layer
|
|
87
|
+
* advertisement). One backend's probe/verdict failure (e.g. explicit
|
|
88
|
+
* `sandbox: bwrap` without bubblewrap installed) must not brick the whole
|
|
89
|
+
* selector: that backend advertises no mode here and its error resurfaces
|
|
90
|
+
* loudly at its first routed command — the documented lazy contract.
|
|
91
|
+
*/
|
|
92
|
+
get sandboxMode() {
|
|
93
|
+
this.ensureBackends();
|
|
94
|
+
for (const name of this.config.backends) {
|
|
95
|
+
let mode;
|
|
96
|
+
try {
|
|
97
|
+
mode = this.backends.get(name)?.sandboxMode;
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (mode !== void 0) return mode;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
resolve(request) {
|
|
105
|
+
this.ensureBackends();
|
|
106
|
+
const name = request.shell ?? this.config.default;
|
|
107
|
+
return {
|
|
108
|
+
...this.requireBackend(name).resolve(request),
|
|
109
|
+
shellBackend: name
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
run(spec) {
|
|
113
|
+
this.ensureBackends();
|
|
114
|
+
return this.requireBackend(spec.shellBackend ?? this.config.default).run(spec);
|
|
115
|
+
}
|
|
116
|
+
start(spec) {
|
|
117
|
+
this.ensureBackends();
|
|
118
|
+
return this.requireBackend(spec.shellBackend ?? this.config.default).start(spec);
|
|
119
|
+
}
|
|
120
|
+
requireBackend(name) {
|
|
121
|
+
const backend = this.backends.get(name);
|
|
122
|
+
if (backend === void 0) throw new HarnessError(`shell-select: backend "${name}" is not enabled (enabled: ${[...this.backends.keys()].join(", ")})`, SHELL_BACKEND_UNAVAILABLE);
|
|
123
|
+
return backend;
|
|
124
|
+
}
|
|
125
|
+
/** The first rebuild runs at first use, after the internals hook is installed. */
|
|
126
|
+
ensureBackends() {
|
|
127
|
+
if (this.backends.size > 0) return;
|
|
128
|
+
this.rebuildBackends(this.config);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Construct one backend per enabled name on its own child fiber with
|
|
132
|
+
* isolated shell/settings scopes (see the class doc), or throw for an
|
|
133
|
+
* unknown name. Old fibers are disposed first so rebuilds never leak.
|
|
134
|
+
*/
|
|
135
|
+
rebuildBackends(entry) {
|
|
136
|
+
for (const fiber of this.fibers.splice(0)) fiber.dispose();
|
|
137
|
+
this.backends.clear();
|
|
138
|
+
for (const name of entry.backends) {
|
|
139
|
+
const factory = this.internals.factories?.[name] ?? DEFAULT_FACTORIES[name];
|
|
140
|
+
if (factory === void 0) throw new Error(`shell-select: unknown backend "${name}" (enabled: ${entry.backends.join(", ")})`);
|
|
141
|
+
const fiber = this.ctx.plugin({
|
|
142
|
+
inject: [
|
|
143
|
+
"subprocess",
|
|
144
|
+
"sandbox",
|
|
145
|
+
"sandboxPolicy"
|
|
146
|
+
],
|
|
147
|
+
apply: () => {}
|
|
148
|
+
});
|
|
149
|
+
this.fibers.push(fiber);
|
|
150
|
+
const backendCtx = fiber.ctx.isolate("shell", Symbol(name)).isolate("settings", Symbol(name));
|
|
151
|
+
this.backends.set(name, factory(backendCtx, backendConfig(name, entry)));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* The config partition for one backend name. Names without a dedicated
|
|
157
|
+
* partition (test-hook factories) receive an empty config; the owning factory
|
|
158
|
+
* decides what it needs.
|
|
159
|
+
*/
|
|
160
|
+
function backendConfig(name, entry) {
|
|
161
|
+
switch (name) {
|
|
162
|
+
case "git-bash": return entry.gitBash;
|
|
163
|
+
case "wsl-bash": return entry.wslBash;
|
|
164
|
+
case "pwsh": return entry.pwsh;
|
|
165
|
+
default: return {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
export { SHELL_BACKEND_UNAVAILABLE, ShellSelectExecutor, ShellSelectExecutor as default };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/**
|
|
3
|
+
* Package-owned invariant companion for `@deepseek-ai/dsh-shell-select`.
|
|
4
|
+
* @module @deepseek-ai/dsh-shell-select/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = "@deepseek-ai/dsh-shell-select";
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = "shell-select-invariant";
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ["invariants"];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package exposes no independent event sequence or
|
|
13
|
+
* mutable data relation beyond contracts enforced at its owning seams.
|
|
14
|
+
*/
|
|
15
|
+
const install = () => {};
|
|
16
|
+
/**
|
|
17
|
+
* Register this package's invariant companion.
|
|
18
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
19
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
20
|
+
*/
|
|
21
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
22
|
+
//#endregion
|
|
23
|
+
export { apply, inject, name };
|