fix: fall back to non-PTY exec
This commit is contained in:
@@ -31,6 +31,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
- Control UI: resolve local avatar URLs with basePath across injection + identity RPC. (#1457) Thanks @dlauer.
|
||||||
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
- Discord: clarify Message Content Intent onboarding hint. (#1487) Thanks @kyleok.
|
||||||
- Agents: surface concrete API error details instead of generic AI service errors.
|
- Agents: surface concrete API error details instead of generic AI service errors.
|
||||||
|
- Exec: fall back to non-PTY when PTY spawn fails (EBADF). (#1484)
|
||||||
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
- Exec approvals: allow per-segment allowlists for chained shell commands on gateway + node hosts. (#1458) Thanks @czekaj.
|
||||||
- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
|
- Agents: make OpenAI sessions image-sanitize-only; gate tool-id/repair sanitization by provider.
|
||||||
- Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.
|
- Doctor: honor CLAWDBOT_GATEWAY_TOKEN for auth checks and security audit token reuse. (#1448) Thanks @azade-c.
|
||||||
|
|||||||
31
src/agents/bash-tools.exec.pty-fallback.test.ts
Normal file
31
src/agents/bash-tools.exec.pty-fallback.test.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { afterEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { resetProcessRegistryForTests } from "./bash-process-registry";
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resetProcessRegistryForTests();
|
||||||
|
vi.resetModules();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("exec falls back when PTY spawn fails", async () => {
|
||||||
|
vi.doMock("@lydell/node-pty", () => ({
|
||||||
|
spawn: () => {
|
||||||
|
const err = new Error("spawn EBADF");
|
||||||
|
(err as NodeJS.ErrnoException).code = "EBADF";
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { createExecTool } = await import("./bash-tools.exec");
|
||||||
|
const tool = createExecTool({ allowBackground: false });
|
||||||
|
const result = await tool.execute("toolcall", {
|
||||||
|
command: "printf ok",
|
||||||
|
pty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.details.status).toBe("completed");
|
||||||
|
const text = result.content?.[0]?.text ?? "";
|
||||||
|
expect(text).toContain("ok");
|
||||||
|
expect(text).toContain("PTY spawn failed");
|
||||||
|
});
|
||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
resolveShellEnvFallbackTimeoutMs,
|
resolveShellEnvFallbackTimeoutMs,
|
||||||
} from "../infra/shell-env.js";
|
} from "../infra/shell-env.js";
|
||||||
import { enqueueSystemEvent } from "../infra/system-events.js";
|
import { enqueueSystemEvent } from "../infra/system-events.js";
|
||||||
import { logInfo } from "../logger.js";
|
import { logInfo, logWarn } from "../logger.js";
|
||||||
import {
|
import {
|
||||||
type ProcessSession,
|
type ProcessSession,
|
||||||
type SessionStdin,
|
type SessionStdin,
|
||||||
@@ -381,41 +381,56 @@ async function runExecProcess(opts: {
|
|||||||
) as ChildProcessWithoutNullStreams;
|
) as ChildProcessWithoutNullStreams;
|
||||||
stdin = child.stdin;
|
stdin = child.stdin;
|
||||||
} else if (opts.usePty) {
|
} else if (opts.usePty) {
|
||||||
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
|
||||||
spawn?: PtySpawn;
|
|
||||||
default?: { spawn?: PtySpawn };
|
|
||||||
};
|
|
||||||
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
|
||||||
if (!spawnPty) {
|
|
||||||
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
|
||||||
}
|
|
||||||
const { shell, args: shellArgs } = getShellConfig();
|
const { shell, args: shellArgs } = getShellConfig();
|
||||||
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
try {
|
||||||
cwd: opts.workdir,
|
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
||||||
env: opts.env,
|
spawn?: PtySpawn;
|
||||||
name: process.env.TERM ?? "xterm-256color",
|
default?: { spawn?: PtySpawn };
|
||||||
cols: 120,
|
};
|
||||||
rows: 30,
|
const spawnPty = ptyModule.spawn ?? ptyModule.default?.spawn;
|
||||||
});
|
if (!spawnPty) {
|
||||||
stdin = {
|
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
||||||
destroyed: false,
|
}
|
||||||
write: (data, cb) => {
|
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
||||||
try {
|
cwd: opts.workdir,
|
||||||
pty?.write(data);
|
env: opts.env,
|
||||||
cb?.(null);
|
name: process.env.TERM ?? "xterm-256color",
|
||||||
} catch (err) {
|
cols: 120,
|
||||||
cb?.(err as Error);
|
rows: 30,
|
||||||
}
|
});
|
||||||
},
|
stdin = {
|
||||||
end: () => {
|
destroyed: false,
|
||||||
try {
|
write: (data, cb) => {
|
||||||
const eof = process.platform === "win32" ? "\x1a" : "\x04";
|
try {
|
||||||
pty?.write(eof);
|
pty?.write(data);
|
||||||
} catch {
|
cb?.(null);
|
||||||
// ignore EOF errors
|
} catch (err) {
|
||||||
}
|
cb?.(err as Error);
|
||||||
},
|
}
|
||||||
};
|
},
|
||||||
|
end: () => {
|
||||||
|
try {
|
||||||
|
const eof = process.platform === "win32" ? "\x1a" : "\x04";
|
||||||
|
pty?.write(eof);
|
||||||
|
} catch {
|
||||||
|
// ignore EOF errors
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const errText = String(err);
|
||||||
|
const warning = `Warning: PTY spawn failed (${errText}); retrying without PTY for \`${opts.command}\`.`;
|
||||||
|
logWarn(`exec: PTY spawn failed (${errText}); retrying without PTY for "${opts.command}".`);
|
||||||
|
opts.warnings.push(warning);
|
||||||
|
child = spawn(shell, [...shellArgs, opts.command], {
|
||||||
|
cwd: opts.workdir,
|
||||||
|
env: opts.env,
|
||||||
|
detached: process.platform !== "win32",
|
||||||
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
|
windowsHide: true,
|
||||||
|
}) as ChildProcessWithoutNullStreams;
|
||||||
|
stdin = child.stdin;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const { shell, args: shellArgs } = getShellConfig();
|
const { shell, args: shellArgs } = getShellConfig();
|
||||||
child = spawn(shell, [...shellArgs, opts.command], {
|
child = spawn(shell, [...shellArgs, opts.command], {
|
||||||
@@ -1320,7 +1335,7 @@ export function createExecTool(
|
|||||||
|
|
||||||
const effectiveTimeout =
|
const effectiveTimeout =
|
||||||
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
||||||
const warningText = warnings.length ? `${warnings.join("\n")}\n\n` : "";
|
const getWarningText = () => (warnings.length ? `${warnings.join("\n")}\n\n` : "");
|
||||||
const usePty = params.pty === true && !sandbox;
|
const usePty = params.pty === true && !sandbox;
|
||||||
const run = await runExecProcess({
|
const run = await runExecProcess({
|
||||||
command: params.command,
|
command: params.command,
|
||||||
@@ -1360,7 +1375,7 @@ export function createExecTool(
|
|||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text:
|
text:
|
||||||
`${warningText}` +
|
`${getWarningText()}` +
|
||||||
`Command still running (session ${run.session.id}, pid ${
|
`Command still running (session ${run.session.id}, pid ${
|
||||||
run.session.pid ?? "n/a"
|
run.session.pid ?? "n/a"
|
||||||
}). ` +
|
}). ` +
|
||||||
@@ -1410,7 +1425,7 @@ export function createExecTool(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: `${warningText}${outcome.aggregated || "(no output)"}`,
|
text: `${getWarningText()}${outcome.aggregated || "(no output)"}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
details: {
|
details: {
|
||||||
|
|||||||
Reference in New Issue
Block a user