diff --git a/docs/testing.md b/docs/testing.md index e0c3a870c..4cb6d74cb 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -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_COMMAND="/full/path/to/claude"` - `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: diff --git a/src/agents/cli-backends.ts b/src/agents/cli-backends.ts index 3fc90b917..2cfb4085e 100644 --- a/src/agents/cli-backends.ts +++ b/src/agents/cli-backends.ts @@ -43,7 +43,7 @@ const DEFAULT_CLAUDE_BACKEND: CliBackendConfig = { systemPromptArg: "--append-system-prompt", systemPromptMode: "append", systemPromptWhen: "first", - clearEnv: ["ANTHROPIC_API_KEY"], + clearEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"], serialize: true, }; diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index f3d6b8a8d..00f26e350 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -455,6 +455,7 @@ export async function runCliAgent(params: { backend, prompt: params.prompt, }); + const stdinPayload = stdin ?? ""; const args = buildCliArgs({ backend, modelId: normalizedModel, @@ -526,7 +527,7 @@ export async function runCliAgent(params: { timeoutMs: params.timeoutMs, cwd: workspaceDir, env, - ...(stdin ? { input: stdin } : {}), + input: stdinPayload, }); const stdout = result.stdout.trim(); diff --git a/src/gateway/gateway-cli-backend.live.test.ts b/src/gateway/gateway-cli-backend.live.test.ts index a1219449d..c51c88fa4 100644 --- a/src/gateway/gateway-cli-backend.live.test.ts +++ b/src/gateway/gateway-cli-backend.live.test.ts @@ -21,7 +21,7 @@ const DEFAULT_ARGS = [ "json", "--dangerously-skip-permissions", ]; -const DEFAULT_CLEAR_ENV: string[] = []; +const DEFAULT_CLEAR_ENV = ["ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY_OLD"]; function extractPayloadText(result: unknown): string { const record = result as Record; @@ -52,6 +52,20 @@ function parseJsonStringArray( 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 { return await new Promise((resolve, reject) => { const srv = createServer(); @@ -134,12 +148,16 @@ describeLive("gateway live (cli backend)", () => { skipGmail: process.env.CLAWDBOT_SKIP_GMAIL_WATCHER, skipCron: process.env.CLAWDBOT_SKIP_CRON, 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_GMAIL_WATCHER = "1"; process.env.CLAWDBOT_SKIP_CRON = "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()}`; process.env.CLAWDBOT_GATEWAY_TOKEN = token; @@ -156,7 +174,7 @@ describeLive("gateway live (cli backend)", () => { const cliCommand = process.env.CLAWDBOT_LIVE_CLI_BACKEND_COMMAND ?? "claude"; - const cliArgs = + const baseCliArgs = parseJsonStringArray( "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, ) ?? 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 existingBackends = cfg.agents?.defaults?.cliBackends ?? {}; const nextCfg = { @@ -185,16 +217,13 @@ describeLive("gateway live (cli backend)", () => { command: cliCommand, args: cliArgs, clearEnv: cliClearEnv, + systemPromptWhen: "never", }, }, sandbox: { mode: "off" }, }, }, }; - - const tempDir = await fs.mkdtemp( - path.join(os.tmpdir(), "clawdbot-live-cli-"), - ); const tempConfigPath = path.join(tempDir, "clawdbot.json"); await fs.writeFile(tempConfigPath, `${JSON.stringify(nextCfg, null, 2)}\n`); process.env.CLAWDBOT_CONFIG_PATH = tempConfigPath; @@ -252,6 +281,12 @@ describeLive("gateway live (cli backend)", () => { if (previous.skipCanvas === undefined) delete process.env.CLAWDBOT_SKIP_CANVAS_HOST; 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); }); diff --git a/src/process/exec.ts b/src/process/exec.ts index f0ea1c5a4..19a19ac2b 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -59,13 +59,14 @@ export async function runCommandWithTimeout( ? { timeoutMs: optionsOrTimeout } : optionsOrTimeout; const { timeoutMs, cwd, input, env } = options; + const hasInput = input !== undefined; // Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed. return await new Promise((resolve, reject) => { const child = spawn(argv[0], argv.slice(1), { - stdio: [input ? "pipe" : "inherit", "pipe", "pipe"], + stdio: [hasInput ? "pipe" : "inherit", "pipe", "pipe"], cwd, - env: env ? { ...process.env, ...env } : process.env, + env: env ?? process.env, }); let stdout = ""; let stderr = ""; @@ -74,8 +75,8 @@ export async function runCommandWithTimeout( child.kill("SIGKILL"); }, timeoutMs); - if (input && child.stdin) { - child.stdin.write(input); + if (hasInput && child.stdin) { + child.stdin.write(input ?? ""); child.stdin.end(); }