fix: unblock claude-cli live runs

This commit is contained in:
Peter Steinberger
2026-01-11 00:22:48 +00:00
parent d8f1124d59
commit 24c3ab6fe0
5 changed files with 51 additions and 13 deletions

View File

@@ -177,7 +177,8 @@ CLAWDBOT_LIVE_TEST=1 CLAWDBOT_LIVE_SETUP_TOKEN=1 CLAWDBOT_LIVE_SETUP_TOKEN_PROFI
- `CLAWDBOT_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-5"` - `CLAWDBOT_LIVE_CLI_BACKEND_MODEL="claude-cli/claude-opus-4-5"`
- `CLAWDBOT_LIVE_CLI_BACKEND_COMMAND="/full/path/to/claude"` - `CLAWDBOT_LIVE_CLI_BACKEND_COMMAND="/full/path/to/claude"`
- `CLAWDBOT_LIVE_CLI_BACKEND_ARGS='["-p","--output-format","json","--permission-mode","bypassPermissions"]'` - `CLAWDBOT_LIVE_CLI_BACKEND_ARGS='["-p","--output-format","json","--permission-mode","bypassPermissions"]'`
- `CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV='["ANTHROPIC_API_KEY"]'` - `CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV='["ANTHROPIC_API_KEY","ANTHROPIC_API_KEY_OLD"]'`
- `CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG=0` to keep Claude CLI MCP config enabled (default disables MCP config with a temporary empty file).
Example: Example:

View File

@@ -43,7 +43,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = {
systemPromptArg: "--append-system-prompt", systemPromptArg: "--append-system-prompt",
systemPromptMode: "append", systemPromptMode: "append",
systemPromptWhen: "first", systemPromptWhen: "first",
clearEnv: ["ANTHROPIC_API_KEY"], clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"],
serialize: true, serialize: true,
}; };

View File

