dsh-plugin-wsl-env 0.1.10 → 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/CHANGELOG.md +268 -0
- package/README.md +21 -2
- package/agent/wsl-agent.sh +498 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.md +37 -1
- package/docs/CONFIGURATION.md +12 -1
- package/docs/LIMITATIONS.md +70 -5
- package/docs/PARITY.md +70 -0
- package/docs/PEER-PARITY.md +60 -0
- package/docs/README.zh.md +31 -20
- package/docs/UPSTREAM-FSIO-EXPORT.md +87 -0
- package/lib/agent-confined.js +81 -0
- package/lib/agent-errors.js +33 -0
- package/lib/agent-exec.js +204 -0
- package/lib/agent-protocol.js +174 -0
- package/lib/agent-shared.js +32 -0
- package/lib/agent.js +564 -0
- package/lib/bwrap.js +74 -0
- package/lib/fs-publish.js +42 -0
- package/lib/fs-substrate.js +285 -0
- package/lib/fsio-agent.js +481 -0
- package/lib/fsio-text.js +153 -0
- package/lib/index.js +430 -12
- package/lib/ports.js +61 -0
- package/lib/sandbox.js +35 -28
- package/lib/shell-env.js +28 -2
- package/lib/subprocess.js +15 -0
- package/lib/terminal-route.js +31 -0
- package/lib/watcher.js +127 -0
- package/package.json +13 -4
package/lib/sandbox.js
CHANGED
|
@@ -32,6 +32,12 @@ import { SandboxUnavailableError } from "@deepseek-ai/dsh-sandbox";
|
|
|
32
32
|
import { createProbeCache } from "./probe-cache.js";
|
|
33
33
|
import { toLinuxPath } from "./paths.js";
|
|
34
34
|
import { runCapture } from "./wsl.js";
|
|
35
|
+
import { bwrapArgvPrefix, bwrapProfileArgs } from "./bwrap.js";
|
|
36
|
+
|
|
37
|
+
// The profile and the argv prefix are also what `lib/agent-confined.js` builds
|
|
38
|
+
// for its residents, so both live in the peer-free `lib/bwrap.js` and are
|
|
39
|
+
// re-exported here, where the sandbox's callers have always found them.
|
|
40
|
+
export { bwrapProfileArgs };
|
|
35
41
|
|
|
36
42
|
/**
|
|
37
43
|
* bwrap's denial dialect: a write outside the granted mounts fails with EROFS,
|
|
@@ -60,6 +66,22 @@ const RUNNER_FAILURE_RULES = [{ fatalSignatures: ["bwrap: "] }];
|
|
|
60
66
|
*/
|
|
61
67
|
const ENFORCEMENT = "partial";
|
|
62
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The one mitigation the profile can offer, and its honest ceiling.
|
|
71
|
+
*
|
|
72
|
+
* `maskWindowsDrive` mounts an empty tmpfs over `/mnt`: the Windows drive's
|
|
73
|
+
* files vanish from the command's view, which takes the drive's data off the
|
|
74
|
+
* table and makes the on-drive executables unreachable. It does NOT make the
|
|
75
|
+
* run `full`: binfmt interop dispatches on file CONTENT, so a command that can
|
|
76
|
+
* write the workspace (which `workspace-write` grants) can write a PE there
|
|
77
|
+
* and execute it — binfmt launches it as a Windows process outside the Linux
|
|
78
|
+
* sandbox regardless of where the file sits. The only complete closures are
|
|
79
|
+
* distro-level (`[interop] enabled=false` in `wsl.conf`, which costs every
|
|
80
|
+
* legitimate interop user their tooling) — so the provider reports `partial`
|
|
81
|
+
* either way, and the mask is a decision an operator makes to shrink the hole,
|
|
82
|
+
* recorded as the `windowsDrive: "masked"` fact the model can see.
|
|
83
|
+
*/
|
|
84
|
+
|
|
63
85
|
/**
|
|
64
86
|
* Probe verdicts, keyed by `wslPath\0distro`. The policy — a success is durable, a
|
|
65
87
|
* failure is re-probed, concurrent probes are shared — lives in `probe-cache.js`,
|
|
@@ -67,29 +89,6 @@ const ENFORCEMENT = "partial";
|
|
|
67
89
|
*/
|
|
68
90
|
const probeVerdicts = createProbeCache();
|
|
69
91
|
|
|
70
|
-
/**
|
|
71
|
-
* Build the bwrap profile arguments for one file-effect policy, exactly as
|
|
72
|
-
* upstream's `bwrapProfileArgs` does.
|
|
73
|
-
*
|
|
74
|
-
* `--ro-bind / /` makes the whole distro read-only, `--dev /dev` supplies the
|
|
75
|
-
* `/dev/null` sink a shell needs, `--unshare-pid` keeps the sandbox's process
|
|
76
|
-
* view its own, and `--proc /proc` gives it a matching `/proc`. Under
|
|
77
|
-
* `workspace-write` the workspace is bound read-write and `/tmp` becomes an
|
|
78
|
-
* ephemeral tmpfs — the temp area the mode promises, without exposing the
|
|
79
|
-
* distro's real `/tmp`.
|
|
80
|
-
*
|
|
81
|
-
* @param policy - the policy to express as bwrap mounts.
|
|
82
|
-
* @returns profile arguments, before the `--` separator and the command argv.
|
|
83
|
-
*/
|
|
84
|
-
export function bwrapProfileArgs(policy) {
|
|
85
|
-
const args = ["--ro-bind", "/", "/", "--dev", "/dev", "--unshare-pid", "--proc", "/proc", "--die-with-parent"];
|
|
86
|
-
if (policy.mode === "workspace-write") {
|
|
87
|
-
args.push("--tmpfs", "/tmp");
|
|
88
|
-
args.push("--bind", policy.workspaceRoot, policy.workspaceRoot);
|
|
89
|
-
}
|
|
90
|
-
return args;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
92
|
/**
|
|
94
93
|
* Confine commands inside one distro with `bwrap`.
|
|
95
94
|
*
|
|
@@ -100,9 +99,14 @@ export function bwrapProfileArgs(policy) {
|
|
|
100
99
|
export class WslSandbox {
|
|
101
100
|
/**
|
|
102
101
|
* @param config - the `wsl.exe` path, overridable for a non-standard install.
|
|
102
|
+
* @param config.maskWindowsDrive - shadow `/mnt` with an empty tmpfs in every
|
|
103
|
+
* profile this sandbox builds, hiding the Windows drive from confined
|
|
104
|
+
* commands (see the module doc above {@link ENFORCEMENT} for what that
|
|
105
|
+
* does and does not close).
|
|
103
106
|
*/
|
|
104
107
|
constructor(config = {}) {
|
|
105
108
|
this.wslPath = config.wslPath ?? "wsl.exe";
|
|
109
|
+
this.maskWindowsDrive = config.maskWindowsDrive ?? false;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
/**
|
|
@@ -123,8 +127,8 @@ export class WslSandbox {
|
|
|
123
127
|
async usable(distro, signal) {
|
|
124
128
|
const key = `${this.wslPath}\u0000${distro}`;
|
|
125
129
|
return probeVerdicts.run(key, async () => {
|
|
126
|
-
const
|
|
127
|
-
await runCapture([this.wslPath, "-d", distro, "--exec",
|
|
130
|
+
const prefix = bwrapArgvPrefix({ mode: "read-only", workspaceRoot: "/" });
|
|
131
|
+
await runCapture([this.wslPath, "-d", distro, "--exec", ...prefix, "true"], signal);
|
|
128
132
|
return true;
|
|
129
133
|
});
|
|
130
134
|
}
|
|
@@ -146,16 +150,19 @@ export class WslSandbox {
|
|
|
146
150
|
if (!(await this.usable(distro, signal))) {
|
|
147
151
|
throw new SandboxUnavailableError(
|
|
148
152
|
policy.mode,
|
|
149
|
-
`bwrap is not usable inside distro "${distro}"
|
|
153
|
+
`bwrap is not usable inside distro "${distro}". Install it with ` +
|
|
154
|
+
`"scripts/bootstrap.sh ${distro} --install" (from the package), or directly: ` +
|
|
155
|
+
`wsl.exe -d ${distro} -u root -- apt-get install -y bubblewrap`,
|
|
150
156
|
);
|
|
151
157
|
}
|
|
152
158
|
const workspaceRoot = toLinuxPath(policy.workspaceRoot, { distro });
|
|
153
|
-
const
|
|
159
|
+
const prefix = bwrapArgvPrefix({ ...policy, workspaceRoot }, { maskWindowsDrive: this.maskWindowsDrive });
|
|
154
160
|
return {
|
|
155
|
-
argv: [
|
|
161
|
+
argv: [...prefix, ...argv],
|
|
156
162
|
enforcement: ENFORCEMENT,
|
|
157
163
|
denialSignatures: DENIAL_SIGNATURES,
|
|
158
164
|
runnerFailureRules: RUNNER_FAILURE_RULES,
|
|
165
|
+
windowsDrive: this.maskWindowsDrive ? "masked" : "visible",
|
|
159
166
|
};
|
|
160
167
|
}
|
|
161
168
|
}
|
package/lib/shell-env.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
import z from "@deepseek-ai/schemastery";
|
|
27
27
|
import { isWslUnc } from "./paths.js";
|
|
28
28
|
import { defaultDistro, defaultShell, linuxHomePath } from "./wsl.js";
|
|
29
|
+
import { sharedAgent } from "./agent-shared.js";
|
|
30
|
+
import { listeningPorts } from "./ports.js";
|
|
29
31
|
|
|
30
32
|
export const name = "wsl-shell-env";
|
|
31
33
|
|
|
@@ -34,6 +36,8 @@ export const inject = ["shellEnv"];
|
|
|
34
36
|
export const Config = z.object({
|
|
35
37
|
/** Distro these facts describe; empty means this machine's default distro. */
|
|
36
38
|
distro: z.string().default(""),
|
|
39
|
+
/** How often the listening-port snapshot refreshes, in milliseconds. */
|
|
40
|
+
portsRefreshMs: z.number().default(10 * 1000),
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
export async function apply(ctx, config = {}) {
|
|
@@ -56,7 +60,21 @@ export async function apply(ctx, config = {}) {
|
|
|
56
60
|
DSH_WSL_HOME: {
|
|
57
61
|
description: `Home directory of the user model shell calls run as inside ${distro}.`,
|
|
58
62
|
},
|
|
63
|
+
DSH_WSL_PORTS: {
|
|
64
|
+
description: `Ports with a listener inside ${distro} (comma-separated, ascending), as of the last snapshot — refreshed every ${Math.round(config.portsRefreshMs / 1000)}s, so a dev server started moments ago may not be listed yet. Empty means none were listening at the last scan.`,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// The port snapshot refreshes on a timer and is served synchronously: the
|
|
69
|
+
// registry's resolve is sync by contract, so live reads are impossible —
|
|
70
|
+
// the timer bounds staleness instead (see lib/ports.js).
|
|
71
|
+
let ports = [];
|
|
72
|
+
const refreshPorts = async () => {
|
|
73
|
+
ports = await listeningPorts(sharedAgent(distro), 8000);
|
|
59
74
|
};
|
|
75
|
+
void refreshPorts();
|
|
76
|
+
const portsTimer = setInterval(() => void refreshPorts(), config.portsRefreshMs);
|
|
77
|
+
portsTimer.unref?.();
|
|
60
78
|
|
|
61
79
|
const dispose = ctx.shellEnv.register({
|
|
62
80
|
name: "dsh-plugin-wsl-env",
|
|
@@ -64,11 +82,19 @@ export async function apply(ctx, config = {}) {
|
|
|
64
82
|
resolve: (execution) => {
|
|
65
83
|
const cwd = execution?.agent?.session?.header?.cwd;
|
|
66
84
|
if (!isWslUnc(cwd)) return {};
|
|
67
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
DSH_WSL_DISTRO: distro,
|
|
87
|
+
DSH_WSL_SHELL: shell,
|
|
88
|
+
DSH_WSL_HOME: home,
|
|
89
|
+
DSH_WSL_PORTS: ports.join(","),
|
|
90
|
+
};
|
|
68
91
|
},
|
|
69
92
|
});
|
|
70
93
|
|
|
71
94
|
// Disposing matters: a config hot-reload re-runs apply(), and the registry
|
|
72
95
|
// rejects a second contributor claiming the same keys.
|
|
73
|
-
ctx.effect(() =>
|
|
96
|
+
ctx.effect(() => {
|
|
97
|
+
clearInterval(portsTimer);
|
|
98
|
+
dispose();
|
|
99
|
+
});
|
|
74
100
|
}
|
package/lib/subprocess.js
CHANGED
|
@@ -74,6 +74,7 @@ import z from "@deepseek-ai/schemastery";
|
|
|
74
74
|
import { LocalSubprocessRuntime } from "@deepseek-ai/dsh-subprocess-local";
|
|
75
75
|
import { isPosixAbsolute, toLinuxPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
76
76
|
import { DEFAULT_WSL_PATH, defaultDistro, wslEnvValue, wslTerminalArgv } from "./wsl.js";
|
|
77
|
+
import { terminalRoute } from "./terminal-route.js";
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
80
|
* Env names a terminal launch keeps for the distro. `TERM` rides along because
|
|
@@ -120,6 +121,13 @@ export class WslSubprocessRuntime extends LocalSubprocessRuntime {
|
|
|
120
121
|
hostCwd: z.string().volatile(),
|
|
121
122
|
/** Extra env names to forward into the distro through WSLENV. */
|
|
122
123
|
forwardEnv: z.array(z.string()).default([]),
|
|
124
|
+
/**
|
|
125
|
+
* Give a session opened on a WINDOWS folder a host shell instead of the
|
|
126
|
+
* distro's shell started at `/mnt/<drive>/…`. WSL-folder sessions always
|
|
127
|
+
* get the distro shell — that is this provider's purpose. `false` restores
|
|
128
|
+
* the composition-owned terminal for every session.
|
|
129
|
+
*/
|
|
130
|
+
hostSessions: z.boolean().default(true),
|
|
123
131
|
});
|
|
124
132
|
|
|
125
133
|
constructor(ctx, config) {
|
|
@@ -185,6 +193,13 @@ export class WslSubprocessRuntime extends LocalSubprocessRuntime {
|
|
|
185
193
|
*/
|
|
186
194
|
async spawnTerminal(spec) {
|
|
187
195
|
if (!this.launchesDistro(spec.argv)) return super.spawnTerminal(spec);
|
|
196
|
+
// A session on a Windows folder is a HOST session: it gets the host shell
|
|
197
|
+
// in its own directory, not the distro's shell at `/mnt/<drive>/…`. The
|
|
198
|
+
// wsl.exe argv still names this provider's launch, so the substitution
|
|
199
|
+
// stays inside the override — every other consumer is untouched.
|
|
200
|
+
if (this.config.hostSessions && terminalRoute(spec.cwd) === "host") {
|
|
201
|
+
return super.spawnTerminal({ ...spec, argv: ["powershell.exe", "-NoLogo"] });
|
|
202
|
+
}
|
|
188
203
|
const target = await this.terminalTarget(spec.cwd);
|
|
189
204
|
const argv = wslTerminalArgv({
|
|
190
205
|
wslPath: this.config.wslPath,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which execution world a terminal launch belongs to.
|
|
3
|
+
*
|
|
4
|
+
* The terminal controller hands this provider the request's directory, and the
|
|
5
|
+
* directory's coordinate system is the evidence of intent: a UNC path under a
|
|
6
|
+
* distro's share names a WSL session outright, a POSIX absolute path is a
|
|
7
|
+
* distro path by construction, and a Windows drive path is a session opened on
|
|
8
|
+
* a Windows folder — which deserves a HOST shell, not the distro's shell
|
|
9
|
+
* started at `/mnt/<drive>/…`. An empty directory carries no evidence either
|
|
10
|
+
* way and defaults to the distro, which is what a profile pins.
|
|
11
|
+
*
|
|
12
|
+
* Pure by construction.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-plugin-wsl/terminal-route
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isPosixAbsolute, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param cwd - the terminal request's working directory, any coordinate system.
|
|
21
|
+
* @returns `"distro"` when the launch belongs inside the pinned distro,
|
|
22
|
+
* `"host"` when it belongs to a Windows-folder session.
|
|
23
|
+
*/
|
|
24
|
+
export function terminalRoute(cwd) {
|
|
25
|
+
const text = String(cwd ?? "").trim();
|
|
26
|
+
if (text.length === 0) return "distro";
|
|
27
|
+
if (uncToPosix(text) !== undefined) return "distro";
|
|
28
|
+
if (isPosixAbsolute(text)) return "distro";
|
|
29
|
+
if (windowsToLinuxMount(text) !== undefined) return "host";
|
|
30
|
+
return "distro";
|
|
31
|
+
}
|
package/lib/watcher.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-distro file watching for `WslFileSystem.watch`.
|
|
3
|
+
*
|
|
4
|
+
* A 9p share gives Node no usable change notification — chokidar over
|
|
5
|
+
* `\\wsl.localhost` is the unreliability that made this provider refuse to
|
|
6
|
+
* watch at all. The seam's contract, however, asks for far less than events:
|
|
7
|
+
* `watch(target, changed, signal)` wants ONE invalidation callback, invoked
|
|
8
|
+
* whenever something under `target` may have changed. That is cheap to honor
|
|
9
|
+
* from inside the distro, where the kernel does know.
|
|
10
|
+
*
|
|
11
|
+
* The watcher is a long-lived `wsl.exe` shell loop: periodically
|
|
12
|
+
* `find <dir> -newer <stamp>` — the directory itself is in the scan, so
|
|
13
|
+
* creations AND deletions (which bump the parent's mtime) are both seen — and
|
|
14
|
+
* one `C` line per hit. No `inotifywait` dependency; the mtime walk is exact
|
|
15
|
+
* for interactive-paced edits, which is the only pace a session watch serves.
|
|
16
|
+
*
|
|
17
|
+
* Known blind spot, accepted and documented: a file created with an mtime
|
|
18
|
+
* older than the stamp (`cp -p`, `tar -x`) is not seen until something else
|
|
19
|
+
* touches the tree.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-plugin-wsl/watcher
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import { createInterface } from "node:readline";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The shell loop the watcher runs, parameterised by argv so no path can break
|
|
29
|
+
* quoting: `$1` is the Linux directory, `$2` the poll interval in seconds.
|
|
30
|
+
* Pure text, so it is unit-testable.
|
|
31
|
+
* @param {number} [intervalSeconds] - poll cadence; must be >= 1.
|
|
32
|
+
* @returns {string} the `sh -c` script body.
|
|
33
|
+
*/
|
|
34
|
+
export function watchLoopScript(intervalSeconds = 2) {
|
|
35
|
+
const seconds = Math.max(1, Math.floor(intervalSeconds));
|
|
36
|
+
// `find` lists the directory itself first: a child's creation or deletion
|
|
37
|
+
// bumps the parent's mtime, so both edges are seen, not just modifications.
|
|
38
|
+
return [
|
|
39
|
+
'D="$1"',
|
|
40
|
+
'S="${TMPDIR:-/tmp}/wsl-watch.$$.stamp"',
|
|
41
|
+
'touch "$S"',
|
|
42
|
+
// The READY line is the activation barrier: everything changed BEFORE it
|
|
43
|
+
// predates the stamp, so the host must not resolve the watch promise until
|
|
44
|
+
// it arrives — otherwise a write racing the first tick is invisible forever.
|
|
45
|
+
'printf \'R\\n\'',
|
|
46
|
+
// Check first, sleep second: a change made right after activation is seen
|
|
47
|
+
// on the first cadence, not one cadence later.
|
|
48
|
+
'while :; do',
|
|
49
|
+
' if [ -n "$(find "$D" -newer "$S" -print -quit 2>/dev/null)" ]; then',
|
|
50
|
+
' touch "$S"',
|
|
51
|
+
' printf \'C\\n\'',
|
|
52
|
+
' fi',
|
|
53
|
+
` sleep ${seconds}`,
|
|
54
|
+
'done',
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Arm one in-distro watcher over a Linux directory.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} options - the watch request.
|
|
62
|
+
* @param {string} options.wslPath - the `wsl.exe` path.
|
|
63
|
+
* @param {string} options.distro - the distro to watch inside.
|
|
64
|
+
* @param {string} options.linuxPath - the absolute Linux path to observe.
|
|
65
|
+
* @param {(error: Error) => void} [options.onError] - watcher-level failures
|
|
66
|
+
* (an exited loop); the seam reports these through its own channel.
|
|
67
|
+
* @param {() => void} options.onChange - the invalidation callback.
|
|
68
|
+
* @param {AbortSignal} options.signal - cancels initialization; the CALLER
|
|
69
|
+
* invokes the returned close to stop an armed watcher.
|
|
70
|
+
* @param {number} [options.intervalSeconds] - poll cadence, default 2.
|
|
71
|
+
* @param {(wslPath: string, args: string[]) => import("node:child_process").ChildProcess} [options.spawn]
|
|
72
|
+
* injectable process factory for tests.
|
|
73
|
+
* @returns {Promise<() => Promise<void>>} resolves once observation is active,
|
|
74
|
+
* with the async close function the seam's contract asks for.
|
|
75
|
+
*/
|
|
76
|
+
export function armDistroWatcher({ wslPath, distro, linuxPath, onChange, onError, signal, intervalSeconds = 2, spawn: spawnProcess }) {
|
|
77
|
+
const doSpawn = spawnProcess ?? ((path, args) => spawn(path, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }));
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
if (signal?.aborted) {
|
|
80
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const child = doSpawn(wslPath, ["-d", distro, "--exec", "sh", "-c", watchLoopScript(intervalSeconds), "wsl-watch", linuxPath]);
|
|
84
|
+
let settled = false;
|
|
85
|
+
let closed = false;
|
|
86
|
+
const lines = createInterface({ input: child.stdout });
|
|
87
|
+
lines.on("line", (line) => {
|
|
88
|
+
if (line === "R") {
|
|
89
|
+
if (!settled && !signal?.aborted) {
|
|
90
|
+
settled = true;
|
|
91
|
+
resolve(close);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (line === "C") {
|
|
96
|
+
onChange();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const close = async () => {
|
|
100
|
+
if (closed) return;
|
|
101
|
+
closed = true;
|
|
102
|
+
child.removeAllListeners("exit");
|
|
103
|
+
lines.close();
|
|
104
|
+
child.stdout.destroy();
|
|
105
|
+
child.kill();
|
|
106
|
+
await new Promise((resolveClose) => {
|
|
107
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolveClose();
|
|
108
|
+
child.once("exit", resolveClose);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
const fail = (error) => {
|
|
112
|
+
if (settled) {
|
|
113
|
+
onError?.(error);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
settled = true;
|
|
117
|
+
void close();
|
|
118
|
+
reject(error);
|
|
119
|
+
};
|
|
120
|
+
signal?.addEventListener("abort", () => fail(signal.reason instanceof Error ? signal.reason : new Error("aborted")), { once: true });
|
|
121
|
+
child.on("error", fail);
|
|
122
|
+
child.on("exit", (code, signalName) => {
|
|
123
|
+
if (!closed) fail(new Error(`distro watcher exited unexpectedly (code=${code} signal=${signalName ?? "none"})`));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
});
|
|
127
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wsl-env",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "让 DeepSeek Harness 以 WSL 子系统作为会话的执行环境:shell、文件读写与命令执行均在子系统内完成,目录选择器与 GUI 终端同样定位到子系统路径,并通过子系统内的 bubblewrap 对命令进行沙箱隔离",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./shell-env": "./lib/shell-env.js",
|
|
23
23
|
"./wsl": "./lib/wsl.js",
|
|
24
24
|
"./paths": "./lib/paths.js",
|
|
25
|
+
"./agent": "./lib/agent.js",
|
|
25
26
|
"./package.json": "./package.json"
|
|
26
27
|
},
|
|
27
28
|
"dsh": {
|
|
@@ -31,12 +32,16 @@
|
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"lib/**/*.js",
|
|
35
|
+
"agent/*.sh",
|
|
34
36
|
"cordis.patch.yml",
|
|
35
37
|
"examples/**/*.yml",
|
|
36
38
|
"README.md",
|
|
37
39
|
"docs/README.zh.md",
|
|
38
40
|
"docs/CONFIGURATION.md",
|
|
39
41
|
"docs/LIMITATIONS.md",
|
|
42
|
+
"docs/PARITY.md",
|
|
43
|
+
"docs/PEER-PARITY.md",
|
|
44
|
+
"docs/UPSTREAM-FSIO-EXPORT.md",
|
|
40
45
|
"docs/ARCHITECTURE.md",
|
|
41
46
|
"CHANGELOG.md",
|
|
42
47
|
"CONTRIBUTING.md",
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
"test": "npm run lint:style && npm run test:syntax && npm run test:unit",
|
|
49
54
|
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=85 --test-coverage-functions=70 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",
|
|
50
55
|
"test:syntax": "node test/syntax.mjs",
|
|
51
|
-
"test:unit": "node --test 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",
|
|
56
|
+
"test:unit": "node --test 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/fs-publish.test.mjs test/agent-exec.test.mjs test/ports.test.mjs test/watcher.test.mjs",
|
|
52
57
|
"lint:style": "node test/style.mjs",
|
|
53
58
|
"prepublishOnly": "npm test",
|
|
54
59
|
"probe": "bash test/probe/run.sh",
|
|
@@ -59,7 +64,11 @@
|
|
|
59
64
|
"probe:terminal": "bash test/probe/terminal.sh",
|
|
60
65
|
"probe:sandbox": "bash test/probe/sandbox.sh",
|
|
61
66
|
"probe:sandbox-shell": "bash test/probe/sandbox-shell.sh",
|
|
62
|
-
"sync:windows": "bash test/probe/sync-to-windows.sh"
|
|
67
|
+
"sync:windows": "bash test/probe/sync-to-windows.sh",
|
|
68
|
+
"probe:watch": "bash test/probe/watch.sh",
|
|
69
|
+
"probe:substrate": "bash test/probe/substrate.sh",
|
|
70
|
+
"probe:exec": "bash test/probe/exec.sh",
|
|
71
|
+
"bootstrap": "bash scripts/bootstrap.sh"
|
|
63
72
|
},
|
|
64
73
|
"engines": {
|
|
65
74
|
"node": ">=20"
|