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,6 +381,8 @@ async function runExecProcess(opts: {
|
|||||||
) as ChildProcessWithoutNullStreams;
|
) as ChildProcessWithoutNullStreams;
|
||||||
stdin = child.stdin;
|
stdin = child.stdin;
|
||||||
} else if (opts.usePty) {
|
} else if (opts.usePty) {
|
||||||
|
const { shell, args: shellArgs } = getShellConfig();
|
||||||
|
try {
|
||||||
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
const ptyModule = (await import("@lydell/node-pty")) as unknown as {
|
||||||
spawn?: PtySpawn;
|
spawn?: PtySpawn;
|
||||||
default?: { spawn?: PtySpawn };
|
default?: { spawn?: PtySpawn };
|
||||||
@@ -389,7 +391,6 @@ async function runExecProcess(opts: {
|
|||||||
if (!spawnPty) {
|
if (!spawnPty) {
|
||||||
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
throw new Error("PTY support is unavailable (node-pty spawn not found).");
|
||||||
}
|
}
|
||||||
const { shell, args: shellArgs } = getShellConfig();
|
|
||||||
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
pty = spawnPty(shell, [...shellArgs, opts.command], {
|
||||||
cwd: opts.workdir,
|
cwd: opts.workdir,
|
||||||
env: opts.env,
|
env: opts.env,
|
||||||
@@ -416,6 +417,20 @@ async function runExecProcess(opts: {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} 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