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
package/lib/sandbox-core.js
CHANGED
|
@@ -65,6 +65,188 @@ const ENFORCEMENT = "partial";
|
|
|
65
65
|
* where it is unit-testable without the peers this module needs.
|
|
66
66
|
*/
|
|
67
67
|
const probeVerdicts = createProbeCache();
|
|
68
|
+
/** The cache key one (wsl.exe, distro) pair's bwrap verdict lives under. */
|
|
69
|
+
const probeKey = (wslPath, distro) => `${wslPath}\u0000${distro}`;
|
|
70
|
+
/**
|
|
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.
|
|
91
|
+
*
|
|
92
|
+
* @param output - the failed probe's combined stdout+stderr, NULs stripped.
|
|
93
|
+
* @returns why the probe failed, as the remedy needs it.
|
|
94
|
+
*/
|
|
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;
|
|
127
|
+
}
|
|
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.`);
|
|
151
|
+
}
|
|
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.`);
|
|
172
|
+
}
|
|
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
|
+
}
|
|
68
250
|
/**
|
|
69
251
|
* Build the sandbox class, bound to one unavailable-error implementation.
|
|
70
252
|
*
|
|
@@ -81,27 +263,16 @@ export function createSandboxCore({ SandboxUnavailableError }) {
|
|
|
81
263
|
this.maskWindowsDrive = config.maskWindowsDrive ?? false;
|
|
82
264
|
}
|
|
83
265
|
/**
|
|
84
|
-
* Whether `bwrap` can create a profile inside this distro.
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* spawn and probes unusable, which is what makes the provider fail closed rather
|
|
88
|
-
* than silently run unconfined.
|
|
89
|
-
*
|
|
90
|
-
* A success is remembered for the process lifetime; a failure is not, so a
|
|
91
|
-
* `bubblewrap` installed while the app is running is believed on the next
|
|
92
|
-
* command.
|
|
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.
|
|
93
269
|
*
|
|
94
270
|
* @param distro - the distro to probe.
|
|
95
271
|
* @param signal - optional cancellation.
|
|
96
272
|
* @returns true when a confined command can run there.
|
|
97
273
|
*/
|
|
98
274
|
async usable(distro, signal) {
|
|
99
|
-
|
|
100
|
-
return probeVerdicts.run(key, async () => {
|
|
101
|
-
const prefix = bwrapArgvPrefix({ mode: "read-only", workspaceRoot: "/" });
|
|
102
|
-
await runCapture([this.wslPath, "-d", distro, "--exec", ...prefix, "true"], signal);
|
|
103
|
-
return true;
|
|
104
|
-
});
|
|
275
|
+
return bwrapUsable({ wslPath: this.wslPath, distro, signal });
|
|
105
276
|
}
|
|
106
277
|
/**
|
|
107
278
|
* Wrap one distro-side argv in the bwrap profile the policy asks for.
|
|
@@ -122,9 +293,7 @@ export function createSandboxCore({ SandboxUnavailableError }) {
|
|
|
122
293
|
async confine(argv, policy, options) {
|
|
123
294
|
const { distro, signal } = options;
|
|
124
295
|
if (!(await this.usable(distro, signal))) {
|
|
125
|
-
throw new SandboxUnavailableError(policy.mode,
|
|
126
|
-
`"scripts/bootstrap.sh ${distro} --install" (from the package), or directly: ` +
|
|
127
|
-
`wsl.exe -d ${distro} -u root -- apt-get install -y bubblewrap`);
|
|
296
|
+
throw new SandboxUnavailableError(policy.mode, bwrapFailure(this.wslPath, distro) ?? missingBwrapRemedy(distro));
|
|
128
297
|
}
|
|
129
298
|
const workspaceRoot = toLinuxPath(policy.workspaceRoot, { distro });
|
|
130
299
|
const prefix = bwrapArgvPrefix({ ...policy, workspaceRoot }, { maskWindowsDrive: this.maskWindowsDrive });
|
package/lib/wsl.js
CHANGED
|
@@ -104,8 +104,11 @@ export function parseDistroList(stdout) {
|
|
|
104
104
|
*/
|
|
105
105
|
export async function defaultDistro(options = {}) {
|
|
106
106
|
const [first] = await listDistros(options);
|
|
107
|
-
if (first === undefined)
|
|
108
|
-
throw new Error("dsh-plugin-wsl: no WSL distro is installed (`wsl.exe -l -q` returned nothing)"
|
|
107
|
+
if (first === undefined) {
|
|
108
|
+
throw new Error("dsh-plugin-wsl: no WSL distro is installed (`wsl.exe -l -q` returned nothing). " +
|
|
109
|
+
"Install one: `wsl.exe --list --online` lists the available names, then `wsl --install -d <name>`, " +
|
|
110
|
+
"or point this row's `distro` at an existing one.");
|
|
111
|
+
}
|
|
109
112
|
return first;
|
|
110
113
|
}
|
|
111
114
|
/**
|
|
@@ -134,7 +137,8 @@ export async function linuxHomePath(distro, options = {}) {
|
|
|
134
137
|
export function parseHomePath(distro, stdout) {
|
|
135
138
|
const home = String(stdout).trim();
|
|
136
139
|
if (home.length === 0 || !home.startsWith("/")) {
|
|
137
|
-
throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}")`
|
|
140
|
+
throw new Error(`dsh-plugin-wsl: distro "${distro}" reported no usable $HOME (got "${home}"). ` +
|
|
141
|
+
`Check the distro's default user in /etc/wsl.conf, and that the user's /etc/passwd entry carries a home directory.`);
|
|
138
142
|
}
|
|
139
143
|
return home;
|
|
140
144
|
}
|
|
@@ -337,7 +341,9 @@ export async function defaultShell(distro, options = {}) {
|
|
|
337
341
|
export async function runInDistro(distro, command, options = {}) {
|
|
338
342
|
const shell = options.shell ?? (await defaultShell(distro, options));
|
|
339
343
|
if (shell === undefined) {
|
|
340
|
-
throw new Error(`dsh-plugin-wsl: distro "${distro}" named no usable shell (passwd probe and $SHELL both failed)`
|
|
344
|
+
throw new Error(`dsh-plugin-wsl: distro "${distro}" named no usable shell (passwd probe and $SHELL both failed). ` +
|
|
345
|
+
`Check the user's login shell in /etc/passwd (\`getent passwd "$(id -u)"\` inside the distro), ` +
|
|
346
|
+
`or point this row's \`shell\` at an existing absolute path.`);
|
|
341
347
|
}
|
|
342
348
|
return runCapture([options.wslPath ?? DEFAULT_WSL_PATH, "-d", distro, "--exec", shell, ...shellArgs(shell, options.loginShell !== false), command], options.signal);
|
|
343
349
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "WSL sandbox preflight",
|
|
4
|
+
"description": "Probes bubblewrap when a distro session opens and surfaces a missing or unusable bwrap as a warning with the exact remedy — instead of the first command's fail-closed error being the discovery moment. Advisory only; never installs anything."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -47,12 +47,18 @@
|
|
|
47
47
|
"types": "./lib/paths.d.ts",
|
|
48
48
|
"default": "./lib/paths.js"
|
|
49
49
|
},
|
|
50
|
+
"./preflight": {
|
|
51
|
+
"types": "./lib/preflight.d.ts",
|
|
52
|
+
"default": "./lib/preflight.js"
|
|
53
|
+
},
|
|
50
54
|
"./picker": {
|
|
51
55
|
"types": "./lib/picker.d.ts",
|
|
52
56
|
"default": "./lib/picker.js"
|
|
53
57
|
},
|
|
54
58
|
"./picker/locale/en.json": "./locale/picker/en.json",
|
|
55
59
|
"./picker/locale/zh.json": "./locale/picker/zh.json",
|
|
60
|
+
"./preflight/locale/en.json": "./locale/preflight/en.json",
|
|
61
|
+
"./preflight/locale/zh.json": "./locale/preflight/zh.json",
|
|
56
62
|
"./shell": {
|
|
57
63
|
"types": "./lib/shell.d.ts",
|
|
58
64
|
"default": "./lib/shell.js"
|
|
@@ -104,16 +110,17 @@
|
|
|
104
110
|
"SECURITY.md",
|
|
105
111
|
"SUPPORT.md",
|
|
106
112
|
"LICENSE",
|
|
107
|
-
"scripts/bootstrap.sh"
|
|
113
|
+
"scripts/bootstrap.sh",
|
|
114
|
+
"scripts/diagnose.sh"
|
|
108
115
|
],
|
|
109
116
|
"scripts": {
|
|
110
117
|
"test": "pnpm run lint:style && pnpm run test:syntax && pnpm run lint:build && pnpm run lint:types && pnpm run test:unit",
|
|
111
118
|
"lint:types": "tsc --noEmit",
|
|
112
119
|
"build": "node scripts/build.mjs",
|
|
113
120
|
"lint:build": "node test/build-freshness.mjs",
|
|
114
|
-
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/
|
|
121
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 test/agent-exec.test.mjs test/agent-fs-script.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/fs-substrate.test.mjs test/fs.boot.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/fsio-agent.test.mjs test/fsio-text.test.mjs test/git-route.test.mjs test/listing.test.mjs test/paths.test.mjs test/ports.test.mjs test/preset-choice.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/sandbox.test.mjs test/search-exec.test.mjs test/search-route.test.mjs test/shell.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs",
|
|
115
122
|
"test:syntax": "node test/syntax.mjs",
|
|
116
|
-
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs",
|
|
123
|
+
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.test.mjs test/workspace-files-wsl.test.mjs test/composition.test.mjs test/provider.test.mjs test/file-reference-wsl.test.mjs test/paths.test.mjs test/listing.test.mjs test/shell.test.mjs test/terminal.test.mjs test/probe-cache.test.mjs test/preflight.test.mjs test/preset-choice.test.mjs test/sandbox.test.mjs test/agent-protocol.test.mjs test/agent.test.mjs test/agent-fs-script.test.mjs test/fsio-agent.test.mjs test/fs-substrate.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/git-route.test.mjs test/fsio-text.test.mjs test/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs",
|
|
117
124
|
"lint:style": "node test/style.mjs",
|
|
118
125
|
"prepublishOnly": "pnpm test",
|
|
119
126
|
"probe": "bash test/probe/run.sh",
|
|
@@ -129,7 +136,8 @@
|
|
|
129
136
|
"probe:agent": "bash test/probe/agent.sh",
|
|
130
137
|
"probe:substrate": "bash test/probe/substrate.sh",
|
|
131
138
|
"probe:exec": "bash test/probe/exec.sh",
|
|
132
|
-
"bootstrap": "bash scripts/bootstrap.sh"
|
|
139
|
+
"bootstrap": "bash scripts/bootstrap.sh",
|
|
140
|
+
"diagnose": "bash scripts/diagnose.sh"
|
|
133
141
|
},
|
|
134
142
|
"packageManager": "pnpm@11.7.0",
|
|
135
143
|
"engines": {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Collect a read-only diagnostic report for a bug report, in one command:
|
|
3
|
+
#
|
|
4
|
+
# pnpm run diagnose -- [distro] # distro optional; default distro otherwise
|
|
5
|
+
#
|
|
6
|
+
# Prints versions and probe verdicts as plain text that can be pasted into an
|
|
7
|
+
# issue as-is. It runs NO installation, changes nothing in the distro, and reads
|
|
8
|
+
# nothing secret: the report is tool versions, a kernel release and probe
|
|
9
|
+
# verdicts. Exit code is always 0 — the report is informational; the verdict
|
|
10
|
+
# lines carry the failures.
|
|
11
|
+
#
|
|
12
|
+
# Deeper sandbox probing lives in `npm run probe:sandbox` (run inside the
|
|
13
|
+
# distro); this script answers "what is installed and does bwrap run at all".
|
|
14
|
+
|
|
15
|
+
set -u
|
|
16
|
+
export WSL_UTF8=1 # wsl.exe writes UTF-16LE without it; read as UTF-8 that is mojibake
|
|
17
|
+
|
|
18
|
+
DISTRO="${1:-}"
|
|
19
|
+
|
|
20
|
+
ok() { printf 'ok %s\n' "$1"; }
|
|
21
|
+
miss() { printf 'MISSING %s\n' "$1"; }
|
|
22
|
+
info() { printf 'info %s\n' "$1"; }
|
|
23
|
+
header(){ printf '\n== %s ==\n' "$1"; }
|
|
24
|
+
|
|
25
|
+
echo "# dsh-plugin-wsl-env diagnose report"
|
|
26
|
+
|
|
27
|
+
header "host"
|
|
28
|
+
winver="$(cmd.exe /c ver 2>/dev/null | tr -d '\r' | grep -o '[0-9][0-9.]*' | head -1)"
|
|
29
|
+
if [ -n "$winver" ]; then ok "windows build $winver"; else info "windows build unknown (cmd.exe not reachable through interop)"; fi
|
|
30
|
+
wsl_ver="$(wsl.exe --version 2>/dev/null | tr -d '\0\r' | head -2 | tr '\n' ' ')"
|
|
31
|
+
if [ -n "$wsl_ver" ]; then ok "wsl: $wsl_ver"; else info "wsl --version unsupported on this WSL build"; fi
|
|
32
|
+
wsl_status="$(wsl.exe --status 2>/dev/null | tr -d '\0\r' | tr '\n' ' ')"
|
|
33
|
+
[ -n "$wsl_status" ] && info "wsl status: $wsl_status"
|
|
34
|
+
wsl.exe -l -v 2>/dev/null | tr -d '\0\r' | grep -v '^$' | sed 's/^/ /'
|
|
35
|
+
|
|
36
|
+
header "plugin"
|
|
37
|
+
echo " plugin version: $(node -p "require('./package.json').version" 2>/dev/null || echo unknown)"
|
|
38
|
+
echo " dsh version: $(dsh --version 2>/dev/null || echo unknown)"
|
|
39
|
+
|
|
40
|
+
if [ -z "$DISTRO" ]; then
|
|
41
|
+
DISTRO="$(wsl.exe -l -q 2>/dev/null | tr -d '\0\r' | grep -v '^$' | head -1)"
|
|
42
|
+
fi
|
|
43
|
+
if [ -z "$DISTRO" ]; then
|
|
44
|
+
header "distro"
|
|
45
|
+
miss "no distro installed — 'wsl.exe --list --online' lists names, 'wsl --install -d <name>' installs one"
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
header "distro: $DISTRO"
|
|
50
|
+
in_distro() { wsl.exe -d "$DISTRO" --exec sh -c "$1" 2>/dev/null; }
|
|
51
|
+
|
|
52
|
+
info "kernel: $(in_distro 'uname -r')"
|
|
53
|
+
family="$(in_distro 'for m in apt-get dnf pacman zypper; do command -v $m && break; done' | tr -d '\r')"
|
|
54
|
+
case "$family" in
|
|
55
|
+
*apt-get) info "package family: apt" ;;
|
|
56
|
+
*dnf) info "package family: dnf" ;;
|
|
57
|
+
*pacman) info "package family: pacman" ;;
|
|
58
|
+
*zypper) info "package family: zypper" ;;
|
|
59
|
+
*) info "package family: unknown" ;;
|
|
60
|
+
esac
|
|
61
|
+
|
|
62
|
+
for tool in bwrap rg git inotifywait; do
|
|
63
|
+
if in_distro "command -v $tool" | grep -q .; then
|
|
64
|
+
case "$tool" in
|
|
65
|
+
bwrap) ver="$(in_distro 'bwrap --version' | tr -d '\r' | head -1)" ;;
|
|
66
|
+
rg) ver="$(in_distro 'rg --version' | tr -d '\r' | head -1)" ;;
|
|
67
|
+
git) ver="$(in_distro 'git --version' | tr -d '\r' | head -1)" ;;
|
|
68
|
+
inotifywait) ver="inotifywait (no version flag)" ;;
|
|
69
|
+
esac
|
|
70
|
+
ok "$ver"
|
|
71
|
+
else
|
|
72
|
+
case "$tool" in
|
|
73
|
+
bwrap) miss "bwrap — REQUIRED; every confined command fails closed without it. Install: wsl.exe -d $DISTRO -u root -- apt-get install -y bubblewrap (or: pnpm run bootstrap -- $DISTRO --install)" ;;
|
|
74
|
+
rg) miss "ripgrep — the search tool falls back to slower matching without it" ;;
|
|
75
|
+
git) miss "git — snapshot and diff baselines degrade without it" ;;
|
|
76
|
+
inotifywait) miss "inotify-tools — file watching falls back to polling without it" ;;
|
|
77
|
+
esac
|
|
78
|
+
fi
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
if in_distro 'command -v bwrap' | grep -q .; then
|
|
82
|
+
if in_distro 'bwrap --ro-bind / / -- true'; then
|
|
83
|
+
ok "bwrap runs a read-only profile (the usability probe passes)"
|
|
84
|
+
else
|
|
85
|
+
miss "bwrap is installed but FAILED the read-only profile probe — likely the kernel (unprivileged user namespaces disabled) or an unusable build; reinstalling will not help"
|
|
86
|
+
fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
header "notes"
|
|
90
|
+
info "trace file: set DSH_WSL_TRACE=<path> to record the auto-preset decisions, then re-run the scenario"
|
|
91
|
+
info "paste this report into the issue's Output section; redact anything you would rather not publish"
|
|
92
|
+
exit 0
|