dsh-plugin-wsl-env 0.15.0 → 0.17.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 +144 -15
- package/CONTRIBUTING.md +25 -33
- package/README.md +10 -10
- package/SECURITY.md +6 -4
- package/SUPPORT.md +8 -8
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +51 -12
- package/cordis.patch.yml +23 -10
- package/docs/ARCHITECTURE.md +56 -41
- package/docs/COMPATIBILITY.md +121 -0
- package/docs/CONFIGURATION.md +275 -273
- package/docs/LIMITATIONS.md +114 -38
- package/docs/MIGRATION.md +16 -13
- package/docs/PARITY.md +4 -3
- package/docs/PERFORMANCE.md +11 -9
- package/docs/QUICKSTART.md +46 -21
- package/docs/README.zh.md +7 -7
- package/docs/RELEASING.md +257 -0
- package/docs/REQUIREMENTS.zh.md +77 -0
- package/docs/ROADMAP-1.0.md +105 -61
- package/docs/ROADMAP.md +15 -9
- package/docs/UPSTREAM-FILE-REFERENCE-SEAM.md +3 -3
- package/docs/UPSTREAM-FSIO-EXPORT.md +7 -7
- package/docs/UPSTREAM-OPEN-IN-APP.md +57 -0
- package/docs/UPSTREAM-SESSION-NOTICE.md +4 -4
- package/docs/UPSTREAM-TERMINAL-TITLE.md +10 -12
- package/docs/VALUE.zh.md +144 -0
- package/docs/adr/0002-native-agent.md +348 -0
- package/docs/root-fs-routing.md +186 -0
- package/docs/screenshots/picker.png +0 -0
- package/docs/screenshots/preview.png +0 -0
- package/docs/screenshots/sandbox.png +0 -0
- package/docs/screenshots/terminal.png +0 -0
- package/lib/agent-exec.d.ts +7 -3
- package/lib/agent-protocol.d.ts +18 -5
- package/lib/agent-protocol.js +11 -6
- package/lib/agent-shared.js +1 -0
- package/lib/agent.d.ts +28 -9
- package/lib/agent.js +74 -14
- package/lib/fs-routing.d.ts +2 -1
- package/lib/fs-routing.js +13 -1
- package/lib/fs-substrate.d.ts +11 -9
- package/lib/fs-substrate.js +6 -9
- package/lib/fsio-agent.d.ts +2 -3
- package/lib/fsio-agent.js +2 -3
- package/lib/git-exec.d.ts +117 -0
- package/lib/git-exec.js +105 -0
- package/lib/git-route.d.ts +43 -13
- package/lib/git-route.js +54 -15
- package/lib/index.d.ts +20 -13
- package/lib/index.js +18 -11
- package/lib/session-runtime.d.ts +5 -3
- package/lib/session-runtime.js +5 -4
- package/lib/subprocess.d.ts +21 -7
- package/lib/subprocess.js +63 -9
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -4
- package/scripts/diagnose.sh +1 -1
- package/scripts/doctor.mjs +23 -3
package/lib/index.d.ts
CHANGED
|
@@ -407,6 +407,8 @@ export declare class WslShellExecutor extends LocalBashExecutor {
|
|
|
407
407
|
sessionRuntimes: SessionRuntimes;
|
|
408
408
|
/** The runtimes this executor holds, by registry key. */
|
|
409
409
|
sessionRuntimeEntries: Map<string, SessionRuntimeEntry>;
|
|
410
|
+
/** Isolates calls that have no session identity. */
|
|
411
|
+
sessionRuntimeOwner: string;
|
|
410
412
|
/** The preflight's host runner; tests substitute it. */
|
|
411
413
|
sessionScopeProbe: (argv: string[]) => Promise<string>;
|
|
412
414
|
/** Distros whose session-runtime preflight has succeeded. */
|
|
@@ -491,7 +493,7 @@ export declare class WslShellExecutor extends LocalBashExecutor {
|
|
|
491
493
|
executionAgent(distro: string): WslAgent;
|
|
492
494
|
/**
|
|
493
495
|
* The persistent session runtime for one policy, when `sessionSandbox` is
|
|
494
|
-
* on. The executor holds one reference per `(distro, workspace, policy)` and
|
|
496
|
+
* on. The executor holds one reference per `(session, distro, workspace, policy)` and
|
|
495
497
|
* releases them when the session's scope disposes.
|
|
496
498
|
*
|
|
497
499
|
* @param policy - the resolved file-effect policy, when the executor has one.
|
|
@@ -502,6 +504,7 @@ export declare class WslShellExecutor extends LocalBashExecutor {
|
|
|
502
504
|
sessionRuntimeFor(policy: {
|
|
503
505
|
mode: string;
|
|
504
506
|
workspaceRoot: string;
|
|
507
|
+
sessionId?: string;
|
|
505
508
|
} | undefined, resolved: WslExecSpec, distro: string): SessionRuntimeEntry | undefined;
|
|
506
509
|
/**
|
|
507
510
|
* Run the session runtime's preflight once per executor. A failure is a
|
|
@@ -815,11 +818,11 @@ export declare class WslFileSystem extends LocalFileSystem {
|
|
|
815
818
|
sandbox: z<boolean, boolean, "defined">;
|
|
816
819
|
/**
|
|
817
820
|
* Run confined mutations in the session's systemd user scope — the same
|
|
818
|
-
* runtime the shell executor acquires for the same
|
|
819
|
-
* policy
|
|
820
|
-
*
|
|
821
|
-
* the
|
|
822
|
-
*
|
|
821
|
+
* runtime the shell executor acquires for the same session, distro,
|
|
822
|
+
* workspace and policy — instead of a per-policy confined resident. A
|
|
823
|
+
* missing session ID keeps the providers separate. The kernel enforcement
|
|
824
|
+
* is the runtime's mount profile; the scope stops when its providers
|
|
825
|
+
* release it. Requires systemd + `Linger`, like `wsl-shell`'s
|
|
823
826
|
* `sessionSandbox`; the preflight refuses with the remedy. Off for direct
|
|
824
827
|
* constructions; on in the shipped preset.
|
|
825
828
|
*/
|
|
@@ -890,11 +893,11 @@ export declare class WslFileSystem extends LocalFileSystem {
|
|
|
890
893
|
sandbox: z<boolean, boolean, "defined">;
|
|
891
894
|
/**
|
|
892
895
|
* Run confined mutations in the session's systemd user scope — the same
|
|
893
|
-
* runtime the shell executor acquires for the same
|
|
894
|
-
* policy
|
|
895
|
-
*
|
|
896
|
-
* the
|
|
897
|
-
*
|
|
896
|
+
* runtime the shell executor acquires for the same session, distro,
|
|
897
|
+
* workspace and policy — instead of a per-policy confined resident. A
|
|
898
|
+
* missing session ID keeps the providers separate. The kernel enforcement
|
|
899
|
+
* is the runtime's mount profile; the scope stops when its providers
|
|
900
|
+
* release it. Requires systemd + `Linger`, like `wsl-shell`'s
|
|
898
901
|
* `sessionSandbox`; the preflight refuses with the remedy. Off for direct
|
|
899
902
|
* constructions; on in the shipped preset.
|
|
900
903
|
*/
|
|
@@ -962,6 +965,8 @@ export declare class WslFileSystem extends LocalFileSystem {
|
|
|
962
965
|
sessionRuntimes: SessionRuntimes;
|
|
963
966
|
/** The runtimes this provider holds, by registry key. */
|
|
964
967
|
sessionRuntimeEntries: Map<string, SessionRuntimeEntry>;
|
|
968
|
+
/** Isolates calls that have no session identity. */
|
|
969
|
+
sessionRuntimeOwner: string;
|
|
965
970
|
/** The preflight's host runner; tests substitute it. */
|
|
966
971
|
sessionScopeProbe: (argv: string[]) => Promise<string>;
|
|
967
972
|
/** Distros whose session-runtime preflight has succeeded. */
|
|
@@ -1038,6 +1043,7 @@ export declare class WslFileSystem extends LocalFileSystem {
|
|
|
1038
1043
|
sessionRuntimeFor(policy: {
|
|
1039
1044
|
mode: string;
|
|
1040
1045
|
workspaceRoot: string;
|
|
1046
|
+
sessionId?: string;
|
|
1041
1047
|
}, distro: string): SessionRuntimeEntry | undefined;
|
|
1042
1048
|
/**
|
|
1043
1049
|
* Run the session runtime's preflight once per provider. Only success is
|
|
@@ -1082,6 +1088,7 @@ export declare class WslFileSystem extends LocalFileSystem {
|
|
|
1082
1088
|
mutationAgent(policy: {
|
|
1083
1089
|
mode: string;
|
|
1084
1090
|
workspaceRoot: string;
|
|
1091
|
+
sessionId?: string;
|
|
1085
1092
|
} | undefined, forDistro?: string): Promise<WslAgent>;
|
|
1086
1093
|
/**
|
|
1087
1094
|
* Map a substrate error onto the peer's `FsError`, so the tool layer reads
|
|
@@ -1292,8 +1299,8 @@ export declare class WslFileSystem extends LocalFileSystem {
|
|
|
1292
1299
|
* Fence the write by the per-call policy, then publish through the
|
|
1293
1300
|
* substrate. The fence's fresh target is the one checked and written, so a
|
|
1294
1301
|
* refusal costs no I/O and cannot be raced by a swapped symlink; the guard
|
|
1295
|
-
* rides the write op, where the agent re-verifies it
|
|
1296
|
-
*
|
|
1302
|
+
* rides the write op, where the agent re-verifies it before the rename;
|
|
1303
|
+
* the confined resident enforces the mount boundary in the kernel.
|
|
1297
1304
|
*
|
|
1298
1305
|
* @param target - the resolved target.
|
|
1299
1306
|
* @param content - the full new file content.
|
package/lib/index.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
* @module dsh-plugin-wsl
|
|
41
41
|
*/
|
|
42
42
|
import z from "@deepseek-ai/schemastery";
|
|
43
|
+
import { randomUUID } from "node:crypto";
|
|
43
44
|
import { LocalBashExecutor } from "@deepseek-ai/dsh-bash-local";
|
|
44
45
|
import { LocalFileSystem } from "@deepseek-ai/dsh-fs-local";
|
|
45
46
|
import { FsError } from "@deepseek-ai/dsh-fs";
|
|
@@ -355,6 +356,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
355
356
|
this.processFacts = new Map();
|
|
356
357
|
this.sessionRuntimes = sessionRuntimes;
|
|
357
358
|
this.sessionRuntimeEntries = new Map();
|
|
359
|
+
this.sessionRuntimeOwner = randomUUID();
|
|
358
360
|
this.sessionScopeProbe = (argv) => runCapture(argv);
|
|
359
361
|
this.sessionRuntimePreflight = new Set();
|
|
360
362
|
this.autoSessionScope = new Map();
|
|
@@ -485,7 +487,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
485
487
|
}
|
|
486
488
|
/**
|
|
487
489
|
* The persistent session runtime for one policy, when `sessionSandbox` is
|
|
488
|
-
* on. The executor holds one reference per `(distro, workspace, policy)` and
|
|
490
|
+
* on. The executor holds one reference per `(session, distro, workspace, policy)` and
|
|
489
491
|
* releases them when the session's scope disposes.
|
|
490
492
|
*
|
|
491
493
|
* @param policy - the resolved file-effect policy, when the executor has one.
|
|
@@ -499,11 +501,13 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
499
501
|
return undefined;
|
|
500
502
|
const mode = policy?.mode ?? "danger-full-access";
|
|
501
503
|
const workspaceRoot = policy?.workspaceRoot ?? (configuredCwd(this.config) || resolved.workdir);
|
|
502
|
-
const
|
|
504
|
+
const sessionId = policy?.sessionId || resolved.dshEnv?.DSH_SESSION_ID || this.sessionRuntimeOwner;
|
|
505
|
+
const key = SessionRuntimes.key({ sessionId, distro, workspaceRoot, mode, maskWindowsDrive: effectiveMaskWindowsDrive(config) });
|
|
503
506
|
const held = this.sessionRuntimeEntries.get(key);
|
|
504
507
|
if (held !== undefined)
|
|
505
508
|
return held;
|
|
506
509
|
const entry = this.sessionRuntimes.acquire({
|
|
510
|
+
sessionId,
|
|
507
511
|
distro,
|
|
508
512
|
wslPath: config.wslPath,
|
|
509
513
|
workspaceRoot,
|
|
@@ -876,7 +880,7 @@ export class WslShellExecutor extends LocalBashExecutor {
|
|
|
876
880
|
: confined !== undefined ? confined.argv : this.distroArgv(resolved, shell);
|
|
877
881
|
// The managed `DSH_*` snapshot merges last, exactly as the one-shot path's
|
|
878
882
|
// spawn env does (`ShellExecSpec`: `dshEnv` wins over `env`). The agent
|
|
879
|
-
// applies these
|
|
883
|
+
// applies these to the individual command, so
|
|
880
884
|
// the resident path now carries `DSH_HOME`, `DSH_SESSION_ID` and the
|
|
881
885
|
// `DSH_WSL_*` facts too — without this the whole registry was invisible on
|
|
882
886
|
// the default path while the one-shot fallback forwarded it.
|
|
@@ -1074,11 +1078,11 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1074
1078
|
sandbox: z.boolean().default(true),
|
|
1075
1079
|
/**
|
|
1076
1080
|
* Run confined mutations in the session's systemd user scope — the same
|
|
1077
|
-
* runtime the shell executor acquires for the same
|
|
1078
|
-
* policy
|
|
1079
|
-
*
|
|
1080
|
-
* the
|
|
1081
|
-
*
|
|
1081
|
+
* runtime the shell executor acquires for the same session, distro,
|
|
1082
|
+
* workspace and policy — instead of a per-policy confined resident. A
|
|
1083
|
+
* missing session ID keeps the providers separate. The kernel enforcement
|
|
1084
|
+
* is the runtime's mount profile; the scope stops when its providers
|
|
1085
|
+
* release it. Requires systemd + `Linger`, like `wsl-shell`'s
|
|
1082
1086
|
* `sessionSandbox`; the preflight refuses with the remedy. Off for direct
|
|
1083
1087
|
* constructions; on in the shipped preset.
|
|
1084
1088
|
*/
|
|
@@ -1147,6 +1151,7 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1147
1151
|
this.agentInstances = new Map();
|
|
1148
1152
|
this.sessionRuntimes = sessionRuntimes;
|
|
1149
1153
|
this.sessionRuntimeEntries = new Map();
|
|
1154
|
+
this.sessionRuntimeOwner = randomUUID();
|
|
1150
1155
|
this.sessionScopeProbe = (argv) => runCapture(argv);
|
|
1151
1156
|
this.sessionRuntimePreflight = new Set();
|
|
1152
1157
|
this.autoSessionScope = new Map();
|
|
@@ -1263,11 +1268,13 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1263
1268
|
if (!config.sessionSandbox)
|
|
1264
1269
|
return undefined;
|
|
1265
1270
|
const workspaceRoot = policy.workspaceRoot || config.cwd;
|
|
1266
|
-
const
|
|
1271
|
+
const sessionId = policy.sessionId || this.sessionRuntimeOwner;
|
|
1272
|
+
const key = SessionRuntimes.key({ sessionId, distro, workspaceRoot, mode: policy.mode, maskWindowsDrive: config.maskWindowsDrive });
|
|
1267
1273
|
const held = this.sessionRuntimeEntries.get(key);
|
|
1268
1274
|
if (held !== undefined)
|
|
1269
1275
|
return held;
|
|
1270
1276
|
const entry = this.sessionRuntimes.acquire({
|
|
1277
|
+
sessionId,
|
|
1271
1278
|
distro,
|
|
1272
1279
|
wslPath: config.wslPath,
|
|
1273
1280
|
workspaceRoot,
|
|
@@ -1751,8 +1758,8 @@ export class WslFileSystem extends LocalFileSystem {
|
|
|
1751
1758
|
* Fence the write by the per-call policy, then publish through the
|
|
1752
1759
|
* substrate. The fence's fresh target is the one checked and written, so a
|
|
1753
1760
|
* refusal costs no I/O and cannot be raced by a swapped symlink; the guard
|
|
1754
|
-
* rides the write op, where the agent re-verifies it
|
|
1755
|
-
*
|
|
1761
|
+
* rides the write op, where the agent re-verifies it before the rename;
|
|
1762
|
+
* the confined resident enforces the mount boundary in the kernel.
|
|
1756
1763
|
*
|
|
1757
1764
|
* @param target - the resolved target.
|
|
1758
1765
|
* @param content - the full new file content.
|
package/lib/session-runtime.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session-scoped runtime registry (ADR 0001).
|
|
3
3
|
*
|
|
4
|
-
* One long-lived agent per `(distro, workspaceRoot, policy)`, shared by the
|
|
4
|
+
* One long-lived agent per `(sessionId, distro, workspaceRoot, policy)`, shared by the
|
|
5
5
|
* providers that need it and refcounted: the first `acquire` spawns the agent
|
|
6
6
|
* inside a systemd user scope, the last `release` closes the agent and stops
|
|
7
7
|
* the scope (the cgroup then reaps any background dev server a command left
|
|
@@ -43,6 +43,8 @@ export declare function probeSessionRuntime({ wslPath, distro, run }: {
|
|
|
43
43
|
}): Promise<string | null>;
|
|
44
44
|
/** One request for a session runtime. */
|
|
45
45
|
export interface SessionRuntimeRequest {
|
|
46
|
+
/** The session identity; absent requests each get a separate runtime. */
|
|
47
|
+
sessionId?: string;
|
|
46
48
|
/** The distro the runtime lives in. */
|
|
47
49
|
distro: string;
|
|
48
50
|
/** The `wsl.exe` path; defaults to `wsl.exe`. */
|
|
@@ -125,14 +127,14 @@ export declare class SessionRuntimes {
|
|
|
125
127
|
*/
|
|
126
128
|
constructor(options?: SessionRuntimeOptions);
|
|
127
129
|
/**
|
|
128
|
-
* The key a request addresses: distro, workspace, mode and mask. A policy
|
|
130
|
+
* The key a request addresses: session, distro, workspace, mode and mask. A policy
|
|
129
131
|
* change is a different runtime, because the mount profile is fixed for the
|
|
130
132
|
* agent's lifetime.
|
|
131
133
|
*
|
|
132
134
|
* @param request - the runtime request.
|
|
133
135
|
* @returns the registry key.
|
|
134
136
|
*/
|
|
135
|
-
static key(request: Pick<SessionRuntimeRequest, "distro" | "workspaceRoot" | "mode" | "maskWindowsDrive">): string;
|
|
137
|
+
static key(request: Pick<SessionRuntimeRequest, "distro" | "workspaceRoot" | "mode" | "maskWindowsDrive" | "sessionId">): string;
|
|
136
138
|
/**
|
|
137
139
|
* Acquire the runtime for one key, creating it on first use.
|
|
138
140
|
*
|
package/lib/session-runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The session-scoped runtime registry (ADR 0001).
|
|
3
3
|
*
|
|
4
|
-
* One long-lived agent per `(distro, workspaceRoot, policy)`, shared by the
|
|
4
|
+
* One long-lived agent per `(sessionId, distro, workspaceRoot, policy)`, shared by the
|
|
5
5
|
* providers that need it and refcounted: the first `acquire` spawns the agent
|
|
6
6
|
* inside a systemd user scope, the last `release` closes the agent and stops
|
|
7
7
|
* the scope (the cgroup then reaps any background dev server a command left
|
|
@@ -83,6 +83,7 @@ const spawnSessionAgent = (options) => {
|
|
|
83
83
|
idleMs: options.idleMs,
|
|
84
84
|
leaseMs: options.leaseMs,
|
|
85
85
|
signalable: false,
|
|
86
|
+
serialRequests: false,
|
|
86
87
|
spawnTransport: ({ wslPath, distro, argvPrefix }) => spawn(wslPath, ["-d", distro, "--exec", ...argvPrefix, native.path], {
|
|
87
88
|
stdio: ["pipe", "pipe", "pipe"],
|
|
88
89
|
windowsHide: true,
|
|
@@ -137,7 +138,7 @@ export class SessionRuntimes {
|
|
|
137
138
|
this.token = options.token ?? defaultToken;
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
140
|
-
* The key a request addresses: distro, workspace, mode and mask. A policy
|
|
141
|
+
* The key a request addresses: session, distro, workspace, mode and mask. A policy
|
|
141
142
|
* change is a different runtime, because the mount profile is fixed for the
|
|
142
143
|
* agent's lifetime.
|
|
143
144
|
*
|
|
@@ -145,7 +146,7 @@ export class SessionRuntimes {
|
|
|
145
146
|
* @returns the registry key.
|
|
146
147
|
*/
|
|
147
148
|
static key(request) {
|
|
148
|
-
return [request.distro, request.workspaceRoot, request.mode, request.maskWindowsDrive === true ? "masked" : "visible"].join("\u0000");
|
|
149
|
+
return [request.sessionId ?? "", request.distro, request.workspaceRoot, request.mode, request.maskWindowsDrive === true ? "masked" : "visible"].join("\u0000");
|
|
149
150
|
}
|
|
150
151
|
/**
|
|
151
152
|
* Acquire the runtime for one key, creating it on first use.
|
|
@@ -154,7 +155,7 @@ export class SessionRuntimes {
|
|
|
154
155
|
* @returns the live entry; the caller holds one reference.
|
|
155
156
|
*/
|
|
156
157
|
acquire(request) {
|
|
157
|
-
const key = SessionRuntimes.key(request);
|
|
158
|
+
const key = SessionRuntimes.key({ ...request, sessionId: request.sessionId || defaultToken() });
|
|
158
159
|
const existing = this.runtimes.get(key);
|
|
159
160
|
if (existing !== undefined) {
|
|
160
161
|
existing.refs += 1;
|
package/lib/subprocess.d.ts
CHANGED
|
@@ -60,13 +60,17 @@
|
|
|
60
60
|
*
|
|
61
61
|
* ## What the operator must pair it with
|
|
62
62
|
*
|
|
63
|
-
* `terminalEnvironment()`
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
63
|
+
* `terminalEnvironment()` is also resolved from the root realm and receives no
|
|
64
|
+
* Session or directory, so this provider deliberately leaves it alone:
|
|
65
|
+
* rewriting it would change environment lookup for every other root consumer
|
|
66
|
+
* to make the *shell menu* of one window prettier. `resolveExecutable()` gets
|
|
67
|
+
* ONE scoped exception — a git fallback when the host has none (see the
|
|
68
|
+
* override below); every other name's lookup is untouched. The profile also
|
|
69
|
+
* configures the terminal controller with the distro shell as the default
|
|
70
|
+
* profile plus deliberate host candidates: `shell: { path: wsl.exe, name: WSL }`
|
|
71
|
+
* and `shellCandidates: [wsl.exe, powershell, cmd]` — the first dedupes into
|
|
72
|
+
* the default's `wsl` kind, and the other two are launches this terminal really
|
|
73
|
+
* starts (host sessions, or an explicit user selection, titled after itself).
|
|
70
74
|
*
|
|
71
75
|
* Consequence worth stating plainly: the terminal is a property of the
|
|
72
76
|
* composition, not of the Session. A Session whose workspace is a Windows folder
|
|
@@ -297,6 +301,16 @@ export declare class WslSubprocessRuntime extends LocalSubprocessRuntime {
|
|
|
297
301
|
* @returns the live search handle.
|
|
298
302
|
*/
|
|
299
303
|
spawn(spec: SubprocessSpawnSpec): SubprocessHandle;
|
|
304
|
+
/**
|
|
305
|
+
* Guarded git fallback: when the HOST has no git, resolve the name to the
|
|
306
|
+
* distro sentinel so the change-review pipeline's every spawn (discovery
|
|
307
|
+
* included) routes into the distro instead of dying at the executable
|
|
308
|
+
* lookup. Scoped to the one name — every other consumer's lookup (pwsh,
|
|
309
|
+
* LSP, ripgrep) is untouched — and only on the super's failure, so a
|
|
310
|
+
* machine with Windows git behaves exactly as shipped. See
|
|
311
|
+
* `git-route.ts`'s `DISTRO_GIT_SENTINEL` for the routing side.
|
|
312
|
+
*/
|
|
313
|
+
resolveExecutable(command: string, env: Record<string, string> | undefined, signal: AbortSignal | undefined): Promise<string>;
|
|
300
314
|
/**
|
|
301
315
|
* The environment one `wsl.exe` launch forwards: the safe list plus every
|
|
302
316
|
* managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
|
package/lib/subprocess.js
CHANGED
|
@@ -60,13 +60,17 @@
|
|
|
60
60
|
*
|
|
61
61
|
* ## What the operator must pair it with
|
|
62
62
|
*
|
|
63
|
-
* `terminalEnvironment()`
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
63
|
+
* `terminalEnvironment()` is also resolved from the root realm and receives no
|
|
64
|
+
* Session or directory, so this provider deliberately leaves it alone:
|
|
65
|
+
* rewriting it would change environment lookup for every other root consumer
|
|
66
|
+
* to make the *shell menu* of one window prettier. `resolveExecutable()` gets
|
|
67
|
+
* ONE scoped exception — a git fallback when the host has none (see the
|
|
68
|
+
* override below); every other name's lookup is untouched. The profile also
|
|
69
|
+
* configures the terminal controller with the distro shell as the default
|
|
70
|
+
* profile plus deliberate host candidates: `shell: { path: wsl.exe, name: WSL }`
|
|
71
|
+
* and `shellCandidates: [wsl.exe, powershell, cmd]` — the first dedupes into
|
|
72
|
+
* the default's `wsl` kind, and the other two are launches this terminal really
|
|
73
|
+
* starts (host sessions, or an explicit user selection, titled after itself).
|
|
70
74
|
*
|
|
71
75
|
* Consequence worth stating plainly: the terminal is a property of the
|
|
72
76
|
* composition, not of the Session. A Session whose workspace is a Windows folder
|
|
@@ -79,13 +83,15 @@
|
|
|
79
83
|
* @module dsh-plugin-wsl/subprocess
|
|
80
84
|
*/
|
|
81
85
|
import { randomUUID } from "node:crypto";
|
|
86
|
+
import { Buffer } from "node:buffer";
|
|
82
87
|
import z from "@deepseek-ai/schemastery";
|
|
83
88
|
import { LocalSubprocessRuntime } from "@deepseek-ai/dsh-subprocess-local";
|
|
84
89
|
import { isPosixAbsolute, toLinuxPath, uncToPosix, windowsToLinuxMount } from "./paths.js";
|
|
85
90
|
import { DEFAULT_WSL_PATH, defaultDistro, wslEnvValue, wslTerminalArgv } from "./wsl.js";
|
|
86
91
|
import { searchSpawnRewrite } from "./search-route.js";
|
|
87
|
-
import { gitSpawnRewrite, translateGitEnv } from "./git-route.js";
|
|
92
|
+
import { DISTRO_GIT_SENTINEL, gitSpawnRewrite, translateGitEnv } from "./git-route.js";
|
|
88
93
|
import { searchExecutionHandle } from "./search-exec.js";
|
|
94
|
+
import { gitExecutionHandle } from "./git-exec.js";
|
|
89
95
|
import { sharedAgent } from "./agent-shared.js";
|
|
90
96
|
import { parseTerminalActivity, TERMINAL_ID_ENV, terminalActivityProbe, wrapTerminalHandle } from "./terminal-activity.js";
|
|
91
97
|
import { AgentUnavailableError } from "./agent-errors.js";
|
|
@@ -341,12 +347,40 @@ export class WslSubprocessRuntime extends LocalSubprocessRuntime {
|
|
|
341
347
|
// object store, work tree, and refs stay untouched by design — the
|
|
342
348
|
// snapshot runs in the tool's private scratch.
|
|
343
349
|
const { env, wslenv } = translateGitEnv(spec.env);
|
|
344
|
-
|
|
350
|
+
const oneShot = () => super.spawn({
|
|
345
351
|
...spec,
|
|
346
352
|
argv: gitRewrite.wslArgv,
|
|
347
353
|
cwd: this.hostCwd(),
|
|
348
354
|
env: wslenv.length > 0 ? { ...env, WSLENV: wslEnvValue(wslenv) } : env,
|
|
349
355
|
});
|
|
356
|
+
// Agent-first: the warm resident answers a git call in ~2 ms where a
|
|
357
|
+
// one-shot `wsl.exe` spends ~230 ms of process startup, and a snapshot
|
|
358
|
+
// makes 6-8 of those calls plus per-file `cat-file`. Only the translated
|
|
359
|
+
// GIT_* names ride the agent's per-request environment — a host PATH
|
|
360
|
+
// must not leak into the distro, and the one-shot forwards exactly those
|
|
361
|
+
// names through WSLENV too. The agent being out falls back to the
|
|
362
|
+
// one-shot handle lazily (see git-exec.ts).
|
|
363
|
+
const agentEnv = {};
|
|
364
|
+
for (const [name, value] of Object.entries(env)) {
|
|
365
|
+
if (name.startsWith("GIT_") && typeof value === "string")
|
|
366
|
+
agentEnv[name] = value;
|
|
367
|
+
}
|
|
368
|
+
const stdinSpec = spec.stdio.stdin;
|
|
369
|
+
const stdinData = typeof stdinSpec === "object" && stdinSpec !== null && typeof stdinSpec.data === "string" ? stdinSpec.data : undefined;
|
|
370
|
+
const stdoutCap = spec.stdio.stdout?.maxBytes ?? 0;
|
|
371
|
+
const stderrCap = spec.stdio.stderr?.maxBytes ?? 0;
|
|
372
|
+
return gitExecutionHandle({
|
|
373
|
+
agent: sharedAgent(gitRewrite.distro),
|
|
374
|
+
cwd: gitRewrite.linuxCwd,
|
|
375
|
+
argv: gitRewrite.gitArgv,
|
|
376
|
+
env: agentEnv,
|
|
377
|
+
stdin: stdinData === undefined ? undefined : Buffer.from(stdinData, "utf8"),
|
|
378
|
+
caps: { stdout: stdoutCap, stderr: stderrCap },
|
|
379
|
+
distro: gitRewrite.distro,
|
|
380
|
+
discovery: gitRewrite.discovery === true,
|
|
381
|
+
signal: spec.signal,
|
|
382
|
+
spawnFallback: oneShot,
|
|
383
|
+
});
|
|
350
384
|
}
|
|
351
385
|
const rewrite = searchSpawnRewrite({ argv: spec.argv, cwd: spec.cwd, wslPath: this.config.wslPath });
|
|
352
386
|
if (rewrite === undefined)
|
|
@@ -372,6 +406,26 @@ export class WslSubprocessRuntime extends LocalSubprocessRuntime {
|
|
|
372
406
|
}),
|
|
373
407
|
});
|
|
374
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Guarded git fallback: when the HOST has no git, resolve the name to the
|
|
411
|
+
* distro sentinel so the change-review pipeline's every spawn (discovery
|
|
412
|
+
* included) routes into the distro instead of dying at the executable
|
|
413
|
+
* lookup. Scoped to the one name — every other consumer's lookup (pwsh,
|
|
414
|
+
* LSP, ripgrep) is untouched — and only on the super's failure, so a
|
|
415
|
+
* machine with Windows git behaves exactly as shipped. See
|
|
416
|
+
* `git-route.ts`'s `DISTRO_GIT_SENTINEL` for the routing side.
|
|
417
|
+
*/
|
|
418
|
+
async resolveExecutable(command, env, signal) {
|
|
419
|
+
try {
|
|
420
|
+
return await super.resolveExecutable(command, env, signal);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
const base = command.slice(Math.max(command.lastIndexOf("/"), command.lastIndexOf("\\")) + 1).toLowerCase().replace(/\.exe$/u, "");
|
|
424
|
+
if (base === "git")
|
|
425
|
+
return DISTRO_GIT_SENTINEL;
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
375
429
|
/**
|
|
376
430
|
* The environment one `wsl.exe` launch forwards: the safe list plus every
|
|
377
431
|
* managed `DSH_*` name, admitted through `WSLENV` and never `PATH` — see the
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "WSL execution environment",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Opens the project workspace inside a WSL Linux instance: command execution, shell scripts and file access all run in the Linux environment for the full Linux shell toolchain, with commands confined by bubblewrap."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "WSL 执行环境",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "在 WSL 的 Linux 实例中打开项目工作区,在对应 Linux 空间中进行命令执行、运行 shell 脚本和文件访问,以获得全套 Linux Shell 工具链收益,并通过 bubblewrap 对命令进行沙箱隔离"
|
|
5
5
|
}
|
|
6
6
|
}
|
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.17.0",
|
|
4
|
+
"description": "\u5728 WSL \u7684 Linux \u5b9e\u4f8b\u4e2d\u6253\u5f00\u9879\u76ee\u5de5\u4f5c\u533a\uff0c\u5728\u5bf9\u5e94 Linux \u7a7a\u95f4\u4e2d\u8fdb\u884c\u547d\u4ee4\u6267\u884c\u3001\u8fd0\u884c shell \u811a\u672c\u548c\u6587\u4ef6\u8bbf\u95ee\uff0c\u4ee5\u83b7\u5f97\u5168\u5957 Linux Shell \u5de5\u5177\u94fe\u6536\u76ca\uff0c\u5e76\u901a\u8fc7 bubblewrap \u5bf9\u547d\u4ee4\u8fdb\u884c\u6c99\u7bb1\u9694\u79bb",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
7
7
|
"dsh",
|
|
@@ -96,9 +96,17 @@
|
|
|
96
96
|
"README.md",
|
|
97
97
|
"docs/README.zh.md",
|
|
98
98
|
"docs/QUICKSTART.md",
|
|
99
|
+
"docs/screenshots/*.png",
|
|
99
100
|
"docs/PERFORMANCE.md",
|
|
101
|
+
"docs/COMPATIBILITY.md",
|
|
100
102
|
"docs/CONFIGURATION.md",
|
|
101
103
|
"docs/LIMITATIONS.md",
|
|
104
|
+
"docs/RELEASING.md",
|
|
105
|
+
"docs/REQUIREMENTS.zh.md",
|
|
106
|
+
"docs/root-fs-routing.md",
|
|
107
|
+
"docs/VALUE.zh.md",
|
|
108
|
+
"docs/UPSTREAM-OPEN-IN-APP.md",
|
|
109
|
+
"docs/adr/0002-native-agent.md",
|
|
102
110
|
"docs/PARITY.md",
|
|
103
111
|
"docs/PEER-PARITY.md",
|
|
104
112
|
"docs/UPSTREAM-FSIO-EXPORT.md",
|
|
@@ -125,9 +133,9 @@
|
|
|
125
133
|
"lint:types": "tsc --noEmit",
|
|
126
134
|
"build": "node scripts/build.mjs",
|
|
127
135
|
"lint:build": "node test/build-freshness.mjs",
|
|
128
|
-
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 --test-coverage-exclude='scripts/**' 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/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/shell-env.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs",
|
|
136
|
+
"test:coverage": "node --test --experimental-test-coverage --test-coverage-lines=85 --test-coverage-branches=84 --test-coverage-functions=70 --test-coverage-exclude='scripts/**' --test-coverage-exclude='test/**' 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/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/shell-env.test.mjs test/terminal-activity.test.mjs test/terminal.test.mjs test/watcher.test.mjs test/wsl.test.mjs test/fs-decisions.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs test/probe-runner.test.mjs",
|
|
129
137
|
"test:syntax": "node test/syntax.mjs",
|
|
130
|
-
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.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/shell-env.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/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs",
|
|
138
|
+
"test:unit": "node --test test/fs.boot.test.mjs test/fsio-text.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/shell-env.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/search-route.test.mjs test/search-exec.test.mjs test/terminal-activity.test.mjs test/session-scope.test.mjs test/session-runtime.test.mjs test/native-agent.test.mjs test/peers.test.mjs test/agent-shared.test.mjs test/distro-route.test.mjs test/fs-routing.test.mjs test/doctor.test.mjs test/profile-preset.test.mjs test/packaging.test.mjs test/peer-canary.test.mjs test/probe-runner.test.mjs",
|
|
131
139
|
"lint:style": "node test/style.mjs",
|
|
132
140
|
"lint:links": "node scripts/check-links.mjs",
|
|
133
141
|
"prepublishOnly": "pnpm test",
|
|
@@ -139,6 +147,7 @@
|
|
|
139
147
|
"probe:terminal": "bash test/probe/terminal.sh",
|
|
140
148
|
"probe:sandbox": "bash test/probe/sandbox.sh",
|
|
141
149
|
"probe:sandbox-shell": "bash test/probe/sandbox-shell.sh",
|
|
150
|
+
"probe:interop-shell": "bash test/probe/interop-shell.sh",
|
|
142
151
|
"sync:windows": "bash test/probe/sync-to-windows.sh",
|
|
143
152
|
"probe:watch": "bash test/probe/watch.sh",
|
|
144
153
|
"probe:agent": "bash test/probe/agent.sh",
|
package/scripts/diagnose.sh
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# to change the machine. `--fix`/`--install` are refused here on purpose —
|
|
4
4
|
# diagnostics collected for a bug report must not repair anything.
|
|
5
5
|
#
|
|
6
|
-
# pnpm run diagnose
|
|
6
|
+
# pnpm run diagnose [distro] [--json]
|
|
7
7
|
#
|
|
8
8
|
# Doctor is `scripts/doctor.mjs`; see its header for what it covers. Exit status
|
|
9
9
|
# follows doctor: 0 when no check failed, 1 when one did.
|
package/scripts/doctor.mjs
CHANGED
|
@@ -255,7 +255,7 @@ function installRemedy(distro, family) {
|
|
|
255
255
|
* @returns {number} the exit status.
|
|
256
256
|
*/
|
|
257
257
|
function rejectReadOnly() {
|
|
258
|
-
console.error("doctor: --read-only refuses --fix/--install; run `pnpm run doctor --
|
|
258
|
+
console.error("doctor: --read-only refuses --fix/--install; run `pnpm run doctor --fix` instead");
|
|
259
259
|
return 2;
|
|
260
260
|
}
|
|
261
261
|
|
|
@@ -334,7 +334,7 @@ async function main(options) {
|
|
|
334
334
|
const bwrapPath = (await inDistro(distro, "command -v bwrap")).stdout.trim();
|
|
335
335
|
const install = installRemedy(distro, family);
|
|
336
336
|
if (bwrapPath.length === 0) {
|
|
337
|
-
check("sandbox.bwrap", "fail", "bubblewrap", "missing; every confined command fails closed without it", `${install}, or run "pnpm run bootstrap
|
|
337
|
+
check("sandbox.bwrap", "fail", "bubblewrap", "missing; every confined command fails closed without it", `${install}, or run "pnpm run bootstrap ${distro} --install"`);
|
|
338
338
|
} else {
|
|
339
339
|
check("sandbox.bwrap", "ok", "bubblewrap", bwrapPath);
|
|
340
340
|
const userns = await inDistro(distro, "bwrap --ro-bind / / -- true");
|
|
@@ -394,6 +394,26 @@ async function main(options) {
|
|
|
394
394
|
if (m === null) return [];
|
|
395
395
|
return (m[1] ?? "").trim().split(/\s+/).filter((p) => p.length > 0);
|
|
396
396
|
});
|
|
397
|
+
// ---- Windows git trust (WSL change review) ------------------------------
|
|
398
|
+
// The change-review pipeline's discovery call runs the WINDOWS git against
|
|
399
|
+
// a \\wsl.localhost UNC when Windows git is installed; git's
|
|
400
|
+
// dubious-ownership check refuses the share unless it is whitelisted, and
|
|
401
|
+
// the pipeline silently degrades to "git is unavailable" (no diff cards for
|
|
402
|
+
// WSL workspaces). A machine without Windows git is fine too — the provider
|
|
403
|
+
// resolves git to a distro sentinel and routes everything. What the doctor
|
|
404
|
+
// can probe cheaply from here is interop git.exe and its whitelist.
|
|
405
|
+
const winGitVersion = await run(["git.exe", "--version"]);
|
|
406
|
+
if (!winGitVersion.ok) {
|
|
407
|
+
check("git.win-safe", "info", "Windows git trust", "Windows git not found through interop — WSL change review uses the distro fallback; host (C:\\...) workspaces would need git for their own review");
|
|
408
|
+
} else {
|
|
409
|
+
const entries = (await run(["git.exe", "config", "--global", "--get-all", "safe.directory"])).stdout.split(/\r?\n/).filter(Boolean);
|
|
410
|
+
const whitelisted = entries.some((entry) => /wsl\.localhost/i.test(entry));
|
|
411
|
+
check("git.win-safe", whitelisted ? "ok" : "warn", "Windows git trust",
|
|
412
|
+
whitelisted ? `the \\\\wsl.localhost share is whitelisted (${entries.length} entr${entries.length === 1 ? "y" : "ies"})`
|
|
413
|
+
: "Windows git does not trust the \\\\wsl.localhost share; WSL workspaces' change review silently degrades to file-tool edits",
|
|
414
|
+
whitelisted ? undefined : 'on Windows: git config --global --add safe.directory "//wsl.localhost/*", then restart the app');
|
|
415
|
+
}
|
|
416
|
+
|
|
397
417
|
// ---- resident handshake --------------------------------------------------
|
|
398
418
|
const agentScript = join(ROOT, "agent", "wsl-agent.sh");
|
|
399
419
|
if (!agentScript.startsWith("/")) {
|
|
@@ -516,7 +536,7 @@ async function main(options) {
|
|
|
516
536
|
if (/^\s*-\s*id:\s*(wsl-shell|wsl-fs)\s*$/m.test(text)) trapped.push(entry.name);
|
|
517
537
|
}
|
|
518
538
|
if (trapped.length > 0) {
|
|
519
|
-
check("config.preset-trap", "warn", "Profile patches", `nested rows patched by id in: ${trapped.join(", ")}`, "wsl-shell and wsl-fs live inside preset-wsl; an id patch matches nothing and is
|
|
539
|
+
check("config.preset-trap", "warn", "Profile patches", `nested rows patched by id in: ${trapped.join(", ")}`, "wsl-shell and wsl-fs live inside preset-wsl; an id patch matches nothing and is ignored with a warning. Run \"pnpm run preset --check <patch>\" to get the complete block, or see docs/CONFIGURATION.md#configure-wsl-shell-and-wsl-fs");
|
|
520
540
|
} else {
|
|
521
541
|
check("config.profiles", "ok", "Profile patches", `${seen} profile patch(es) scanned`);
|
|
522
542
|
}
|