@@ -455,6 +455,7 @@ export async function runCliAgent(params: {
backend, backend,
prompt: params.prompt, prompt: params.prompt,
}); });
const stdinPayload = stdin ?? "";
const args = buildCliArgs({ const args = buildCliArgs({
backend, backend,
modelId: normalizedModel, modelId: normalizedModel,
@@ -526,7 +527,7 @@ export async function runCliAgent(params: {
timeoutMs: params.timeoutMs, timeoutMs: params.timeoutMs,
cwd: workspaceDir, cwd: workspaceDir,
env, env,
...(stdin ? { input: stdin } : {}), input: stdinPayload,
}); });
const stdout = result.stdout.trim(); const stdout = result.stdout.trim();

View File

@@ -21,7 +21,7 @@ const DEFAULT_ARGS = [
"json", "json",
"--dangerously-skip-permissions", "--dangerously-skip-permissions",
]; ];
const DEFAULT_CLEAR_ENV: string[] = []; const DEFAULT_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"];
function extractPayloadText(result: unknown): string { function extractPayloadText(result: unknown): string {
const record = result as Record<string, unknown>; const record = result as Record<string, unknown>;
@@ -52,6 +52,20 @@ function parseJsonStringArray(
return parsed; return parsed;
} }
function withMcpConfigOverrides(
args: string[],
mcpConfigPath: string,
): string[] {
const next = [...args];
if (!next.includes("--strict-mcp-config")) {
next.push("--strict-mcp-config");
}
if (!next.includes("--mcp-config")) {
next.push("--mcp-config", mcpConfigPath);
}
return next;
}
async function getFreePort(): Promise<number> { async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const srv = createServer(); const srv = createServer();
@@ -134,12 +148,16 @@ describeLive("gateway live (cli backend)", () => {
skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER,
skipCron: process.env.CLAWDBOT_SKIP_CRON, skipCron: process.env.CLAWDBOT_SKIP_CRON,
skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST, skipCanvas: process.env.CLAWDBOT_SKIP_CANVAS_HOST,
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
anthropicApiKeyOld: process.env.ANTHROPIC_API_KEY_OLD,
}; };
process.env.CLAWDBOT_SKIP_PROVIDERS = "1"; process.env.CLAWDBOT_SKIP_PROVIDERS = "1";
process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1"; process.env.CLAWDBOT_SKIP_GMAIL_WATCHER = "1";
process.env.CLAWDBOT_SKIP_CRON = "1"; process.env.CLAWDBOT_SKIP_CRON = "1";
process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1"; process.env.CLAWDBOT_SKIP_CANVAS_HOST = "1";
delete process.env.ANTHROPIC_API_KEY;
delete process.env.ANTHROPIC_API_KEY_OLD;
const token = `test-${randomUUID()}`; const token = `test-${randomUUID()}`;
process.env.CLAWDBOT_GATEWAY_TOKEN = token; process.env.CLAWDBOT_GATEWAY_TOKEN = token;
@@ -156,7 +174,7 @@ describeLive("gateway live (cli backend)", () => {
const cliCommand = const cliCommand =
process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ?? "claude"; process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ?? "claude";
const cliArgs = const baseCliArgs =
parseJsonStringArray( parseJsonStringArray(
"CLAWDBOT_LIVE_CLI_BACKEND_ARGS", "CLAWDBOT_LIVE_CLI_BACKEND_ARGS",
process.env.CLAWDBOT_LIVE_CLI_BACKEND_ARGS, process.env.CLAWDBOT_LIVE_CLI_BACKEND_ARGS,
@@ -167,6 +185,20 @@ describeLive("gateway live (cli backend)", () => {
process.env.CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV, process.env.CLAWDBOT_LIVE_CLI_BACKEND_CLEAR_ENV,
) ?? DEFAULT_CLEAR_ENV; ) ?? DEFAULT_CLEAR_ENV;
const tempDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-live-cli-"),
);
const mcpConfigPath = path.join(tempDir, "claude-mcp.json");
await fs.writeFile(
mcpConfigPath,
`${JSON.stringify({ mcpServers: {} }, null, 2)}\n`,
);
const disableMcpConfig =
process.env.CLAWDBOT_LIVE_CLI_BACKEND_DISABLE_MCP_CONFIG !== "0";
const cliArgs = disableMcpConfig
? withMcpConfigOverrides(baseCliArgs, mcpConfigPath)
: baseCliArgs;
const cfg = loadConfig(); const cfg = loadConfig();
const existingBackends = cfg.agents?.defaults?.cliBackends ?? {}; const existingBackends = cfg.agents?.defaults?.cliBackends ?? {};
const nextCfg = { const nextCfg = {
@@ -185,16 +217,13 @@ describeLive("gateway live (cli backend)", () => {
command: cliCommand, command: cliCommand,
args: cliArgs, args: cliArgs,
clearEnv: cliClearEnv, clearEnv: cliClearEnv,
systemPromptWhen: "never",
}, },
}, },
sandbox: { mode: "off" }, sandbox: { mode: "off" },
}, },
}, },
}; };
const tempDir = await fs.mkdtemp(
path.join(os.tmpdir(), "clawdbot-live-cli-"),
);
const tempConfigPath = path.join(tempDir, "clawdbot.json"); const tempConfigPath = path.join(tempDir, "clawdbot.json");
await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`); await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`);
process.env.CLAWDBOT_CONFIG_PATH = tempConfigPath; process.env.CLAWDBOT_CONFIG_PATH = tempConfigPath;
@@ -252,6 +281,12 @@ describeLive("gateway live (cli backend)", () => {
if (previous.skipCanvas === undefined) if (previous.skipCanvas === undefined)
delete process.env.CLAWDBOT_SKIP_CANVAS_HOST; delete process.env.CLAWDBOT_SKIP_CANVAS_HOST;
else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas; else process.env.CLAWDBOT_SKIP_CANVAS_HOST = previous.skipCanvas;
if (previous.anthropicApiKey === undefined)
delete process.env.ANTHROPIC_API_KEY;
else process.env.ANTHROPIC_API_KEY = previous.anthropicApiKey;
if (previous.anthropicApiKeyOld === undefined)
delete process.env.ANTHROPIC_API_KEY_OLD;
else process.env.ANTHROPIC_API_KEY_OLD = previous.anthropicApiKeyOld;
} }
}, 60_000); }, 60_000);
}); });

View File

@@ -59,13 +59,14 @@ export async function runCommandWithTimeout(
? { timeoutMs: optionsOrTimeout } ? { timeoutMs: optionsOrTimeout }
: optionsOrTimeout; : optionsOrTimeout;
const { timeoutMs, cwd, input, env } = options; const { timeoutMs, cwd, input, env } = options;
const hasInput = input !== undefined;
// Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed. // Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed.
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const child = spawn(argv[0], argv.slice(1), { const child = spawn(argv[0], argv.slice(1), {
stdio: [input ? "pipe" : "inherit", "pipe", "pipe"], stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"],
cwd, cwd,
env: env ? { ...process.env, ...env } : process.env, env: env ?? process.env,
}); });
let stdout = ""; let stdout = "";
let stderr = ""; let stderr = "";
@@ -74,8 +75,8 @@ export async function runCommandWithTimeout(
child.kill("SIGKILL"); child.kill("SIGKILL");
}, timeoutMs); }, timeoutMs);
if (input && child.stdin) { if (hasInput && child.stdin) {
child.stdin.write(input); child.stdin.write(input ?? "");
child.stdin.end(); child.stdin.end();
} }