Skip to content

Commit e6b78be

Browse files
fix(server): Muse starts in the child's cwd and PATH search, and a command named - runs
Muse SDK startup spawned natively, so a missing muse searched every PATH directory, hung mounts included, on the event loop: the issue's second trace. GNU env reads a lone - as -i even after --, so that name takes the sh path. Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 2ffa64f commit e6b78be

4 files changed

Lines changed: 71 additions & 4 deletions

File tree

‎apps/server/src/process/NonBlockingSpawner.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ describe.skipIf(HostProcess.Platform.defaultValue() !== "linux")("NonBlockingSpa
105105
const native = yield* run(command).pipe(Effect.provide(NodeServices.layer));
106106
expect(yield* run(command).pipe(Effect.provide(spawner))).toBe(native);
107107
}
108+
// GNU env reads a lone `-` as -i even after `--`.
109+
yield* fileSystem.writeFileString(`${root}/fresh/-`, "#!/bin/sh\necho dash\n");
110+
yield* fileSystem.chmod(`${root}/fresh/-`, 0o755);
111+
const dash = ChildProcess.make("-", [], { env: { PATH: `${root}/fresh` }, cwd: root });
112+
expect(yield* run(dash).pipe(Effect.provide(spawner))).toBe("dash\n");
108113
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
109114
);
110115
});

‎apps/server/src/process/NonBlockingSpawner.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const spawnFailure = (command: ChildProcess.StandardCommand, code: SpawnFailure)
9898
};
9999

100100
// GNU env passes the environment through untouched and execvp's the command.
101-
// It reads any operand containing `=` as an assignment, even after `--`.
101+
// It reads any operand containing `=` as an assignment, and a lone `-` as -i, even after `--`.
102102
const viaEnv = (command: ChildProcess.StandardCommand, cwd: string | undefined) =>
103103
ChildProcess.make(
104104
"/usr/bin/env",
@@ -226,7 +226,7 @@ const make = Effect.gen(function* () {
226226
try: () => findExecutable(candidates, command.command.includes("/")),
227227
catch: (code) => spawnFailure(command, code as SpawnFailure),
228228
});
229-
return envChdir && !command.command.includes("=")
229+
return envChdir && command.command !== "-" && !command.command.includes("=")
230230
? viaEnv(command, workingDirectory)
231231
: viaShell(command, workingDirectory);
232232
});

‎packages/provider-muse/src/server/sdk.test.ts‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,48 @@ describe("Muse SDK host", () => {
258258
);
259259
});
260260

261+
describe("Muse startup off Windows", () => {
262+
it.effect("starts Muse in its cwd without handing libuv the cwd or the PATH search", () =>
263+
Effect.gen(function* () {
264+
if ((yield* HostProcess.Platform) === "win32") return;
265+
const fs = yield* FileSystem.FileSystem;
266+
const directory = yield* fs.realPath(yield* fs.makeTempDirectoryScoped());
267+
const sdk = yield* Effect.promise(() =>
268+
vi.importActual<typeof import("@muse-code/sdk")>("@muse-code/sdk"),
269+
);
270+
const fixture = `
271+
require("node:readline").createInterface({ input: process.stdin }).on("line", line => {
272+
const frame = JSON.parse(line);
273+
if (!("id" in frame)) return;
274+
const result = frame.method === "fixture/cwd"
275+
? { cwd: process.cwd() }
276+
: { schema: { version: 1, fingerprint: "t3-cwd-fixture" }, grantedCapabilities: [] };
277+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result }) + "\\n");
278+
});
279+
`;
280+
const spawned: Array<Parameters<typeof sdk.spawnMspConnection>[0]> = [];
281+
const host = yield* createMuseSdkHostEffect(
282+
{
283+
binaryPath: "node",
284+
launch: { command: "node", args: ["-e", fixture] },
285+
cwd: directory,
286+
environment: { PATH: process.env.PATH },
287+
},
288+
(options) =>
289+
createMuseSdkHost(options, (spawnOptions) => {
290+
spawned.push(spawnOptions);
291+
return sdk.spawnMspConnection(spawnOptions);
292+
}),
293+
);
294+
const response = yield* Effect.promise(() => host.connection.request("fixture/cwd", {}));
295+
yield* Effect.promise(() => host.close());
296+
expect(response).toEqual({ cwd: directory });
297+
expect(spawned[0]?.command).toBe("/bin/sh");
298+
expect(spawned[0]?.cwd).toBeUndefined();
299+
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
300+
);
301+
});
302+
261303
describe("Muse launch", () => {
262304
const CMD = "C:\\Windows\\system32\\cmd.exe";
263305
const onWindows = (resolved: string | undefined, env: NodeJS.ProcessEnv = { ComSpec: CMD }) =>

‎packages/provider-muse/src/server/sdk.ts‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,30 @@ export const createMuseSdkHostEffect = Effect.fn("createMuseSdkHostEffect")(func
212212
options: Omit<MuseSdkHostOptions, "signal">,
213213
createHost: typeof createMuseSdkHost = createMuseSdkHost,
214214
) {
215-
const launch = options.launch ?? (yield* museLaunch(options.binaryPath, options.environment));
215+
const native = options.launch ?? (yield* museLaunch(options.binaryPath, options.environment));
216+
// libuv blocks the server while the child runs chdir and the PATH search, which a hung
217+
// mount stalls, so off Windows the shell does both in the child.
218+
const { cwd = ".", ...childEntered } = options;
219+
const hostOptions: Omit<MuseSdkHostOptions, "signal"> =
220+
(yield* HostProcess.Platform) === "win32"
221+
? { ...options, launch: native }
222+
: {
223+
...childEntered,
224+
launch: {
225+
command: "/bin/sh",
226+
args: [
227+
"-c",
228+
'cd -P -- "$1" || exit 125\nshift\nexec "$@"',
229+
"t3-muse",
230+
cwd.startsWith("/") ? cwd : `./${cwd}`,
231+
native.command,
232+
...native.args,
233+
],
234+
},
235+
};
216236
let startup: Promise<MuseSdkHost> | undefined;
217237
return yield* Effect.tryPromise((signal) => {
218-
startup = createHost({ ...options, launch, signal });
238+
startup = createHost({ ...hostOptions, signal });
219239
return startup;
220240
}).pipe(
221241
Effect.onInterrupt(() =>

0 commit comments

Comments
 (0)