dsh-plugin-wsl-env 0.7.5 → 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 +345 -8
- package/CONTRIBUTING.md +221 -68
- package/README.md +50 -35
- package/SECURITY.md +18 -2
- package/SUPPORT.md +23 -7
- package/cordis.patch.yml +9 -0
- package/docs/ARCHITECTURE.md +200 -57
- package/docs/CONFIGURATION.md +180 -16
- package/docs/LIMITATIONS.md +91 -33
- package/docs/PARITY.md +66 -38
- package/docs/PEER-PARITY.md +95 -32
- package/docs/README.zh.md +49 -31
- package/docs/UPSTREAM-FSIO-EXPORT.md +159 -50
- package/docs/UPSTREAM-TERMINAL-TITLE.md +49 -14
- package/lib/agent-confined.d.ts +42 -0
- package/lib/agent-confined.js +25 -41
- package/lib/agent-errors.d.ts +31 -0
- package/lib/agent-errors.js +19 -16
- package/lib/agent-exec.d.ts +159 -0
- package/lib/agent-exec.js +151 -165
- package/lib/agent-protocol.d.ts +192 -0
- package/lib/agent-protocol.js +76 -100
- package/lib/agent-shared.d.ts +27 -0
- package/lib/agent-shared.js +12 -12
- package/lib/agent.d.ts +353 -0
- package/lib/agent.js +536 -552
- package/lib/auto-preset.d.ts +110 -0
- package/lib/auto-preset.js +157 -141
- package/lib/bwrap.d.ts +90 -0
- package/lib/bwrap.js +18 -18
- package/lib/file-reference-wsl.d.ts +160 -0
- package/lib/file-reference-wsl.js +192 -173
- package/lib/fs-decisions.d.ts +107 -0
- package/lib/fs-decisions.js +64 -60
- package/lib/fs-routing.d.ts +182 -0
- package/lib/fs-routing.js +173 -116
- package/lib/fs-substrate.d.ts +258 -0
- package/lib/fs-substrate.js +248 -248
- package/lib/fs.d.ts +10 -0
- package/lib/fs.js +5 -0
- package/lib/fsio-agent.d.ts +307 -0
- package/lib/fsio-agent.js +414 -410
- package/lib/fsio-text.d.ts +115 -0
- package/lib/fsio-text.js +104 -84
- package/lib/git-route.d.ts +100 -0
- package/lib/git-route.js +46 -47
- package/lib/index.d.ts +1003 -0
- package/lib/index.js +1106 -1003
- package/lib/listing.d.ts +114 -0
- package/lib/listing.js +68 -55
- package/lib/paths.d.ts +182 -0
- package/lib/paths.js +134 -109
- package/lib/picker.d.ts +266 -0
- package/lib/picker.js +371 -351
- package/lib/ports.d.ts +44 -0
- package/lib/ports.js +39 -29
- package/lib/preflight.d.ts +91 -0
- package/lib/preflight.js +94 -0
- package/lib/preset-choice.d.ts +33 -0
- package/lib/preset-choice.js +15 -7
- package/lib/probe-cache.d.ts +51 -0
- package/lib/probe-cache.js +43 -29
- package/lib/sandbox-core.d.ts +149 -0
- package/lib/sandbox-core.js +239 -87
- package/lib/sandbox.d.ts +48 -0
- package/lib/sandbox.js +4 -4
- package/lib/search-exec.d.ts +96 -0
- package/lib/search-exec.js +52 -79
- package/lib/search-route.d.ts +75 -0
- package/lib/search-route.js +30 -31
- package/lib/shell-env.d.ts +88 -0
- package/lib/shell-env.js +78 -74
- package/lib/shell.d.ts +10 -0
- package/lib/shell.js +5 -0
- package/lib/subprocess.d.ts +313 -0
- package/lib/subprocess.js +267 -262
- package/lib/terminal-activity.d.ts +98 -0
- package/lib/terminal-activity.js +29 -39
- package/lib/terminal-route.d.ts +27 -0
- package/lib/terminal-route.js +16 -8
- package/lib/watcher.d.ts +77 -0
- package/lib/watcher.js +120 -126
- package/lib/workspace-files-route.d.ts +132 -0
- package/lib/workspace-files-route.js +73 -81
- package/lib/workspace-files-wsl.d.ts +257 -0
- package/lib/workspace-files-wsl.js +311 -251
- package/lib/wsl.d.ts +266 -0
- package/lib/wsl.js +150 -155
- package/locale/preflight/en.json +6 -0
- package/locale/preflight/zh.json +6 -0
- package/package.json +78 -22
- package/scripts/diagnose.sh +92 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-preset: bind an agent to the WSL preset when its workspace directory
|
|
3
|
+
* lives inside a distro.
|
|
4
|
+
*
|
|
5
|
+
* Why an event hook and not configuration: the environment is chosen in
|
|
6
|
+
* `dsh-api-session-controller`'s `composeAgent(presetId)`, whose signature has no
|
|
7
|
+
* working directory — the cwd only exists one frame up in
|
|
8
|
+
* `ensureSession(sessionId, cwd, ...)`. So the path-aware decision has to be made
|
|
9
|
+
* where the path IS available. `agent/created` carries it:
|
|
10
|
+
*
|
|
11
|
+
* ctx.serial(carrier, "agent/created", { agent, source, signal })
|
|
12
|
+
*
|
|
13
|
+
* and `agent.session.header.cwd` is the session's workspace directory.
|
|
14
|
+
*
|
|
15
|
+
* Two details are load-bearing, both established by experiment:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Respect an explicit choice.** By the time the event fires, the resolved
|
|
18
|
+
* preset is already mounted (`composeAgent` mounts request-or-default in
|
|
19
|
+
* `setup`). We cannot tell which of the two it was — so we only ever rewrite
|
|
20
|
+
* when the mounted preset equals the registry default, which means the client
|
|
21
|
+
* omitted it. An operator's pick is never overridden.
|
|
22
|
+
*
|
|
23
|
+
* 2. **Switch after creation, not during.** Switching inside the listener
|
|
24
|
+
* collides with the previous generation's tools:
|
|
25
|
+
* Error: tool "subagent" is already registered in this scope
|
|
26
|
+
* Deferring past the creating call fixes it — the session is still blank
|
|
27
|
+
* (no turn has started), so `select()` remains legal. A short retry loop
|
|
28
|
+
* absorbs the remaining ordering slack instead of relying on one timer.
|
|
29
|
+
*
|
|
30
|
+
* This is a TypeScript source built to `lib/auto-preset.js`; edit THIS file and
|
|
31
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
32
|
+
* fails when it drifts.
|
|
33
|
+
*
|
|
34
|
+
* @module dsh-plugin-wsl/auto-preset
|
|
35
|
+
*/
|
|
36
|
+
import z from "@deepseek-ai/schemastery";
|
|
37
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
38
|
+
export declare const name = "auto-preset";
|
|
39
|
+
export declare const inject: string[];
|
|
40
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
41
|
+
/** Preset to bind for a WSL workspace. */
|
|
42
|
+
preset: z<string, string, "defined">;
|
|
43
|
+
/** Attempts before giving up; each waits `retryMs`. */
|
|
44
|
+
attempts: z<number, number, "defined">;
|
|
45
|
+
/** Gap between attempts, in milliseconds. */
|
|
46
|
+
retryMs: z<number, number, "defined">;
|
|
47
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
48
|
+
/** Preset to bind for a WSL workspace. */
|
|
49
|
+
preset: z<string, string, "defined">;
|
|
50
|
+
/** Attempts before giving up; each waits `retryMs`. */
|
|
51
|
+
attempts: z<number, number, "defined">;
|
|
52
|
+
/** Gap between attempts, in milliseconds. */
|
|
53
|
+
retryMs: z<number, number, "defined">;
|
|
54
|
+
}>>, "plain">;
|
|
55
|
+
/** The config schema's validated output. */
|
|
56
|
+
export interface AutoPresetConfig {
|
|
57
|
+
/** Preset to bind for a WSL workspace. */
|
|
58
|
+
preset?: string;
|
|
59
|
+
/** Attempts before giving up; each waits `retryMs`. */
|
|
60
|
+
attempts?: number;
|
|
61
|
+
/** Gap between attempts, in milliseconds. */
|
|
62
|
+
retryMs?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The slice of a created agent this plugin inspects; the harness's agent
|
|
66
|
+
* object is structural here.
|
|
67
|
+
*/
|
|
68
|
+
export interface CreatedAgentView {
|
|
69
|
+
/** The agent's id. */
|
|
70
|
+
id: string;
|
|
71
|
+
/** The session stub, when mounted. */
|
|
72
|
+
session?: {
|
|
73
|
+
header?: {
|
|
74
|
+
cwd?: string | undefined;
|
|
75
|
+
};
|
|
76
|
+
} | undefined;
|
|
77
|
+
/** The agent's own context. */
|
|
78
|
+
ctx: Context;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The cordis context extended with the agent-service slices this plugin
|
|
82
|
+
* consumes. `agents` and `agentPresets` come from the harness's agent
|
|
83
|
+
* packages, whose declarations are not visible to this plugin, so they are
|
|
84
|
+
* spelled structurally on top of the framework's {@link Context}.
|
|
85
|
+
*/
|
|
86
|
+
export type AutoPresetContext = Context & {
|
|
87
|
+
agents: {
|
|
88
|
+
list: () => CreatedAgentView[];
|
|
89
|
+
};
|
|
90
|
+
agentPresets: {
|
|
91
|
+
composedPreset: (ctx: Context) => string | undefined;
|
|
92
|
+
defaultId: string | undefined;
|
|
93
|
+
select: (agent: CreatedAgentView, presetId: string) => Promise<string>;
|
|
94
|
+
};
|
|
95
|
+
on: (event: "agent/created", listener: (payload: {
|
|
96
|
+
agent?: CreatedAgentView;
|
|
97
|
+
} | undefined) => void) => () => boolean;
|
|
98
|
+
sessionController?: {
|
|
99
|
+
agents?: {
|
|
100
|
+
ensureSession: (this: unknown, sessionId: string, cwd: string | undefined, checkPersistedIdentity: boolean, presetId: string | undefined) => Promise<unknown>;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Mount the auto-preset plugin.
|
|
106
|
+
*
|
|
107
|
+
* @param ctx - the cordis context.
|
|
108
|
+
* @param config - the schema-validated plugin config.
|
|
109
|
+
*/
|
|
110
|
+
export declare function apply(ctx: AutoPresetContext, config?: AutoPresetConfig): Promise<void>;
|
package/lib/auto-preset.js
CHANGED
|
@@ -27,14 +27,16 @@
|
|
|
27
27
|
* (no turn has started), so `select()` remains legal. A short retry loop
|
|
28
28
|
* absorbs the remaining ordering slack instead of relying on one timer.
|
|
29
29
|
*
|
|
30
|
+
* This is a TypeScript source built to `lib/auto-preset.js`; edit THIS file and
|
|
31
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
32
|
+
* fails when it drifts.
|
|
33
|
+
*
|
|
30
34
|
* @module dsh-plugin-wsl/auto-preset
|
|
31
35
|
*/
|
|
32
|
-
|
|
33
36
|
import { appendFileSync } from "node:fs";
|
|
34
37
|
import z from "@deepseek-ai/schemastery";
|
|
35
38
|
import { isWslUnc } from "./paths.js";
|
|
36
39
|
import { shouldAdoptWslPreset } from "./preset-choice.js";
|
|
37
|
-
|
|
38
40
|
/**
|
|
39
41
|
* Set `DSH_WSL_TRACE` to a file path to record every decision this plugin makes.
|
|
40
42
|
*
|
|
@@ -43,156 +45,170 @@ import { shouldAdoptWslPreset } from "./preset-choice.js";
|
|
|
43
45
|
* bug in this plugin stayed invisible once already.
|
|
44
46
|
*/
|
|
45
47
|
const TRACE = process.env.DSH_WSL_TRACE;
|
|
48
|
+
/**
|
|
49
|
+
* Append one decision line to the trace file, when tracing is on.
|
|
50
|
+
*
|
|
51
|
+
* @param message - the decision line to append.
|
|
52
|
+
*/
|
|
46
53
|
function trace(message) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
if (TRACE === void 0 || TRACE === "")
|
|
55
|
+
return;
|
|
56
|
+
try {
|
|
57
|
+
appendFileSync(TRACE, `${new Date().toISOString()} ${message}\n`);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* a trace is never worth failing a session over */
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
|
-
|
|
55
63
|
export const name = "auto-preset";
|
|
56
|
-
|
|
57
64
|
export const inject = ["agents", "agentPresets"];
|
|
58
|
-
|
|
59
65
|
export const Config = z.object({
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
/** Preset to bind for a WSL workspace. */
|
|
67
|
+
preset: z.string().default("wsl"),
|
|
68
|
+
/** Attempts before giving up; each waits `retryMs`. */
|
|
69
|
+
attempts: z.natural().min(1).default(6),
|
|
70
|
+
/** Gap between attempts, in milliseconds. */
|
|
71
|
+
retryMs: z.natural().default(150),
|
|
66
72
|
});
|
|
67
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Mount the auto-preset plugin.
|
|
75
|
+
*
|
|
76
|
+
* @param ctx - the cordis context.
|
|
77
|
+
* @param config - the schema-validated plugin config.
|
|
78
|
+
*/
|
|
68
79
|
export async function apply(ctx, config = {}) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
ctx.logger?.warn?.(`auto-preset: ${message}`);
|
|
79
|
-
logged = ctx.logger !== void 0;
|
|
80
|
-
} catch {
|
|
81
|
-
logged = false;
|
|
82
|
-
}
|
|
83
|
-
if (!logged) {
|
|
84
|
-
try {
|
|
85
|
-
process.stderr.write(`auto-preset: ${message}\n`);
|
|
86
|
-
} catch {
|
|
87
|
-
/* logging is never worth failing a session over */
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const consider = (agent) => {
|
|
93
|
-
if (agent === void 0) return;
|
|
94
|
-
|
|
95
|
-
let cwd;
|
|
96
|
-
let current;
|
|
97
|
-
try {
|
|
98
|
-
cwd = agent.session?.header?.cwd;
|
|
99
|
-
current = ctx.agentPresets.composedPreset(agent.ctx);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
warn(`cannot inspect the new agent: ${error?.message ?? error}`);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
trace(`consider agent=${agent.id} cwd=${JSON.stringify(cwd)} current=${String(current)}`);
|
|
105
|
-
|
|
106
|
-
// Only a workspace inside a distro, and only when the preset is still the
|
|
107
|
-
// registry default (i.e. the operator did not ask for one). `undefined` means
|
|
108
|
-
// the creating caller mounted nothing at all — the headless runner does this
|
|
109
|
-
// — which is likewise "no explicit choice".
|
|
110
|
-
let fallback;
|
|
111
|
-
try {
|
|
112
|
-
fallback = ctx.agentPresets.defaultId;
|
|
113
|
-
} catch {
|
|
114
|
-
fallback = void 0;
|
|
115
|
-
}
|
|
116
|
-
if (!shouldAdoptWslPreset({ cwd, current, fallback })) return;
|
|
117
|
-
trace(` -> acting: fallback=${String(fallback)} isWsl=${isWslUnc(cwd)}`);
|
|
118
|
-
|
|
119
|
-
// Deferred on purpose — see the module comment.
|
|
120
|
-
void (async () => {
|
|
121
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
80
|
+
const target = config.preset ?? "wsl";
|
|
81
|
+
const attempts = config.attempts ?? 6;
|
|
82
|
+
const retryMs = config.retryMs ?? 150;
|
|
83
|
+
const warn = (message) => {
|
|
84
|
+
trace(`WARN ${message}`);
|
|
85
|
+
// `logger` is not injectable here, so an unavailable or throwing logger must
|
|
86
|
+
// not turn this into a silent no-op: fall back to stderr.
|
|
87
|
+
let logged = false;
|
|
122
88
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
89
|
+
ctx.logger?.warn?.(`auto-preset: ${message}`);
|
|
90
|
+
logged = ctx.logger !== void 0;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
logged = false;
|
|
94
|
+
}
|
|
95
|
+
if (!logged) {
|
|
96
|
+
try {
|
|
97
|
+
process.stderr.write(`auto-preset: ${message}\n`);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* logging is never worth failing a session over */
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const consider = (agent) => {
|
|
105
|
+
if (agent === void 0)
|
|
136
106
|
return;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
107
|
+
let cwd;
|
|
108
|
+
let current;
|
|
109
|
+
try {
|
|
110
|
+
cwd = agent.session?.header?.cwd;
|
|
111
|
+
current = ctx.agentPresets.composedPreset(agent.ctx);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
warn(`cannot inspect the new agent: ${error?.message ?? error}`);
|
|
140
115
|
return;
|
|
141
|
-
}
|
|
142
|
-
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
143
116
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
117
|
+
trace(`consider agent=${agent.id} cwd=${JSON.stringify(cwd)} current=${String(current)}`);
|
|
118
|
+
// Only a workspace inside a distro, and only when the preset is still the
|
|
119
|
+
// registry default (i.e. the operator did not ask for one). `undefined` means
|
|
120
|
+
// the creating caller mounted nothing at all — the headless runner does this
|
|
121
|
+
// — which is likewise "no explicit choice".
|
|
122
|
+
let fallback;
|
|
123
|
+
try {
|
|
124
|
+
fallback = ctx.agentPresets.defaultId;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
fallback = void 0;
|
|
128
|
+
}
|
|
129
|
+
if (!shouldAdoptWslPreset({ cwd, current, fallback }))
|
|
130
|
+
return;
|
|
131
|
+
trace(` -> acting: fallback=${String(fallback)} isWsl=${isWslUnc(cwd)}`);
|
|
132
|
+
// Deferred on purpose — see the module comment.
|
|
133
|
+
void (async () => {
|
|
134
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
135
|
+
try {
|
|
136
|
+
const chosen = await ctx.agentPresets.select(agent, target);
|
|
137
|
+
ctx.logger?.debug?.(`auto-preset: "${cwd}" -> preset "${chosen}"`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
const code = error?.code;
|
|
142
|
+
// The session already started a turn: its preset is fixed, and that is
|
|
143
|
+
// a legitimate outcome (the operator drove it before we got here).
|
|
144
|
+
if (code === "agent-preset/locked") {
|
|
145
|
+
warn(`"${cwd}" already began its first turn; leaving the preset as "${String(current)}"`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (code === "agent-preset/not-found") {
|
|
149
|
+
warn(`preset "${target}" is not composed in this profile; skipping "${cwd}"`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (attempt === attempts) {
|
|
153
|
+
warn(`could not bind preset "${target}" for "${cwd}": ${code ?? error?.name}: ${error?.message}`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, retryMs));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
183
160
|
};
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
161
|
+
ctx.on("agent/created", (payload) => consider(payload?.agent));
|
|
162
|
+
// ── Make the INITIAL preset correct, instead of correcting it afterwards ────
|
|
163
|
+
//
|
|
164
|
+
// A WSL workspace mounted on the HOST preset puts the Windows ACL sandbox in
|
|
165
|
+
// front of a `\\wsl.localhost` path, which cannot carry security descriptors:
|
|
166
|
+
// the shell tool then dies in sandbox preparation (`GetNamedSecurityInfoW
|
|
167
|
+
// failed (Win32 1)`) and the workspace is unusable — not merely mis-labelled.
|
|
168
|
+
// Correcting the preset after creation loses whenever the first turn does not
|
|
169
|
+
// wait, so the choice is made where the cwd still exists: the controller
|
|
170
|
+
// resolves the preset in `composeAgent(presetId)`, which has no cwd, but it
|
|
171
|
+
// calls `agents.ensureSession(sessionId, cwd, …, presetId)` — and that frame
|
|
172
|
+
// holds both. Filling the preset there makes the very first mount already
|
|
173
|
+
// correct, so the host sandbox never meets the distro path at all.
|
|
174
|
+
ctx.inject(["sessionController"], (controllerCtx) => {
|
|
175
|
+
const facade = controllerCtx.sessionController?.agents;
|
|
176
|
+
if (facade === void 0 || typeof facade.ensureSession !== "function") {
|
|
177
|
+
warn("sessionController exposes no agents.ensureSession(); the creation-time preset fix is inactive");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const original = facade.ensureSession;
|
|
181
|
+
facade.ensureSession = async function (sessionId, cwd, checkPersistedIdentity, presetId) {
|
|
182
|
+
// An explicit choice always wins, and only a distro workspace is affected.
|
|
183
|
+
if (presetId !== void 0 || cwd === undefined || !isWslUnc(cwd)) {
|
|
184
|
+
return original.call(this, sessionId, cwd, checkPersistedIdentity, presetId);
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return await original.call(this, sessionId, cwd, checkPersistedIdentity, target);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
// A session already stored under another preset rejects a different one.
|
|
191
|
+
// Fall back rather than turn adoption into a new failure mode.
|
|
192
|
+
if (!String(error?.message ?? "").includes("agent preset"))
|
|
193
|
+
throw error;
|
|
194
|
+
trace(`creation-time preset "${target}" refused for ${sessionId}; falling back to the recorded one`);
|
|
195
|
+
return original.call(this, sessionId, cwd, checkPersistedIdentity, presetId);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
trace("creation-time preset fix installed");
|
|
199
|
+
controllerCtx.effect(() => () => {
|
|
200
|
+
facade.ensureSession = original;
|
|
201
|
+
});
|
|
187
202
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
203
|
+
// Mount order is not a guarantee: a runner that creates its session during
|
|
204
|
+
// startup can finish before this plugin mounts and miss the event entirely
|
|
205
|
+
// (dsh-headless does exactly that). Sweep the agents already live so the
|
|
206
|
+
// decision never depends on who mounted first.
|
|
207
|
+
try {
|
|
208
|
+
for (const agent of ctx.agents.list())
|
|
209
|
+
consider(agent);
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
warn(`could not sweep already-live agents: ${error?.message ?? error}`);
|
|
213
|
+
}
|
|
198
214
|
}
|
package/lib/bwrap.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The distro-side `bwrap` command line, shared by every confinement site.
|
|
3
|
+
*
|
|
4
|
+
* Two callers compose a confined command and they must agree on it exactly:
|
|
5
|
+
* `lib/sandbox.js` wraps a *single* command (`confine()`, and the usability
|
|
6
|
+
* probe), and `lib/agent-confined.js` wraps a *resident* whose whole lifetime
|
|
7
|
+
* runs inside the profile — it hands the prefix to the transport, which inserts
|
|
8
|
+
* it between `wsl.exe --exec` and the `sh <script>` pair.
|
|
9
|
+
*
|
|
10
|
+
* That split is why this module exists. The two sites used to assemble the argv
|
|
11
|
+
* themselves, and only one of them prepended the program: the confined resident's
|
|
12
|
+
* prefix began at `--ro-bind`, so `wsl.exe --exec` tried to run a `--ro-bind`
|
|
13
|
+
* executable, the agent exited 1 during its handshake, and every confined
|
|
14
|
+
* mutation failed with `FS_IO_ERROR: … agent exited during handshake`. The
|
|
15
|
+
* verified probe could not see it because it inlined its own copy of the argv.
|
|
16
|
+
*
|
|
17
|
+
* This module imports nothing, so a bare checkout can assert the argv both sites
|
|
18
|
+
* actually produce.
|
|
19
|
+
*
|
|
20
|
+
* This is a TypeScript source built to `lib/bwrap.js`; edit THIS file and run
|
|
21
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
22
|
+
* fails when it drifts.
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-plugin-wsl/bwrap
|
|
25
|
+
*/
|
|
26
|
+
/** Profile options shared by the two confinement sites. */
|
|
27
|
+
export interface BwrapProfileOptions {
|
|
28
|
+
/** Shadow `/mnt` with an empty tmpfs. */
|
|
29
|
+
maskWindowsDrive?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Workspace-write binds the real `/tmp` read-write instead of mounting an
|
|
32
|
+
* ephemeral tmpfs over it.
|
|
33
|
+
*/
|
|
34
|
+
realTmp?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the bwrap profile arguments for one file-effect policy, exactly as
|
|
38
|
+
* upstream's `bwrapProfileArgs` does.
|
|
39
|
+
*
|
|
40
|
+
* `--ro-bind / /` makes the whole distro read-only, `--dev /dev` supplies the
|
|
41
|
+
* `/dev/null` sink a shell needs, `--unshare-pid` keeps the sandbox's process
|
|
42
|
+
* view its own, and `--proc /proc` gives it a matching `/proc`. Under
|
|
43
|
+
* `workspace-write` the workspace is bound read-write and `/tmp` becomes an
|
|
44
|
+
* ephemeral tmpfs — the temp area the mode promises, without exposing the
|
|
45
|
+
* distro's real `/tmp`.
|
|
46
|
+
*
|
|
47
|
+
* `options.maskWindowsDrive` mounts an empty tmpfs over `/mnt` AFTER the
|
|
48
|
+
* read-only root, shadowing the Windows drive: the drive's files disappear
|
|
49
|
+
* from the command's view, which severs interop as a side effect — a Windows
|
|
50
|
+
* executable on the drive can no longer be reached to launch, and the drive's
|
|
51
|
+
* data cannot be read or exfiltrated. It is a narrowing, not a closure: a
|
|
52
|
+
* command may still WRITE an executable into the workspace and run it
|
|
53
|
+
* (binfmt interop dispatches on file content, not location), so `enforcement`
|
|
54
|
+
* stays `partial` — see `lib/sandbox.js`.
|
|
55
|
+
*
|
|
56
|
+
* `options.realTmp` changes what workspace-write does at `/tmp`. The default
|
|
57
|
+
* (`--tmpfs /tmp`) gives the sandbox an ephemeral, empty temp area — the right
|
|
58
|
+
* shape for a COMMAND: fresh per run, cannot touch the distro's real `/tmp`,
|
|
59
|
+
* cannot be poisoned by a previous run. `realTmp: true` binds the REAL `/tmp`
|
|
60
|
+
* read-write instead — the right shape for the FILE-WRITES resident: the
|
|
61
|
+
* fence grants `/tmp` as a writable root, so a write the fence approved must
|
|
62
|
+
* land where every reader (the plain agent, the user's own shell) reads — not
|
|
63
|
+
* inside a private tmpfs that reads cannot see and that dies with the
|
|
64
|
+
* resident. Read-only mode mounts no `/tmp` at all, so the option is inert
|
|
65
|
+
* there.
|
|
66
|
+
*
|
|
67
|
+
* @param policy - the policy to express as bwrap mounts.
|
|
68
|
+
* @param options - profile options.
|
|
69
|
+
* @returns profile arguments, before the `--` separator and the command argv.
|
|
70
|
+
*/
|
|
71
|
+
export declare function bwrapProfileArgs(policy: {
|
|
72
|
+
mode: string;
|
|
73
|
+
workspaceRoot: string;
|
|
74
|
+
}, options?: BwrapProfileOptions): string[];
|
|
75
|
+
/**
|
|
76
|
+
* The argv prefix that runs one command (or one resident) inside the profile.
|
|
77
|
+
*
|
|
78
|
+
* The prefix is a complete command from the distro's point of view: it names the
|
|
79
|
+
* program (`bwrap`), the profile, and the `--` separator that ends bwrap's own
|
|
80
|
+
* options. `workspaceRoot` must already be spelled as a Linux path inside the
|
|
81
|
+
* distro; the callers own that translation.
|
|
82
|
+
*
|
|
83
|
+
* @param policy - the resolved policy: `mode`, plus the Linux `workspaceRoot`.
|
|
84
|
+
* @param options - profile options, passed through to {@link bwrapProfileArgs}.
|
|
85
|
+
* @returns argv to insert before the command, as the transport receives it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function bwrapArgvPrefix(policy: {
|
|
88
|
+
mode: string;
|
|
89
|
+
workspaceRoot: string;
|
|
90
|
+
}, options?: BwrapProfileOptions): string[];
|
package/lib/bwrap.js
CHANGED
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
* This module imports nothing, so a bare checkout can assert the argv both sites
|
|
18
18
|
* actually produce.
|
|
19
19
|
*
|
|
20
|
+
* This is a TypeScript source built to `lib/bwrap.js`; edit THIS file and run
|
|
21
|
+
* `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
|
|
22
|
+
* fails when it drifts.
|
|
23
|
+
*
|
|
20
24
|
* @module dsh-plugin-wsl/bwrap
|
|
21
25
|
*/
|
|
22
|
-
|
|
23
26
|
/**
|
|
24
27
|
* Build the bwrap profile arguments for one file-effect policy, exactly as
|
|
25
28
|
* upstream's `bwrapProfileArgs` does.
|
|
@@ -53,27 +56,24 @@
|
|
|
53
56
|
*
|
|
54
57
|
* @param policy - the policy to express as bwrap mounts.
|
|
55
58
|
* @param options - profile options.
|
|
56
|
-
* @param options.maskWindowsDrive - shadow `/mnt` with an empty tmpfs.
|
|
57
|
-
* @param options.realTmp - workspace-write binds the real `/tmp` read-write
|
|
58
|
-
* instead of mounting an ephemeral tmpfs over it.
|
|
59
59
|
* @returns profile arguments, before the `--` separator and the command argv.
|
|
60
60
|
*/
|
|
61
61
|
export function bwrapProfileArgs(policy, options = {}) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const args = ["--ro-bind", "/", "/", "--dev", "/dev", "--unshare-pid", "--proc", "/proc", "--die-with-parent"];
|
|
63
|
+
if (policy.mode === "workspace-write") {
|
|
64
|
+
if (options.realTmp) {
|
|
65
|
+
args.push("--bind", "/tmp", "/tmp");
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
args.push("--tmpfs", "/tmp");
|
|
69
|
+
}
|
|
70
|
+
args.push("--bind", policy.workspaceRoot, policy.workspaceRoot);
|
|
71
|
+
}
|
|
72
|
+
if (options.maskWindowsDrive) {
|
|
73
|
+
args.push("--tmpfs", "/mnt");
|
|
68
74
|
}
|
|
69
|
-
args
|
|
70
|
-
}
|
|
71
|
-
if (options.maskWindowsDrive) {
|
|
72
|
-
args.push("--tmpfs", "/mnt");
|
|
73
|
-
}
|
|
74
|
-
return args;
|
|
75
|
+
return args;
|
|
75
76
|
}
|
|
76
|
-
|
|
77
77
|
/**
|
|
78
78
|
* The argv prefix that runs one command (or one resident) inside the profile.
|
|
79
79
|
*
|
|
@@ -87,5 +87,5 @@ export function bwrapProfileArgs(policy, options = {}) {
|
|
|
87
87
|
* @returns argv to insert before the command, as the transport receives it.
|
|
88
88
|
*/
|
|
89
89
|
export function bwrapArgvPrefix(policy, options = {}) {
|
|
90
|
-
|
|
90
|
+
return ["bwrap", ...bwrapProfileArgs(policy, options), "--"];
|
|
91
91
|
}
|