fix: harden pty spawn path
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
import type { AgentTool, AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||||
import { Type } from "@sinclair/typebox";
|
import { Type } from "@sinclair/typebox";
|
||||||
import type { IPty } from "node-pty";
|
import type { IPty } from "node-pty";
|
||||||
@@ -31,6 +33,9 @@ const DEFAULT_MAX_OUTPUT = clampNumber(
|
|||||||
1_000,
|
1_000,
|
||||||
150_000,
|
150_000,
|
||||||
);
|
);
|
||||||
|
const DEFAULT_SHELL_PATH = "/bin/sh";
|
||||||
|
const DEFAULT_PATH =
|
||||||
|
"/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
||||||
const DEFAULT_PTY_NAME = "xterm-256color";
|
const DEFAULT_PTY_NAME = "xterm-256color";
|
||||||
|
|
||||||
type PtyModule = typeof import("node-pty");
|
type PtyModule = typeof import("node-pty");
|
||||||
@@ -173,12 +178,13 @@ export function createBashTool(
|
|||||||
"falling back to pipe mode.";
|
"falling back to pipe mode.";
|
||||||
stdinMode = "pipe";
|
stdinMode = "pipe";
|
||||||
} else {
|
} else {
|
||||||
const ptyEnv = {
|
const ptyEnv = ensurePath({
|
||||||
...env,
|
...env,
|
||||||
TERM: env.TERM ?? DEFAULT_PTY_NAME,
|
TERM: env.TERM ?? DEFAULT_PTY_NAME,
|
||||||
} as Record<string, string>;
|
} as Record<string, string>);
|
||||||
|
const ptyShell = resolveShellPath(shell, ptyEnv);
|
||||||
try {
|
try {
|
||||||
pty = ptyModule.spawn(shell, [...shellArgs, params.command], {
|
pty = ptyModule.spawn(ptyShell, [...shellArgs, params.command], {
|
||||||
cwd: workdir,
|
cwd: workdir,
|
||||||
env: ptyEnv,
|
env: ptyEnv,
|
||||||
name: ptyEnv.TERM || DEFAULT_PTY_NAME,
|
name: ptyEnv.TERM || DEFAULT_PTY_NAME,
|
||||||
@@ -186,10 +192,31 @@ export function createBashTool(
|
|||||||
rows: 30,
|
rows: 30,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
warning =
|
if (ptyShell !== DEFAULT_SHELL_PATH && existsSync(DEFAULT_SHELL_PATH)) {
|
||||||
`Warning: node-pty failed to start${formatPtyError(error)}; ` +
|
try {
|
||||||
"falling back to pipe mode.";
|
pty = ptyModule.spawn(
|
||||||
stdinMode = "pipe";
|
DEFAULT_SHELL_PATH,
|
||||||
|
[...shellArgs, params.command],
|
||||||
|
{
|
||||||
|
cwd: workdir,
|
||||||
|
env: ptyEnv,
|
||||||
|
name: ptyEnv.TERM || DEFAULT_PTY_NAME,
|
||||||
|
cols: 120,
|
||||||
|
rows: 30,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (fallbackError) {
|
||||||
|
warning =
|
||||||
|
`Warning: node-pty failed to start${formatPtyError(fallbackError)}; ` +
|
||||||
|
"falling back to pipe mode.";
|
||||||
|
stdinMode = "pipe";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warning =
|
||||||
|
`Warning: node-pty failed to start${formatPtyError(error)}; ` +
|
||||||
|
"falling back to pipe mode.";
|
||||||
|
stdinMode = "pipe";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -888,6 +915,30 @@ function killSession(session: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensurePath(env: Record<string, string>) {
|
||||||
|
if (!env.PATH?.trim()) {
|
||||||
|
env.PATH = DEFAULT_PATH;
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveShellPath(shell: string, env: Record<string, string>) {
|
||||||
|
if (process.platform === "win32") return shell;
|
||||||
|
if (shell.includes("/") && existsSync(shell)) {
|
||||||
|
return shell;
|
||||||
|
}
|
||||||
|
const searchPath = env.PATH ?? "";
|
||||||
|
for (const segment of searchPath.split(path.delimiter)) {
|
||||||
|
if (!segment) continue;
|
||||||
|
const candidate = path.join(segment, shell);
|
||||||
|
if (existsSync(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
if (existsSync(DEFAULT_SHELL_PATH)) {
|
||||||
|
return DEFAULT_SHELL_PATH;
|
||||||
|
}
|
||||||
|
return shell;
|
||||||
|
}
|
||||||
|
|
||||||
function formatPtyError(error: unknown) {
|
function formatPtyError(error: unknown) {
|
||||||
if (!error) return "";
|
if (!error) return "";
|
||||||
if (typeof error === "string") return ` (${error})`;
|
if (typeof error === "string") return ` (${error})`;
|
||||||
|
|||||||
Reference in New Issue
Block a user