dsh-win-multi-bash 0.1.2 → 0.3.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/README.i18n.yaml +2 -2
- package/README.md +33 -21
- package/README.zh.md +32 -20
- package/THIRD_PARTY_NOTICES +2 -3
- package/cordis.patch.yml +48 -54
- package/lib/bash-git/index.js +115 -76
- package/lib/bash-wsl/index.js +71 -37
- package/lib/index.js +9 -6
- package/lib/tool-bash/types/backend.js +41 -0
- package/lib/tool-bash/types/background.js +66 -0
- package/lib/tool-bash/types/factory.js +55 -19
- package/lib/tool-bash/types/git-bash.js +14 -6
- package/lib/tool-bash/types/render.js +0 -29
- package/lib/tool-bash/types/wsl-bash.js +14 -6
- package/package.json +7 -20
- package/lib/bash-git/invariant.js +0 -23
- package/lib/bash-wsl/invariant.js +0 -23
- package/lib/shell-select/index.js +0 -169
- package/lib/shell-select/invariant.js +0 -23
- package/lib/tool-bash/index.js +0 -527
- package/lib/tool-bash/invariant.js +0 -23
- package/lib/tool-bash/types/index.js +0 -16
- package/lib/tool-bash/types/invariant.js +0 -22
package/lib/bash-git/index.js
CHANGED
|
@@ -254,7 +254,10 @@ var GitBashExecutor = class extends LocalBashExecutor {
|
|
|
254
254
|
internals = {};
|
|
255
255
|
sandboxStance;
|
|
256
256
|
probeTimeoutMs;
|
|
257
|
-
|
|
257
|
+
/** Memoized probe promise; the provider's `confine` is async as of 0.1.7. */
|
|
258
|
+
confinedProbe;
|
|
259
|
+
/** The verdict `resolveSandboxMode` settled on, read back by the sync getter. */
|
|
260
|
+
sandboxModeVerdict;
|
|
258
261
|
constructor(ctx, config) {
|
|
259
262
|
super(ctx, config);
|
|
260
263
|
const entry = config;
|
|
@@ -264,14 +267,24 @@ var GitBashExecutor = class extends LocalBashExecutor {
|
|
|
264
267
|
}
|
|
265
268
|
/**
|
|
266
269
|
* The capability fact: the policy default mode while confinement is usable.
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
+
* The probe is async (the provider's `confine` returns a promise), so the
|
|
271
|
+
* tool layer awaits {@link resolveSandboxMode} before reading this; until
|
|
272
|
+
* then the mode is undeclared.
|
|
270
273
|
*/
|
|
271
274
|
get sandboxMode() {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
return this.sandboxModeVerdict;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Settle this executor's advertised sandbox mode. With `requireSandbox`,
|
|
279
|
+
* the mode is declared even when the probe fails, so the tool layer
|
|
280
|
+
* advertises escalation (`sandbox_permissions`) and the executor can refuse
|
|
281
|
+
* unconfined runs outside danger-full-access.
|
|
282
|
+
* @returns the mode to advertise, or `undefined` for an unconfining deployment.
|
|
283
|
+
*/
|
|
284
|
+
async resolveSandboxMode() {
|
|
285
|
+
if (this.sandboxStance === "none") return this.sandboxModeVerdict = void 0;
|
|
286
|
+
if (this.requireSandbox) return this.sandboxModeVerdict = this.ctx.sandboxPolicy.defaultMode;
|
|
287
|
+
return this.sandboxModeVerdict = await this.probeConfinedOnce() ? this.ctx.sandboxPolicy.defaultMode : void 0;
|
|
275
288
|
}
|
|
276
289
|
/** The exact argv every (un)confined command runs through. */
|
|
277
290
|
gitArgv(spec) {
|
|
@@ -291,25 +304,29 @@ var GitBashExecutor = class extends LocalBashExecutor {
|
|
|
291
304
|
bashPath() {
|
|
292
305
|
const path = this.tryBashPath();
|
|
293
306
|
if (path !== void 0) return path;
|
|
294
|
-
throw new Error(`bash-git: Git Bash was not found (probed ${candidateBashPaths().join(", ")} and the GitForWindows registry). Install Git for Windows or set gitBash.bashPath
|
|
307
|
+
throw new Error(`bash-git: Git Bash was not found (probed ${candidateBashPaths().join(", ")} and the GitForWindows registry). Install Git for Windows or set gitBash.bashPath on the git_bash tool row.`);
|
|
295
308
|
}
|
|
296
309
|
/**
|
|
297
310
|
* Probe whether the windows-acl runner can launch bash.exe, once per
|
|
298
|
-
* executor lifetime (lazy: the first
|
|
299
|
-
* command).
|
|
300
|
-
* sandbox-local's defaultProbeWindowsAcl shape); a
|
|
301
|
-
*
|
|
311
|
+
* executor lifetime (lazy: the first {@link resolveSandboxMode} await or the
|
|
312
|
+
* first routed command). Wraps a trivial `exit 0` through the provider and
|
|
313
|
+
* runs it (mirroring sandbox-local's defaultProbeWindowsAcl shape); a wrap
|
|
314
|
+
* rejection, a spawn rejection, or a non-zero exit all mean no confinement.
|
|
315
|
+
* @returns the memoized verdict.
|
|
302
316
|
*/
|
|
303
317
|
probeConfinedOnce() {
|
|
304
|
-
|
|
318
|
+
this.confinedProbe ??= this.probeConfined();
|
|
319
|
+
return this.confinedProbe;
|
|
320
|
+
}
|
|
321
|
+
async probeConfined() {
|
|
305
322
|
const probe = this.internals.probeConfined;
|
|
306
|
-
if (probe !== void 0) return
|
|
323
|
+
if (probe !== void 0) return probe();
|
|
307
324
|
const bashPath = this.tryBashPath();
|
|
308
|
-
if (bashPath === void 0) return
|
|
325
|
+
if (bashPath === void 0) return false;
|
|
309
326
|
const root = this.ctx.sandboxPolicy.resolve().workspaceRoot;
|
|
310
327
|
let confined;
|
|
311
328
|
try {
|
|
312
|
-
confined = this.ctx.sandbox.confine([
|
|
329
|
+
confined = await this.ctx.sandbox.confine([
|
|
313
330
|
bashPath,
|
|
314
331
|
"-c",
|
|
315
332
|
"exit 0"
|
|
@@ -318,93 +335,115 @@ var GitBashExecutor = class extends LocalBashExecutor {
|
|
|
318
335
|
workspaceRoot: root
|
|
319
336
|
});
|
|
320
337
|
} catch {
|
|
321
|
-
return
|
|
338
|
+
return false;
|
|
322
339
|
}
|
|
323
340
|
const runner = confined.argv[0];
|
|
324
|
-
if (runner === void 0) return
|
|
341
|
+
if (runner === void 0) return false;
|
|
325
342
|
const probeRun = spawnSync(runner, confined.argv.slice(1), {
|
|
326
343
|
timeout: this.probeTimeoutMs,
|
|
327
344
|
cwd: root,
|
|
328
345
|
stdio: "ignore"
|
|
329
346
|
});
|
|
330
|
-
return
|
|
347
|
+
return probeRun.status === 0;
|
|
331
348
|
}
|
|
332
349
|
confinedPolicy(spec) {
|
|
333
350
|
return spec.sandboxPolicy ?? this.ctx.sandboxPolicy.resolve();
|
|
334
351
|
}
|
|
335
|
-
|
|
352
|
+
/** Per-process confinement facts retained until settlement (bash-sandbox pattern). */
|
|
353
|
+
processFacts = /* @__PURE__ */ new Map();
|
|
354
|
+
/**
|
|
355
|
+
* Execute one resolved spec through the confined (or unconfined) Git Bash
|
|
356
|
+
* argv. Foreground is a property of awaiting `result()`, not of the call —
|
|
357
|
+
* one handle serves both call styles, as the 0.1.7 seam defines it.
|
|
358
|
+
* Confinement is probe-gated; runner evidence at execution time falls back
|
|
359
|
+
* to an unconfined run (the documented lazy contract) so a windows-acl
|
|
360
|
+
* runner that cannot launch MSYS bash never bricks the backend.
|
|
361
|
+
* @param spec - the resolved execution spec from {@link resolve}.
|
|
362
|
+
* @returns the live handle; sandbox facts land on both `proc.sandbox` (at
|
|
363
|
+
* settlement) and the decorated `result()`.
|
|
364
|
+
*/
|
|
365
|
+
async execute(spec) {
|
|
336
366
|
const policy = this.confinedPolicy(spec);
|
|
337
|
-
if (policy.mode === "danger-full-access") return {
|
|
338
|
-
...
|
|
367
|
+
if (policy.mode === "danger-full-access") return GitBashExecutor.decorateResult(await this.executeArgv(spec, this.gitArgv(spec)), (result) => ({
|
|
368
|
+
...result,
|
|
339
369
|
sandbox: {
|
|
340
370
|
mode: "danger-full-access",
|
|
341
371
|
denied: false
|
|
342
372
|
}
|
|
343
|
-
};
|
|
344
|
-
if (this.sandboxStance === "none") return this.
|
|
345
|
-
if (!this.probeConfinedOnce()) {
|
|
373
|
+
}));
|
|
374
|
+
if (this.sandboxStance === "none") return this.executeArgv(spec, this.gitArgv(spec));
|
|
375
|
+
if (!await this.probeConfinedOnce()) {
|
|
346
376
|
if (this.requireSandbox) throw new Error("bash-git: sandbox unavailable — the windows-acl runner cannot confine MSYS bash (probe failed); refusing to run unconfined under " + policy.mode + " mode. Fix the sandbox or retry the command with sandbox_permissions: \"danger-full-access\" and a justification.");
|
|
347
|
-
return this.
|
|
377
|
+
return this.executeArgv(spec, this.gitArgv(spec));
|
|
348
378
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
if (classifyRunnerFailure(result.exitCode, result.stderr.text, confined.runnerFailureRules) !== void 0) return this.runArgv(spec, this.gitArgv(spec));
|
|
362
|
-
return {
|
|
363
|
-
...result,
|
|
364
|
-
sandbox: {
|
|
379
|
+
let confined;
|
|
380
|
+
const proc = await this.executeArgv(spec, async (signal) => {
|
|
381
|
+
confined = await this.ctx.sandbox.confine(this.gitArgv(spec), {
|
|
382
|
+
...policy,
|
|
383
|
+
mode: policy.mode
|
|
384
|
+
}, signal);
|
|
385
|
+
return confined.argv;
|
|
386
|
+
}, (started) => {
|
|
387
|
+
const facts = confined;
|
|
388
|
+
if (facts === void 0) return;
|
|
389
|
+
this.processFacts.set(started, {
|
|
365
390
|
mode: policy.mode,
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
processFacts = /* @__PURE__ */ new Map();
|
|
373
|
-
start(spec) {
|
|
374
|
-
const policy = this.confinedPolicy(spec);
|
|
375
|
-
if (policy.mode === "danger-full-access") return this.startArgv(spec, this.gitArgv(spec));
|
|
376
|
-
if (this.sandboxStance === "none") return this.startArgv(spec, this.gitArgv(spec));
|
|
377
|
-
if (!this.probeConfinedOnce()) {
|
|
378
|
-
if (this.requireSandbox) throw new Error("bash-git: sandbox unavailable — the windows-acl runner cannot confine MSYS bash (probe failed); refusing to run unconfined under " + policy.mode + " mode. Fix the sandbox or retry the command with sandbox_permissions: \"danger-full-access\" and a justification.");
|
|
379
|
-
return this.startArgv(spec, this.gitArgv(spec));
|
|
380
|
-
}
|
|
381
|
-
const confined = this.ctx.sandbox.confine(this.gitArgv(spec), {
|
|
382
|
-
...policy,
|
|
383
|
-
mode: policy.mode
|
|
391
|
+
enforcement: facts.enforcement,
|
|
392
|
+
denialSignatures: facts.denialSignatures,
|
|
393
|
+
runnerFailureRules: facts.runnerFailureRules,
|
|
394
|
+
runnerProgram: facts.argv[0],
|
|
395
|
+
workdir: spec.workdir
|
|
396
|
+
});
|
|
384
397
|
});
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
398
|
+
/** Re-run the command unconfined after runner evidence proved confinement unavailable. */
|
|
399
|
+
const unconfinedFallback = async () => (await this.executeArgv(spec, this.gitArgv(spec))).result();
|
|
400
|
+
return GitBashExecutor.decorateResult(proc, async (result) => {
|
|
401
|
+
if (confined === void 0) return {
|
|
402
|
+
...result,
|
|
403
|
+
sandbox: {
|
|
404
|
+
mode: policy.mode,
|
|
405
|
+
denied: false
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
if (classifyRunnerFailure(result.exitCode, result.stderr.text, confined.runnerFailureRules) !== void 0) return unconfinedFallback();
|
|
409
|
+
return {
|
|
410
|
+
...result,
|
|
411
|
+
sandbox: {
|
|
412
|
+
mode: policy.mode,
|
|
413
|
+
denied: classifyDenial(result, confined.denialSignatures),
|
|
414
|
+
enforcement: confined.enforcement
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}, async (error) => {
|
|
418
|
+
if (spec.signal?.aborted === true) spec.signal.throwIfAborted();
|
|
419
|
+
if (confined !== void 0 && isRunnerSpawnFailure(error, confined.argv[0], spec.workdir)) return unconfinedFallback();
|
|
390
420
|
throw error;
|
|
391
|
-
}
|
|
392
|
-
this.processFacts.set(proc, {
|
|
393
|
-
mode: policy.mode,
|
|
394
|
-
enforcement: confined.enforcement,
|
|
395
|
-
denialSignatures: confined.denialSignatures,
|
|
396
|
-
runnerFailureRules: confined.runnerFailureRules,
|
|
397
|
-
runnerProgram: confined.argv[0],
|
|
398
|
-
workdir: spec.workdir
|
|
399
421
|
});
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Decorate the handle's foreground projection in place, memoized once. The
|
|
425
|
+
* handle keeps its identity (never wrapped in a second object) because the
|
|
426
|
+
* per-process facts and `onProcessDone` key on the exact instance.
|
|
427
|
+
* @param proc - the live execution handle.
|
|
428
|
+
* @param map - maps the settled result.
|
|
429
|
+
* @param mapError - maps a provider rejection.
|
|
430
|
+
* @returns the same handle, with `result()` decorated.
|
|
431
|
+
*/
|
|
432
|
+
static decorateResult(proc, map, mapError) {
|
|
433
|
+
const base = proc.result.bind(proc);
|
|
434
|
+
let decorated;
|
|
435
|
+
proc.result = () => {
|
|
436
|
+
decorated ??= base().then(map, mapError);
|
|
437
|
+
return decorated;
|
|
438
|
+
};
|
|
400
439
|
return proc;
|
|
401
440
|
}
|
|
402
441
|
/** Stamp per-process sandbox facts before `done` settles (bash-sandbox semantics). */
|
|
403
|
-
onProcessDone(proc, stderr,
|
|
442
|
+
onProcessDone(proc, stderr, providerRejected, providerError) {
|
|
404
443
|
const facts = this.processFacts.get(proc);
|
|
405
444
|
if (facts !== void 0) {
|
|
406
445
|
this.processFacts.delete(proc);
|
|
407
|
-
proc.sandbox = (
|
|
446
|
+
proc.sandbox = (providerRejected ? isRunnerSpawnFailure(providerError, facts.runnerProgram, facts.workdir) : classifyRunnerFailure(proc.exitCode, stderr, facts.runnerFailureRules) !== void 0) ? {
|
|
408
447
|
mode: facts.mode,
|
|
409
448
|
denied: false,
|
|
410
449
|
enforcement: facts.enforcement,
|
|
@@ -415,7 +454,7 @@ var GitBashExecutor = class extends LocalBashExecutor {
|
|
|
415
454
|
enforcement: facts.enforcement
|
|
416
455
|
};
|
|
417
456
|
}
|
|
418
|
-
super.onProcessDone(proc, stderr,
|
|
457
|
+
super.onProcessDone(proc, stderr, providerRejected, providerError);
|
|
419
458
|
}
|
|
420
459
|
resolve(request) {
|
|
421
460
|
const bashPath = this.tryBashPath();
|
package/lib/bash-wsl/index.js
CHANGED
|
@@ -165,6 +165,16 @@ var WslBashExecutor = class extends LocalBashExecutor {
|
|
|
165
165
|
if (this.requireSandbox) return this.ctx.sandboxPolicy.defaultMode;
|
|
166
166
|
return this.requireBwrapUsable() ? this.ctx.sandboxPolicy.defaultMode : void 0;
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* The awaitable form of {@link sandboxMode}. The bwrap probe spawns
|
|
170
|
+
* `wsl.exe` synchronously, so the verdict is already available; this exists
|
|
171
|
+
* so both backends present the tool layer the same contract (git-bash's
|
|
172
|
+
* provider probe is genuinely async).
|
|
173
|
+
* @returns the mode to advertise, or `undefined` for an unconfining deployment.
|
|
174
|
+
*/
|
|
175
|
+
async resolveSandboxMode() {
|
|
176
|
+
return this.sandboxMode;
|
|
177
|
+
}
|
|
168
178
|
/** Auto: false means run unconfined. Explicit bwrap: a failed probe throws loud. */
|
|
169
179
|
requireBwrapUsable() {
|
|
170
180
|
if (this.probeBwrapOnce()) return true;
|
|
@@ -223,19 +233,26 @@ var WslBashExecutor = class extends LocalBashExecutor {
|
|
|
223
233
|
payload(command) {
|
|
224
234
|
return `echo ${Buffer.from(command, "utf8").toString("base64")} | base64 -d | bash`;
|
|
225
235
|
}
|
|
226
|
-
/** The workdir `resolve()` fills when a request omits one. */
|
|
227
|
-
defaultWorkdir() {
|
|
228
|
-
return this.config.cwd ?? process.cwd();
|
|
229
|
-
}
|
|
230
236
|
/**
|
|
231
|
-
* The plain WSL argv for one spec (no sandbox).
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* workdir
|
|
237
|
+
* The plain WSL argv for one spec (no sandbox).
|
|
238
|
+
*
|
|
239
|
+
* `--cd` always carries the resolved workdir. That is the auto-cd contract:
|
|
240
|
+
* a command the caller started without an explicit `workdir` still lands in
|
|
241
|
+
* the WSL view of the session directory (`/mnt/<drive>/...`) rather than
|
|
242
|
+
* wherever the distro happens to default to. wsl.exe does translate an
|
|
243
|
+
* inherited Windows cwd on its own, but only for a path it can translate
|
|
244
|
+
* and only under the distro's automount settings — passing the workdir
|
|
245
|
+
* explicitly keeps the start directory a property of the spec instead of a
|
|
246
|
+
* property of the distro. A Linux workdir passes through verbatim.
|
|
247
|
+
*
|
|
248
|
+
* (This used to skip `--cd` when the workdir equaled the configured
|
|
249
|
+
* default. That branch was dead — it compared against the `cwd` volatile
|
|
250
|
+
* *wrapper* rather than its value, so it never matched — and skipping is
|
|
251
|
+
* not worth reviving: the explicit form is what the auto-cd feature is
|
|
252
|
+
* specified as, and it is the behaviour every deployment already runs.)
|
|
235
253
|
*/
|
|
236
254
|
argv(spec) {
|
|
237
|
-
const head = [this.wslPath()];
|
|
238
|
-
if (spec.workdir !== this.defaultWorkdir()) head.push("--cd", spec.workdir);
|
|
255
|
+
const head = [this.wslPath(), "--cd", spec.workdir];
|
|
239
256
|
const distro = this.distro();
|
|
240
257
|
if (distro !== void 0) head.push("-d", distro);
|
|
241
258
|
head.push("--", "bash", "-c", this.payload(spec.command));
|
|
@@ -248,8 +265,7 @@ var WslBashExecutor = class extends LocalBashExecutor {
|
|
|
248
265
|
bwrapArgv(spec) {
|
|
249
266
|
const policy = this.confinedPolicy(spec);
|
|
250
267
|
const linuxRoot = toWslPath(policy.workspaceRoot);
|
|
251
|
-
const head = [this.wslPath()];
|
|
252
|
-
if (spec.workdir !== this.defaultWorkdir()) head.push("--cd", spec.workdir);
|
|
268
|
+
const head = [this.wslPath(), "--cd", spec.workdir];
|
|
253
269
|
const distro = this.distro();
|
|
254
270
|
if (distro !== void 0) head.push("-d", distro);
|
|
255
271
|
const mode = policy.mode;
|
|
@@ -259,22 +275,37 @@ var WslBashExecutor = class extends LocalBashExecutor {
|
|
|
259
275
|
}), "bash", "-c", this.payload(spec.command));
|
|
260
276
|
return head;
|
|
261
277
|
}
|
|
262
|
-
|
|
278
|
+
/** Per-process confinement facts retained until settlement (bash-sandbox pattern). */
|
|
279
|
+
processFacts = /* @__PURE__ */ new Map();
|
|
280
|
+
/**
|
|
281
|
+
* Execute one resolved spec through the bwrap-wrapped (or plain) WSL argv.
|
|
282
|
+
* Foreground is a property of awaiting `result()`, not of the call — one
|
|
283
|
+
* handle serves both call styles, as the 0.1.7 seam defines it. A runner
|
|
284
|
+
* failure is stamped as `runnerFailed` rather than retried: unlike the
|
|
285
|
+
* windows-acl probe, a distro missing bubblewrap is evidence the deployment
|
|
286
|
+
* has no confinement at all, and the spec's error contract surfaces it.
|
|
287
|
+
* @param spec - the resolved execution spec from {@link resolve}.
|
|
288
|
+
* @returns the live handle; sandbox facts land on both `proc.sandbox` (at
|
|
289
|
+
* settlement) and the decorated `result()`.
|
|
290
|
+
*/
|
|
291
|
+
async execute(spec) {
|
|
263
292
|
const policy = this.confinedPolicy(spec);
|
|
264
|
-
if (policy.mode === "danger-full-access") return {
|
|
265
|
-
...
|
|
293
|
+
if (policy.mode === "danger-full-access") return WslBashExecutor.decorateResult(await this.executeArgv(spec, this.argv(spec)), (result) => ({
|
|
294
|
+
...result,
|
|
266
295
|
sandbox: {
|
|
267
296
|
mode: "danger-full-access",
|
|
268
297
|
denied: false
|
|
269
298
|
}
|
|
270
|
-
};
|
|
271
|
-
if (this.sandboxStance === "none") return this.
|
|
299
|
+
}));
|
|
300
|
+
if (this.sandboxStance === "none") return this.executeArgv(spec, this.argv(spec));
|
|
272
301
|
if (!this.requireBwrapUsable()) {
|
|
273
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.");
|
|
274
|
-
return this.
|
|
303
|
+
return this.executeArgv(spec, this.argv(spec));
|
|
275
304
|
}
|
|
276
|
-
const
|
|
277
|
-
|
|
305
|
+
const proc = await this.executeArgv(spec, this.bwrapArgv(spec), (started) => {
|
|
306
|
+
this.processFacts.set(started, { mode: policy.mode });
|
|
307
|
+
});
|
|
308
|
+
return WslBashExecutor.decorateResult(proc, (result) => classifyRunnerFailure(result.exitCode, result.stderr.text, BWRAP_RUNNER_FAILURE_RULES) !== void 0 ? {
|
|
278
309
|
...result,
|
|
279
310
|
sandbox: {
|
|
280
311
|
mode: policy.mode,
|
|
@@ -282,32 +313,35 @@ var WslBashExecutor = class extends LocalBashExecutor {
|
|
|
282
313
|
enforcement: "full",
|
|
283
314
|
runnerFailed: true
|
|
284
315
|
}
|
|
285
|
-
}
|
|
286
|
-
return {
|
|
316
|
+
} : {
|
|
287
317
|
...result,
|
|
288
318
|
sandbox: {
|
|
289
319
|
mode: policy.mode,
|
|
290
320
|
denied: classifyDenial(result, BWRAP_DENIAL_SIGNATURES),
|
|
291
321
|
enforcement: "full"
|
|
292
322
|
}
|
|
293
|
-
};
|
|
323
|
+
});
|
|
294
324
|
}
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
325
|
+
/**
|
|
326
|
+
* Decorate the handle's foreground projection in place, memoized once. The
|
|
327
|
+
* handle keeps its identity (never wrapped in a second object) because the
|
|
328
|
+
* per-process facts and `onProcessDone` key on the exact instance.
|
|
329
|
+
* @param proc - the live execution handle.
|
|
330
|
+
* @param map - maps the settled result.
|
|
331
|
+
* @param mapError - maps a provider rejection.
|
|
332
|
+
* @returns the same handle, with `result()` decorated.
|
|
333
|
+
*/
|
|
334
|
+
static decorateResult(proc, map, mapError) {
|
|
335
|
+
const base = proc.result.bind(proc);
|
|
336
|
+
let decorated;
|
|
337
|
+
proc.result = () => {
|
|
338
|
+
decorated ??= base().then(map, mapError);
|
|
339
|
+
return decorated;
|
|
340
|
+
};
|
|
307
341
|
return proc;
|
|
308
342
|
}
|
|
309
343
|
/** Stamp per-process sandbox facts before `done` settles (bash-sandbox semantics). */
|
|
310
|
-
onProcessDone(proc, stderr,
|
|
344
|
+
onProcessDone(proc, stderr, providerRejected, providerError) {
|
|
311
345
|
const facts = this.processFacts.get(proc);
|
|
312
346
|
if (facts !== void 0) {
|
|
313
347
|
this.processFacts.delete(proc);
|
|
@@ -322,7 +356,7 @@ var WslBashExecutor = class extends LocalBashExecutor {
|
|
|
322
356
|
enforcement: "full"
|
|
323
357
|
};
|
|
324
358
|
}
|
|
325
|
-
super.onProcessDone(proc, stderr,
|
|
359
|
+
super.onProcessDone(proc, stderr, providerRejected, providerError);
|
|
326
360
|
}
|
|
327
361
|
resolve(request) {
|
|
328
362
|
return {
|
package/lib/index.js
CHANGED
|
@@ -4,16 +4,19 @@
|
|
|
4
4
|
// DeepSeek Harness project's shell packages (see THIRD_PARTY_NOTICES),
|
|
5
5
|
// compiled to plain ESM JS:
|
|
6
6
|
//
|
|
7
|
-
// lib/shell-select/ — ShellSelectExecutor (the ctx.shell selector)
|
|
8
7
|
// lib/bash-git/ — GitBashExecutor (MSYS / Git for Windows)
|
|
9
8
|
// lib/bash-wsl/ — WslBashExecutor (WSL distros, base64 payloads)
|
|
10
|
-
// lib/tool-bash/ — defineShellTool factory + git_bash / wsl_bash
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// lib/tool-bash/ — defineShellTool factory + git_bash / wsl_bash
|
|
10
|
+
// instances, each owning its own executor
|
|
11
|
+
// (types/backend.js) instead of sharing the ctx.shell
|
|
12
|
+
// seat: dsh 0.1.7 removed ShellExecRequest.shell, the
|
|
13
|
+
// routing field the old shell-select design used.
|
|
14
|
+
// lib/vendor/ — helpers.js (bash-sandbox classification, including the
|
|
15
|
+
// anchored runner-failure rule the base runtime does
|
|
16
|
+
// not export) and bwrap-profiles.js (bwrap rules/args).
|
|
14
17
|
//
|
|
15
18
|
// Only published @deepseek-ai base packages are imported (dsh-shell,
|
|
16
|
-
// dsh-sandbox, dsh-
|
|
19
|
+
// dsh-sandbox, dsh-bash-local, dsh-tools, dsh-llm, ...), so the plugin runs
|
|
17
20
|
// on any standard deployment — no additional runtime packages required.
|
|
18
21
|
//
|
|
19
22
|
// The composition wiring lives in cordis.patch.yml.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backend ownership for the bundled shell tools.
|
|
3
|
+
*
|
|
4
|
+
* dsh 0.1.7 removed the routing field from the shell seam: `ShellExecRequest`
|
|
5
|
+
* no longer carries `shell`, so the pre-0.1.7 design — one selector executor
|
|
6
|
+
* occupying `ctx.shell` and dispatching each request to a named backend — has
|
|
7
|
+
* no routing input left. The tool is what knows which shell it wants, so each
|
|
8
|
+
* tool now owns exactly one executor instance and drives it directly. The
|
|
9
|
+
* `ctx.shell` seat is left entirely to the base bundle's own executor
|
|
10
|
+
* (`pwsh-sandbox` on win32), which also frees every plugin that injects
|
|
11
|
+
* `shell` (dsh's `tool-pwsh`, `permission-presets`, …) from waiting on us.
|
|
12
|
+
*
|
|
13
|
+
* `ShellExecutor` is a cordis `Service`: constructing one registers it under
|
|
14
|
+
* `shell` on the context it is handed, and a second registration on the same
|
|
15
|
+
* context throws (cordis' standard duplicate-service behavior). An owned
|
|
16
|
+
* executor is therefore built on a child fiber whose `shell` scope is
|
|
17
|
+
* isolated, so it never contends for the seat.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-win-multi-bash/tool-bash/backend
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Services every bundled executor needs before it can be constructed. */
|
|
23
|
+
const EXECUTOR_INJECT = ['subprocess', 'sandbox', 'sandboxPolicy'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build one executor instance on an isolated child fiber.
|
|
27
|
+
* @param ctx - the owning tool's context; the tool must already inject
|
|
28
|
+
* {@link EXECUTOR_INJECT} so those services are present here.
|
|
29
|
+
* @param Executor - the executor class to construct.
|
|
30
|
+
* @param config - the executor's resolved config partition.
|
|
31
|
+
* @returns the instance plus the fiber that owns it; dispose the fiber to tear
|
|
32
|
+
* the backend down with the tool.
|
|
33
|
+
*/
|
|
34
|
+
export function ownExecutor(ctx, Executor, config) {
|
|
35
|
+
const fiber = ctx.plugin({
|
|
36
|
+
inject: EXECUTOR_INJECT,
|
|
37
|
+
apply: () => {},
|
|
38
|
+
});
|
|
39
|
+
const backendCtx = fiber.ctx.isolate('shell', Symbol(Executor.name));
|
|
40
|
+
return { executor: new Executor(backendCtx, config), fiber };
|
|
41
|
+
}
|
|
@@ -22,4 +22,70 @@ export function processOutcome(proc) {
|
|
|
22
22
|
}
|
|
23
23
|
return { status: 'completed', detail: `exit code: ${proc.exitCode ?? 0}` };
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The process's non-consuming stream readers as registry pull sources. They
|
|
27
|
+
* bind lazily because the process is spawned inside the starter, after the
|
|
28
|
+
* registry admitted the job; a read before the spawn yields nothing, and the
|
|
29
|
+
* pump keeps the model's consuming cursor untouched. A rejected spawn's
|
|
30
|
+
* stderr reader carries the provider's `subprocess failed before reporting an
|
|
31
|
+
* outcome: …` note.
|
|
32
|
+
*
|
|
33
|
+
* As of dsh 0.1.7 the registry owns the output ring and pumps these sources
|
|
34
|
+
* itself; a background handle no longer hands its reads back through the
|
|
35
|
+
* starter's return value.
|
|
36
|
+
* @param proc - the started process, once the starter has spawned it (or `undefined` before).
|
|
37
|
+
* @returns one source per stream, stdout first.
|
|
38
|
+
*/
|
|
39
|
+
export function processSources(proc) {
|
|
40
|
+
const source = (channel) => ({
|
|
41
|
+
channel,
|
|
42
|
+
read: (fromByte) => {
|
|
43
|
+
const live = proc();
|
|
44
|
+
return live === undefined
|
|
45
|
+
? { text: '', nextOffset: fromByte, lossy: false }
|
|
46
|
+
: live.observed[channel].readFrom(fromByte);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return [source('stdout'), source('stderr')];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Adapt asynchronous shell preparation after job admission without exposing a
|
|
53
|
+
* partial process. The returned hooks settle only once preparation AND the
|
|
54
|
+
* process have; a cancel before the spawn aborts preparation, and a cancel
|
|
55
|
+
* after it kills the process.
|
|
56
|
+
* @param start - starts the process with the job-owned cancellation signal.
|
|
57
|
+
* @param outcome - projects the settled process into the job outcome.
|
|
58
|
+
* @returns synchronous job hooks whose `done` includes preparation and settlement.
|
|
59
|
+
*/
|
|
60
|
+
export function processJob(start, outcome) {
|
|
61
|
+
const controller = new AbortController();
|
|
62
|
+
let process;
|
|
63
|
+
return {
|
|
64
|
+
cancel: (reason) => {
|
|
65
|
+
if (controller.signal.aborted)
|
|
66
|
+
return;
|
|
67
|
+
controller.abort(reason);
|
|
68
|
+
process?.kill();
|
|
69
|
+
},
|
|
70
|
+
done: (async () => {
|
|
71
|
+
try {
|
|
72
|
+
process = await start(controller.signal);
|
|
73
|
+
try {
|
|
74
|
+
if (controller.signal.aborted)
|
|
75
|
+
process.kill();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
await process.done;
|
|
79
|
+
}
|
|
80
|
+
return outcome(process);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
status: controller.signal.aborted && process === undefined ? 'killed' : 'failed',
|
|
85
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
})(),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
25
91
|
//# sourceMappingURL=background.js.map
|