diff --git a/CHANGELOG.md b/CHANGELOG.md index 9afc20b9d93..17e55022360 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai - Agents/status: use provider-aware context window lookup for fresh Anthropic 4.6 model overrides so `/status` shows the correct 1.0m window instead of an underreported shared-cache minimum. (#54796) Thanks @neeravmakwana. - Agents/errors: surface provider quota/reset details when available, but keep HTML/Cloudflare rate-limit pages on the generic fallback so raw error pages are not shown to users. (#54512) Thanks @bugkill3r. - Claude CLI: switch the bundled Claude CLI backend to `stream-json` output so watchdogs see progress on long runs, and keep session/usage metadata even when Claude finishes with an empty result line. (#49698) Thanks @felear2022. +- Claude CLI/MCP: always pass a strict generated `--mcp-config` overlay for background Claude CLI runs, including the empty-server case, so Claude does not inherit ambient user/global MCP servers. (#54961) Thanks @markojak. - Agents/embedded replies: surface mid-turn 429 and overload failures when embedded runs end without a user-visible reply, while preserving successful media-only replies that still use legacy `mediaUrl`. (#50930) Thanks @infichen. - WhatsApp/allowFrom: show a specific allowFrom policy error for valid blocked targets instead of the misleading `` format hint. Thanks @mcaxtr. - Agents/cooldowns: scope rate-limit cooldowns per model so one 429 no longer blocks every model on the same auth profile, replace the exponential 1 min -> 1 h escalation with a stepped 30 s / 1 min / 5 min ladder, and surface a user-facing countdown message when all models are rate-limited. (#49834) Thanks @kiranvk-2011. diff --git a/src/agents/cli-runner.test.ts b/src/agents/cli-runner.test.ts index ad3676eaa94..cd1ab119952 100644 --- a/src/agents/cli-runner.test.ts +++ b/src/agents/cli-runner.test.ts @@ -272,6 +272,56 @@ describe("runCliAgent with process supervisor", () => { expect(allArgs).toContain("You are a helpful assistant."); }); + it("injects a strict empty MCP config for bundle-MCP-enabled Claude CLI runs", async () => { + supervisorSpawnMock.mockResolvedValueOnce( + createManagedRun({ + reason: "exit", + exitCode: 0, + exitSignal: null, + durationMs: 50, + stdout: JSON.stringify({ + session_id: "session-123", + message: "ok", + }), + stderr: "", + timedOut: false, + noOutputTimedOut: false, + }), + ); + + await runCliAgent({ + sessionId: "s1", + sessionFile: "/tmp/session.jsonl", + workspaceDir: "/tmp", + config: { + agents: { + defaults: { + cliBackends: { + "claude-cli": { + command: "node", + args: ["/tmp/fake-claude.mjs"], + clearEnv: [], + }, + }, + }, + }, + } satisfies OpenClawConfig, + prompt: "hi", + provider: "claude-cli", + model: "claude-sonnet-4-6", + timeoutMs: 1_000, + runId: "run-bundle-mcp-empty", + }); + + const input = supervisorSpawnMock.mock.calls[0]?.[0] as { argv?: string[] }; + expect(input.argv?.[0]).toBe("node"); + expect(input.argv).toContain("/tmp/fake-claude.mjs"); + expect(input.argv).toContain("--strict-mcp-config"); + const configFlagIndex = input.argv?.indexOf("--mcp-config") ?? -1; + expect(configFlagIndex).toBeGreaterThanOrEqual(0); + expect(input.argv?.[configFlagIndex + 1]).toMatch(/^\/.+\/mcp\.json$/); + }); + it("runs CLI through supervisor and returns payload", async () => { supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ diff --git a/src/agents/cli-runner/bundle-mcp.test.ts b/src/agents/cli-runner/bundle-mcp.test.ts index bc98319abc2..4760e2f66e4 100644 --- a/src/agents/cli-runner/bundle-mcp.test.ts +++ b/src/agents/cli-runner/bundle-mcp.test.ts @@ -15,6 +15,32 @@ afterEach(async () => { }); describe("prepareCliBundleMcpConfig", () => { + it("injects a strict empty --mcp-config overlay for bundle-MCP-enabled backends without servers", async () => { + const workspaceDir = await tempHarness.createTempDir("openclaw-cli-bundle-mcp-empty-"); + + const prepared = await prepareCliBundleMcpConfig({ + enabled: true, + backend: { + command: "node", + args: ["./fake-claude.mjs"], + }, + workspaceDir, + config: {}, + }); + + const configFlagIndex = prepared.backend.args?.indexOf("--mcp-config") ?? -1; + expect(configFlagIndex).toBeGreaterThanOrEqual(0); + expect(prepared.backend.args).toContain("--strict-mcp-config"); + const generatedConfigPath = prepared.backend.args?.[configFlagIndex + 1]; + expect(typeof generatedConfigPath).toBe("string"); + const raw = JSON.parse(await fs.readFile(generatedConfigPath as string, "utf-8")) as { + mcpServers?: Record; + }; + expect(raw.mcpServers).toEqual({}); + + await prepared.cleanup?.(); + }); + it("injects a merged --mcp-config overlay for bundle-MCP-enabled backends", async () => { const env = captureEnv(["HOME"]); try { diff --git a/src/agents/cli-runner/bundle-mcp.ts b/src/agents/cli-runner/bundle-mcp.ts index e5dcd545766..3d450eabe9c 100644 --- a/src/agents/cli-runner/bundle-mcp.ts +++ b/src/agents/cli-runner/bundle-mcp.ts @@ -98,10 +98,9 @@ export async function prepareCliBundleMcpConfig(params: { } mergedConfig = applyMergePatch(mergedConfig, bundleConfig.config) as BundleMcpConfig; - if (Object.keys(mergedConfig.mcpServers).length === 0) { - return { backend: params.backend }; - } - + // Always pass an explicit strict MCP config for background claude-cli runs. + // Otherwise Claude may inherit ambient user/global MCP servers (for example + // Playwright) and spawn unexpected background processes. const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-mcp-")); const mcpConfigPath = path.join(tempDir, "mcp.json"); const serializedConfig = `${JSON.stringify(mergedConfig, null, 2)}\n`;