dsh-plugin-wsl-env 0.8.0 → 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 +71 -12
- package/CONTRIBUTING.md +125 -62
- package/README.md +36 -21
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +194 -54
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +87 -42
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +35 -17
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/index.js +11 -6
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/probe-cache.d.ts +12 -1
- package/lib/probe-cache.js +11 -3
- package/lib/sandbox-core.d.ts +70 -0
- package/lib/sandbox-core.js +187 -18
- package/lib/wsl.js +10 -4
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +13 -5
- package/scripts/diagnose.sh +92 -0
|
@@ -2,17 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
Status: proposed upstream as
|
|
4
4
|
[deepseek-ai/deepseek-harness#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769)
|
|
5
|
-
(targeting `packages/api/terminal-controller`, version line `0.2.0-rc.x`).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
(targeting `packages/api/terminal-controller`, version line `0.2.0-rc.x`). The
|
|
6
|
+
plugin worked around the execution half of this gap — a Windows-folder
|
|
7
|
+
session's terminal runs a host shell (`subprocess-wsl.hostSessions`) — and
|
|
8
|
+
could not work around the title half. This is the record: the GUI titles every
|
|
9
|
+
tab from the composition's single shell profile, so a per-session shell choice
|
|
10
|
+
is invisible in the title.
|
|
9
11
|
|
|
10
12
|
## The gap
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
The display title is fixed from the resolved shell profile and never revisited.
|
|
15
|
+
The private `TerminalController.spawn` builds `info`, with `title: shell.name`,
|
|
16
|
+
only after `await subprocess.spawnTerminal(...)` has returned, and it does not
|
|
17
|
+
read back what that call started:
|
|
14
18
|
|
|
15
19
|
```ts
|
|
20
|
+
const handle = await subprocess.spawnTerminal({
|
|
21
|
+
argv: [shell.path, ...shell.args], cwd: environment.cwd, cols: request.cols, ...
|
|
22
|
+
})
|
|
16
23
|
const info: WebTerminalInfo = {
|
|
17
24
|
id: request.id, shell, title: shell.name, cwd: environment.cwd, ...
|
|
18
25
|
}
|
|
@@ -48,10 +55,15 @@ Three escape hatches were evaluated against `0.2.0-rc.2` and each is closed:
|
|
|
48
55
|
inserts `terminal-controller`, and the client model that the
|
|
49
56
|
`dsh-client-ui-sidebar-terminal` entry waits for mounts with
|
|
50
57
|
`inject = ['remote', 'remote.terminal']` — the `terminal` namespace the host
|
|
51
|
-
controller registers in its constructor. Disabling the row fails web boot
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
controller registers in its constructor. Disabling the row fails web boot
|
|
59
|
+
with this two-line error:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
web boot: 1 entry did not activate
|
|
63
|
+
@deepseek-ai/dsh-client-ui-sidebar-terminal: pending (waiting for service: webTerminals)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Verified live against `dsh-desktop 0.2.0-rc.2` (Electron 44.0.0).
|
|
55
67
|
3. **Per-session config.** The `shell` profile is composition state; `!!js`
|
|
56
68
|
expressions evaluate once at config resolution and reach no Session
|
|
57
69
|
identity.
|
|
@@ -62,7 +74,7 @@ Any one of these closes the gap; the first is the smallest and keeps the
|
|
|
62
74
|
profile name as the fallback:
|
|
63
75
|
|
|
64
76
|
- `SubprocessTerminalSpawnSpec` gains an optional `displayTitle` (or the handle
|
|
65
|
-
exposes the launched `argv`), and `
|
|
77
|
+
exposes the launched `argv`), and `spawn()` uses it for `info.title` when
|
|
66
78
|
present — falling back to `shell.name`. A provider that rewrites a launch
|
|
67
79
|
states the program it started; every other launch keeps the profile name.
|
|
68
80
|
- Alternatively, the controller derives the title from the effective program
|
|
@@ -71,11 +83,34 @@ profile name as the fallback:
|
|
|
71
83
|
|
|
72
84
|
With either, this plugin deletes the limitation paragraph in
|
|
73
85
|
[LIMITATIONS.md](LIMITATIONS.md) and the `WSL`-over-PowerShell title, for any
|
|
74
|
-
provider that rewrites launches — not just WSL.
|
|
86
|
+
provider that rewrites launches — not just WSL. The same limitation is stated
|
|
87
|
+
as current fact in four places. Three go stale the day the proposal lands; the
|
|
88
|
+
fourth is already out of step and needs the corrections above whenever it is
|
|
89
|
+
next touched:
|
|
90
|
+
|
|
91
|
+
- the GUI-terminal comment block in [`cordis.patch.yml`](../cordis.patch.yml);
|
|
92
|
+
- the closing paragraph of "Why the terminal provider is app-level" in
|
|
93
|
+
[ARCHITECTURE.md](ARCHITECTURE.md);
|
|
94
|
+
- R4's `残差` (residual) column in [REQUIREMENTS.zh.md](REQUIREMENTS.zh.md);
|
|
95
|
+
- the `WSL` tab-title bullet under "Terminals" in
|
|
96
|
+
[LIMITATIONS.md](LIMITATIONS.md) — the paragraph named just above. It says
|
|
97
|
+
the controller titles a tab *before* the provider rewrites the launch, the
|
|
98
|
+
reverse of the ordering corrected at the top of this file, and it carries the
|
|
99
|
+
one-line `shellCandidates` workaround that drops the row's `shell` block.
|
|
75
100
|
|
|
76
101
|
## Workarounds until then
|
|
77
102
|
|
|
78
103
|
- Double-click a tab's title to rename it (a shipped client feature; the
|
|
79
104
|
controller's `rename` Remote API accepts 1–120 characters).
|
|
80
|
-
-
|
|
81
|
-
manually selected shell is titled after itself.
|
|
105
|
+
- Re-declare the `terminal-controller` row in a user layer with a non-empty
|
|
106
|
+
`shellCandidates`: a manually selected shell is titled after itself. A patch
|
|
107
|
+
config replaces the whole config rather than deep-merging it
|
|
108
|
+
(`packages/boot/app-boot/src/config-schema/document.ts`), so the override has
|
|
109
|
+
to carry this plugin's `shell` block verbatim — `path: wsl.exe`, `name: WSL`,
|
|
110
|
+
`args: []` — or the terminal stops being a distro terminal. Expect the menu
|
|
111
|
+
to grow the host shells discovery finds on the root provider, which receives
|
|
112
|
+
no directory: executables this terminal never starts (the second bullet of
|
|
113
|
+
the GUI-terminal comment in [`cordis.patch.yml`](../cordis.patch.yml)).
|
|
114
|
+
[LIMITATIONS.md](LIMITATIONS.md) still carries the older one-line form of
|
|
115
|
+
this tip, which drops the `shell` block; the version above is the one that
|
|
116
|
+
works.
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,7 @@ import { canonicalPath, classifyRunnerFailure, isRunnerSpawnFailure, matchesSign
|
|
|
47
47
|
import { isAnotherDistrosUnc, isRelativeWorldPath, isWorldPathUnder, isWslUnc, posixToUnc, toLinuxPath, toWorldPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
48
48
|
import { isConfinedMutation, mutationModeRefusal, outsideDistroRefusal, shareSubstrateRefusal, substrateFailure, workspaceWriteDenial } from "./fs-decisions.js";
|
|
49
49
|
import { WslSandbox } from "./sandbox.js";
|
|
50
|
+
import { bwrapFailure, missingBwrapRemedy, withBwrapHint } from "./sandbox-core.js";
|
|
50
51
|
import { armDistroWatcher } from "./watcher.js";
|
|
51
52
|
import { AgentUnavailableError } from "./agent-errors.js";
|
|
52
53
|
import { agentExecutionHandle } from "./agent-exec.js";
|
|
@@ -649,7 +650,10 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
649
650
|
const { enforcement, denialSignatures, runnerFailureRules, windowsDrive } = confined;
|
|
650
651
|
const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, runnerFailureRules);
|
|
651
652
|
if (runnerFailure !== undefined) {
|
|
652
|
-
|
|
653
|
+
// This path computes `mode` as `policy?.mode`, which the seam spells as
|
|
654
|
+
// the wider union: a runner failure is only ever thrown under a
|
|
655
|
+
// confinement, and there the mode is one of the two confined ones.
|
|
656
|
+
throw new SandboxUnavailableError(mode, withBwrapHint(runnerFailure.detail, distro));
|
|
653
657
|
}
|
|
654
658
|
return ({
|
|
655
659
|
...result,
|
|
@@ -732,7 +736,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
732
736
|
// Runner failure outranks denial because the command did not run at all.
|
|
733
737
|
const runnerFailure = classifyRunnerFailure(result.exitCode, result.stderr.text, runnerFailureRules);
|
|
734
738
|
if (runnerFailure !== undefined)
|
|
735
|
-
throw new SandboxUnavailableError(mode, runnerFailure.detail);
|
|
739
|
+
throw new SandboxUnavailableError(mode, withBwrapHint(runnerFailure.detail, distro));
|
|
736
740
|
return ({
|
|
737
741
|
...result,
|
|
738
742
|
sandbox: {
|
|
@@ -747,7 +751,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
747
751
|
if (resolved.signal?.aborted === true)
|
|
748
752
|
resolved.signal.throwIfAborted();
|
|
749
753
|
if (confined !== undefined && isRunnerSpawnFailure(error, confined.argv[0], resolved.workdir)) {
|
|
750
|
-
throw new SandboxUnavailableError(mode, String(error));
|
|
754
|
+
throw new SandboxUnavailableError(mode, withBwrapHint(String(error), distro));
|
|
751
755
|
}
|
|
752
756
|
throw error;
|
|
753
757
|
});
|
|
@@ -903,9 +907,10 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
903
907
|
const confinedPolicy = policy;
|
|
904
908
|
const distro = await this.distro();
|
|
905
909
|
if (!(await this.sandbox.usable(distro))) {
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
910
|
+
// The probe's classified remedy — missing vs present-but-broken, with the
|
|
911
|
+
// distro's own package family — is what every other refusal surfaces;
|
|
912
|
+
// this path has the same failure, so it carries the same text.
|
|
913
|
+
throw new SandboxUnavailableError(confinedPolicy.mode, bwrapFailure(config.wslPath, distro) ?? missingBwrapRemedy(distro));
|
|
909
914
|
}
|
|
910
915
|
return confinedAgent({ distro, wslPath: config.wslPath, policy: confinedPolicy, maskWindowsDrive: config.maskWindowsDrive });
|
|
911
916
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start sandbox preflight: surface a missing or broken `bwrap` when a
|
|
3
|
+
* distro session OPENS, not when its first command fails closed.
|
|
4
|
+
*
|
|
5
|
+
* Without this the discovery moment is the model's first command, whose
|
|
6
|
+
* `SANDBOX_UNAVAILABLE` error scrolls past as one tool failure among many; the
|
|
7
|
+
* operator may not read it until several commands have failed the same way.
|
|
8
|
+
* The probe itself is not new — it is the providers' own usability probe, run
|
|
9
|
+
* through the same shared cache (`lib/sandbox-core.js`), so a session-start
|
|
10
|
+
* check costs a real probe only once per process, and the first confined
|
|
11
|
+
* command after it pays nothing. Nothing else changes: the failure semantics
|
|
12
|
+
* stay exactly as fail-closed as before, this service only MOVES THE NEWS.
|
|
13
|
+
*
|
|
14
|
+
* Advisory by construction. A preflight that cannot run (no WSL, a wedged
|
|
15
|
+
* `wsl.exe`) degrades to a log line; it must never fail a session that has not
|
|
16
|
+
* even asked for confinement yet. It also never installs anything: the remedy
|
|
17
|
+
* it prints is the same one the refusals carry, and running an installer under
|
|
18
|
+
* root uninvited would be a posture this plugin explicitly rejects.
|
|
19
|
+
*
|
|
20
|
+
* This is a TypeScript source built to `lib/preflight.js`; edit THIS file and
|
|
21
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
22
|
+
* `pnpm test` fails when it drifts.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-plugin-wsl/preflight
|
|
25
|
+
*/
|
|
26
|
+
import z from "@deepseek-ai/schemastery";
|
|
27
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
28
|
+
export declare const name = "wsl-preflight";
|
|
29
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
30
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
31
|
+
distro: z<string, string, "defined">;
|
|
32
|
+
/** The `wsl.exe` path the probe spawns; keep in step with the providers' `wslPath`. */
|
|
33
|
+
wslPath: z<string, string, "defined">;
|
|
34
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
35
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
36
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
37
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
38
|
+
distro: z<string, string, "defined">;
|
|
39
|
+
/** The `wsl.exe` path the probe spawns; keep in step with the providers' `wslPath`. */
|
|
40
|
+
wslPath: z<string, string, "defined">;
|
|
41
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
42
|
+
sandbox: z<boolean, boolean, "defined">;
|
|
43
|
+
}>>, "plain">;
|
|
44
|
+
/** The config schema's validated output. */
|
|
45
|
+
export interface PreflightConfig {
|
|
46
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
47
|
+
distro?: string;
|
|
48
|
+
/** The `wsl.exe` path the probe spawns. */
|
|
49
|
+
wslPath?: string;
|
|
50
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
51
|
+
sandbox?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The slice of a created agent this service inspects; the harness's agent
|
|
55
|
+
* object is structural here, the same view `auto-preset` uses.
|
|
56
|
+
*/
|
|
57
|
+
export interface PreflightAgent {
|
|
58
|
+
/** The agent's id. */
|
|
59
|
+
id: string;
|
|
60
|
+
/** The session stub, when mounted. */
|
|
61
|
+
session?: {
|
|
62
|
+
header?: {
|
|
63
|
+
cwd?: string | undefined;
|
|
64
|
+
};
|
|
65
|
+
} | undefined;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The cordis context extended with the slices this service consumes. `logger`
|
|
69
|
+
* and the agent service belong to the harness, whose declarations are not
|
|
70
|
+
* visible to this plugin, so they are spelled structurally on top of the
|
|
71
|
+
* framework's {@link Context}.
|
|
72
|
+
*/
|
|
73
|
+
export type PreflightContext = Context & {
|
|
74
|
+
logger?: {
|
|
75
|
+
debug?: (message: string) => void;
|
|
76
|
+
warn?: (message: string) => void;
|
|
77
|
+
};
|
|
78
|
+
agents?: {
|
|
79
|
+
list: () => PreflightAgent[];
|
|
80
|
+
};
|
|
81
|
+
on: (event: "agent/created", listener: (payload: {
|
|
82
|
+
agent?: PreflightAgent;
|
|
83
|
+
} | undefined) => void) => () => boolean;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Mount the preflight service.
|
|
87
|
+
*
|
|
88
|
+
* @param ctx - the cordis context.
|
|
89
|
+
* @param config - the schema-validated plugin config.
|
|
90
|
+
*/
|
|
91
|
+
export declare function apply(ctx: PreflightContext, config?: PreflightConfig): Promise<void>;
|
package/lib/preflight.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-start sandbox preflight: surface a missing or broken `bwrap` when a
|
|
3
|
+
* distro session OPENS, not when its first command fails closed.
|
|
4
|
+
*
|
|
5
|
+
* Without this the discovery moment is the model's first command, whose
|
|
6
|
+
* `SANDBOX_UNAVAILABLE` error scrolls past as one tool failure among many; the
|
|
7
|
+
* operator may not read it until several commands have failed the same way.
|
|
8
|
+
* The probe itself is not new — it is the providers' own usability probe, run
|
|
9
|
+
* through the same shared cache (`lib/sandbox-core.js`), so a session-start
|
|
10
|
+
* check costs a real probe only once per process, and the first confined
|
|
11
|
+
* command after it pays nothing. Nothing else changes: the failure semantics
|
|
12
|
+
* stay exactly as fail-closed as before, this service only MOVES THE NEWS.
|
|
13
|
+
*
|
|
14
|
+
* Advisory by construction. A preflight that cannot run (no WSL, a wedged
|
|
15
|
+
* `wsl.exe`) degrades to a log line; it must never fail a session that has not
|
|
16
|
+
* even asked for confinement yet. It also never installs anything: the remedy
|
|
17
|
+
* it prints is the same one the refusals carry, and running an installer under
|
|
18
|
+
* root uninvited would be a posture this plugin explicitly rejects.
|
|
19
|
+
*
|
|
20
|
+
* This is a TypeScript source built to `lib/preflight.js`; edit THIS file and
|
|
21
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
22
|
+
* `pnpm test` fails when it drifts.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-plugin-wsl/preflight
|
|
25
|
+
*/
|
|
26
|
+
import z from "@deepseek-ai/schemastery";
|
|
27
|
+
import { isWslUnc } from "./paths.js";
|
|
28
|
+
import { defaultDistro } from "./wsl.js";
|
|
29
|
+
import { bwrapFailure, bwrapUsable, missingBwrapRemedy } from "./sandbox-core.js";
|
|
30
|
+
export const name = "wsl-preflight";
|
|
31
|
+
export const Config = z.object({
|
|
32
|
+
/** Distro to preflight; empty means this machine's default distro. */
|
|
33
|
+
distro: z.string().default(""),
|
|
34
|
+
/** The `wsl.exe` path the probe spawns; keep in step with the providers' `wslPath`. */
|
|
35
|
+
wslPath: z.string().default("wsl.exe"),
|
|
36
|
+
/** Whether the session-start sandbox preflight runs at all. */
|
|
37
|
+
sandbox: z.boolean().default(true),
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Mount the preflight service.
|
|
41
|
+
*
|
|
42
|
+
* @param ctx - the cordis context.
|
|
43
|
+
* @param config - the schema-validated plugin config.
|
|
44
|
+
*/
|
|
45
|
+
export async function apply(ctx, config = {}) {
|
|
46
|
+
if (!(config.sandbox ?? true))
|
|
47
|
+
return;
|
|
48
|
+
const wslPath = config.wslPath ?? "wsl.exe";
|
|
49
|
+
const warn = (message) => ctx.logger?.warn?.(message);
|
|
50
|
+
/**
|
|
51
|
+
* Probe the distro once and surface an unusable `bwrap` as a warning.
|
|
52
|
+
*
|
|
53
|
+
* @param agent - the session that just opened, for the workspace scope.
|
|
54
|
+
*/
|
|
55
|
+
const check = async (agent) => {
|
|
56
|
+
// Only a distro workspace has commands that confine: a host session would
|
|
57
|
+
// warn about a sandbox it never uses.
|
|
58
|
+
const cwd = agent?.session?.header?.cwd;
|
|
59
|
+
if (cwd === undefined || !isWslUnc(cwd))
|
|
60
|
+
return;
|
|
61
|
+
let distro = config.distro ?? "";
|
|
62
|
+
if (distro.length === 0) {
|
|
63
|
+
try {
|
|
64
|
+
distro = await defaultDistro({ wslPath });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// No WSL at all is the missing-wsl probe's territory; the first
|
|
68
|
+
// command's own error is the precise one. Stay quiet at warn level.
|
|
69
|
+
ctx.logger?.debug?.(`wsl-preflight: no default distro to preflight (${error?.message ?? error})`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// bwrapUsable answers false on a failed probe — it never rejects — so
|
|
74
|
+
// nothing here can throw except a broken context, which is the caller's
|
|
75
|
+
// bug, not the session's.
|
|
76
|
+
const usable = await bwrapUsable({ wslPath, distro });
|
|
77
|
+
if (usable) {
|
|
78
|
+
ctx.logger?.debug?.(`wsl-preflight: bwrap is usable inside distro "${distro}"`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
warn(`wsl-preflight: ${bwrapFailure(wslPath, distro) ?? missingBwrapRemedy(distro)}`);
|
|
82
|
+
};
|
|
83
|
+
ctx.on("agent/created", (payload) => void check(payload?.agent));
|
|
84
|
+
// Mount order is not a guarantee: a session created during startup can finish
|
|
85
|
+
// before this service mounts and miss the event entirely (dsh-headless does
|
|
86
|
+
// exactly that, and auto-preset sweeps for the same reason).
|
|
87
|
+
try {
|
|
88
|
+
for (const agent of ctx.agents?.list() ?? [])
|
|
89
|
+
void check(agent);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
ctx.logger?.debug?.(`wsl-preflight: could not sweep already-live agents: ${error?.message ?? error}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
package/lib/probe-cache.d.ts
CHANGED
|
@@ -25,10 +25,21 @@ export interface ProbeCache {
|
|
|
25
25
|
*
|
|
26
26
|
* @param key - the probe's cache key.
|
|
27
27
|
* @param probe - the probe to run on a miss. A throw is a failed probe, never
|
|
28
|
-
* a propagated error.
|
|
28
|
+
* a propagated error; its message is remembered for {@link failure}.
|
|
29
29
|
* @returns the verdict; a success is remembered, a failure is not.
|
|
30
30
|
*/
|
|
31
31
|
run(key: string, probe: () => Promise<boolean> | boolean): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* The message of the probe failure the key last recorded, if any. A failed
|
|
34
|
+
* verdict is not remembered as a verdict, but its reason outlives the miss:
|
|
35
|
+
* the remedy a caller prints is composed from WHY the probe failed, and the
|
|
36
|
+
* next caller — often the very next command — must still see it.
|
|
37
|
+
*
|
|
38
|
+
* @param key - the probe's cache key.
|
|
39
|
+
* @returns the thrown error's message, or undefined while no failure is
|
|
40
|
+
* recorded; cleared on the key's next success.
|
|
41
|
+
*/
|
|
42
|
+
failure(key: string): string | undefined;
|
|
32
43
|
/** Remembered successes, for tests and diagnostics. */
|
|
33
44
|
readonly size: number;
|
|
34
45
|
}
|
package/lib/probe-cache.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
export function createProbeCache() {
|
|
27
27
|
const verdicts = new Map();
|
|
28
|
+
const failures = new Map();
|
|
28
29
|
return {
|
|
29
30
|
async run(key, probe) {
|
|
30
31
|
const cached = verdicts.get(key);
|
|
@@ -34,18 +35,25 @@ export function createProbeCache() {
|
|
|
34
35
|
try {
|
|
35
36
|
return await probe();
|
|
36
37
|
}
|
|
37
|
-
catch {
|
|
38
|
+
catch (error) {
|
|
39
|
+
failures.set(key, error instanceof Error ? error.message : String(error));
|
|
38
40
|
return false;
|
|
39
41
|
}
|
|
40
42
|
})();
|
|
41
43
|
verdicts.set(key, pending);
|
|
42
44
|
const verdict = await pending;
|
|
43
|
-
if (verdict)
|
|
45
|
+
if (verdict) {
|
|
44
46
|
verdicts.set(key, true);
|
|
45
|
-
|
|
47
|
+
failures.delete(key);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
46
50
|
verdicts.delete(key);
|
|
51
|
+
}
|
|
47
52
|
return verdict;
|
|
48
53
|
},
|
|
54
|
+
failure(key) {
|
|
55
|
+
return failures.get(key);
|
|
56
|
+
},
|
|
49
57
|
/** Remembered successes, for tests and diagnostics. */
|
|
50
58
|
get size() {
|
|
51
59
|
return verdicts.size;
|
package/lib/sandbox-core.d.ts
CHANGED
|
@@ -16,6 +16,76 @@
|
|
|
16
16
|
*
|
|
17
17
|
* @module dsh-plugin-wsl/sandbox-core
|
|
18
18
|
*/
|
|
19
|
+
/**
|
|
20
|
+
* The canonical remedy for a distro whose `bwrap` is missing — also the
|
|
21
|
+
* fallback for callers that know only "the probe failed", without a classified
|
|
22
|
+
* reason. The direct command is family-aware when the caller knows the family;
|
|
23
|
+
* the bootstrap script is always the first-choice path because it detects the
|
|
24
|
+
* family itself.
|
|
25
|
+
*
|
|
26
|
+
* @param distro - the distro to install into.
|
|
27
|
+
* @param family - the detected package family, when known.
|
|
28
|
+
* @returns the operator-facing remedy text.
|
|
29
|
+
*/
|
|
30
|
+
export declare function missingBwrapRemedy(distro: string, family?: string): string;
|
|
31
|
+
/**
|
|
32
|
+
* The remedy for a bwrap that PROBED healthy but failed a run — the runtime
|
|
33
|
+
* twin of {@link missingBwrapRemedy}. No family knowledge exists on this path
|
|
34
|
+
* (the probe succeeded, so no family probe ever ran), and reinstalling is only
|
|
35
|
+
* one of the plausible fixes, so the hint leads with diagnosis.
|
|
36
|
+
*
|
|
37
|
+
* @param distro - the distro whose bwrap failed at run time.
|
|
38
|
+
* @returns the operator-facing hint text.
|
|
39
|
+
*/
|
|
40
|
+
export declare function bwrapInstallHint(distro: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Append {@link bwrapInstallHint} to a runner-failure detail that names bwrap.
|
|
43
|
+
* A probe-time failure composes its own classified remedy; this covers the
|
|
44
|
+
* OTHER order — bwrap probed healthy, then failed a real command — where no
|
|
45
|
+
* probe failure is on record and the raw `bwrap: …` line is all the model
|
|
46
|
+
* would see. Details that do not name bwrap pass through untouched: the
|
|
47
|
+
* runner-failure rules match the confinement runner, and the confinement
|
|
48
|
+
* runner is always bwrap here, but the wording stays the decider so a future
|
|
49
|
+
* rule change cannot attach a bwrap remedy to a foreign failure.
|
|
50
|
+
*
|
|
51
|
+
* @param detail - the classified runner-failure detail.
|
|
52
|
+
* @param distro - the distro the command ran in.
|
|
53
|
+
* @returns the detail, with the remedy appended when it names bwrap.
|
|
54
|
+
*/
|
|
55
|
+
export declare function withBwrapHint(detail: string, distro: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* Probe one distro's `bwrap` under the shared verdict cache. The probe mirrors
|
|
58
|
+
* upstream's: apply the real read-only profile around `true`, and treat a zero
|
|
59
|
+
* exit as the kernel having accepted and enforced it. A missing `bwrap` fails
|
|
60
|
+
* the spawn and probes unusable, which is what makes the providers fail closed
|
|
61
|
+
* rather than silently run unconfined.
|
|
62
|
+
*
|
|
63
|
+
* A success is remembered for the process lifetime; a failure is not, so a
|
|
64
|
+
* `bubblewrap` installed while the app is running is believed on the next
|
|
65
|
+
* command. The failure's composed remedy stays readable through
|
|
66
|
+
* {@link bwrapFailure}, which is how every refusal and the preflight surface it.
|
|
67
|
+
*
|
|
68
|
+
* @param options - the `wsl.exe` path and the distro to probe.
|
|
69
|
+
* @param options.wslPath - the `wsl.exe` path.
|
|
70
|
+
* @param options.distro - the distro to probe.
|
|
71
|
+
* @param options.signal - optional cancellation.
|
|
72
|
+
* @returns true when a confined command can run there.
|
|
73
|
+
*/
|
|
74
|
+
export declare function bwrapUsable(options: {
|
|
75
|
+
wslPath: string;
|
|
76
|
+
distro: string;
|
|
77
|
+
signal?: AbortSignal | undefined;
|
|
78
|
+
}): Promise<boolean>;
|
|
79
|
+
/**
|
|
80
|
+
* The remedy text of a key's last failed probe, as {@link bwrapUsable} composed
|
|
81
|
+
* it — the same string the providers' refusals carry. Undefined while the probe
|
|
82
|
+
* never failed (or last succeeded).
|
|
83
|
+
*
|
|
84
|
+
* @param wslPath - the `wsl.exe` path the probe used.
|
|
85
|
+
* @param distro - the distro the probe ran in.
|
|
86
|
+
* @returns the composed remedy, or undefined.
|
|
87
|
+
*/
|
|
88
|
+
export declare function bwrapFailure(wslPath: string, distro: string): string | undefined;
|
|
19
89
|
/**
|
|
20
90
|
* The seam's `ConfinedArgv` settlement facts, plus this core's `windowsDrive`
|
|
21
91
|
* mitigation flag.
|