fix(claude-cli): harden gateway auth env

This commit is contained in:
Peter Steinberger
2026-04-10 08:10:05 +01:00
parent 7e2a1db53b
commit 7e7a8d6b0f
7 changed files with 106 additions and 14 deletions

View File

@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
- Gateway/agents: preserve configured model selection and richer `IDENTITY.md` content across agent create/update flows and workspace moves, and fail safely instead of silently overwriting unreadable identity files. (#61577) Thanks @samzong.
- Windows/exec: settle supervisor waits from child exit state after stdout and stderr drain even when `close` never arrives, so CLI commands stop hanging or dying with forced `SIGKILL` on Windows. (#64072) Thanks @obviyus.
- Agents/timeouts: extend the default LLM idle window to 120s and keep silent no-token idle timeouts on recovery paths, so slow models can retry or fall back before users see an error.
- Claude CLI: clear inherited Anthropic auth/header environment aliases before spawning Claude Code and add sanitized CLI backend auth-env diagnostics for debugging gateway-run provider selection.
## 2026.4.9
@@ -490,7 +491,6 @@ Docs: https://docs.openclaw.ai
- Matrix: avoid failing startup when token auth already knows the user ID but still needs optional device metadata, retry transient auth bootstrap requests, and backfill missing device IDs after startup while keeping unknown-device storage reuse conservative until metadata is repaired. (#61383) Thanks @gumadeiras.
- Agents/exec: stop streaming `tool_execution_update` events after an exec session backgrounds, preventing delayed background output from hitting a stale listener and crashing the gateway while keeping the output available through `process poll/log`. (#61627) Thanks @openperf.
- Matrix: pass configured `deviceId` through health probes and keep probe-only client setup out of durable Matrix storage, so health checks preserve the correct device identity without rewriting `storage-meta.json` or related probe state on disk. (#61581) Thanks @MoerAI.
||||||| parent of b4694a4ac7 (Telegram: add outbound chunker regression coverage)
- Image generation/build: write stable runtime alias files into `dist/` and route provider-auth runtime lookups through those aliases so image-generation providers keep resolving auth/runtime modules after rebuilds instead of crashing on missing hashed chunk files.
- Config/runtime: pin the first successful config load in memory for the running process and refresh that snapshot on successful writes/reloads, so hot paths stop reparsing `openclaw.json` between watcher-driven swaps.
- Config/legacy cleanup: stop probing obsolete alternate legacy config names and service labels during local config/service detection, while keeping the active `~/.openclaw/openclaw.json` path canonical.

View File

@@ -141,7 +141,10 @@ describe("normalizeClaudeBackendConfig", () => {
expect(backend.config.resumeArgs).toContain("--setting-sources");
expect(backend.config.resumeArgs).toContain("user");
expect(backend.config.clearEnv).toEqual([...CLAUDE_CLI_CLEAR_ENV]);
expect(backend.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
expect(backend.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
expect(backend.config.clearEnv).toContain("ANTHROPIC_CUSTOM_HEADERS");
expect(backend.config.clearEnv).toContain("ANTHROPIC_OAUTH_TOKEN");
expect(backend.config.clearEnv).toContain("CLAUDE_CONFIG_DIR");
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_USE_BEDROCK");
expect(backend.config.clearEnv).toContain("CLAUDE_CODE_OAUTH_TOKEN");

View File

@@ -51,8 +51,11 @@ export const CLAUDE_CLI_HOST_MANAGED_ENV = {
export const CLAUDE_CLI_CLEAR_ENV = [
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD",
"ANTHROPIC_API_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_OAUTH_TOKEN",
"ANTHROPIC_UNIX_SOCKET",
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",

View File

@@ -145,8 +145,11 @@ beforeEach(() => {
clearEnv: [
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD",
"ANTHROPIC_API_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_OAUTH_TOKEN",
"ANTHROPIC_UNIX_SOCKET",
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",
@@ -362,7 +365,10 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
expect(resolved?.config.resumeArgs).toContain("--permission-mode");
expect(resolved?.config.resumeArgs).toContain("bypassPermissions");
expect(resolved?.config.env).toEqual({ CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1" });
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_CUSTOM_HEADERS");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_OAUTH_TOKEN");
expect(resolved?.config.clearEnv).toContain("CLAUDE_CONFIG_DIR");
expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_OAUTH_TOKEN");
expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_PLUGIN_CACHE_DIR");
@@ -579,6 +585,9 @@ describe("resolveCliBackendConfig claude-cli defaults", () => {
ANTHROPIC_BASE_URL: "https://evil.example.com/v1",
});
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_BASE_URL");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_API_TOKEN");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_CUSTOM_HEADERS");
expect(resolved?.config.clearEnv).toContain("ANTHROPIC_OAUTH_TOKEN");
expect(resolved?.config.clearEnv).toContain("CLAUDE_CONFIG_DIR");
expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_OAUTH_TOKEN");
expect(resolved?.config.clearEnv).toContain("CLAUDE_CODE_PLUGIN_CACHE_DIR");

View File

@@ -13,7 +13,7 @@ import {
restoreCliRunnerPrepareTestDeps,
supervisorSpawnMock,
} from "./cli-runner.test-support.js";
import { executePreparedCliRun } from "./cli-runner/execute.js";
import { buildCliEnvAuthLog, executePreparedCliRun } from "./cli-runner/execute.js";
import { buildSystemPrompt } from "./cli-runner/helpers.js";
import { setCliRunnerPrepareTestDeps } from "./cli-runner/prepare.js";
import type { PreparedCliRunContext } from "./cli-runner/types.js";
@@ -560,6 +560,9 @@ describe("runCliAgent spawn path", () => {
it("clears claude-cli provider-routing, auth, and telemetry env while keeping host-managed hardening", async () => {
vi.stubEnv("ANTHROPIC_BASE_URL", "https://proxy.example.com/v1");
vi.stubEnv("ANTHROPIC_API_TOKEN", "env-api-token");
vi.stubEnv("ANTHROPIC_CUSTOM_HEADERS", "x-test-header: env");
vi.stubEnv("ANTHROPIC_OAUTH_TOKEN", "env-oauth-token");
vi.stubEnv("CLAUDE_CODE_USE_BEDROCK", "1");
vi.stubEnv("ANTHROPIC_AUTH_TOKEN", "env-auth-token");
vi.stubEnv("CLAUDE_CODE_OAUTH_TOKEN", "env-oauth-token");
@@ -586,6 +589,9 @@ describe("runCliAgent spawn path", () => {
},
clearEnv: [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_API_TOKEN",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_OAUTH_TOKEN",
"CLAUDE_CODE_USE_BEDROCK",
"ANTHROPIC_AUTH_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
@@ -607,6 +613,9 @@ describe("runCliAgent spawn path", () => {
expect(input.env?.SAFE_KEEP).toBe("ok");
expect(input.env?.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST).toBe("1");
expect(input.env?.ANTHROPIC_BASE_URL).toBe("https://override.example.com/v1");
expect(input.env?.ANTHROPIC_API_TOKEN).toBeUndefined();
expect(input.env?.ANTHROPIC_CUSTOM_HEADERS).toBeUndefined();
expect(input.env?.ANTHROPIC_OAUTH_TOKEN).toBeUndefined();
expect(input.env?.CLAUDE_CODE_USE_BEDROCK).toBeUndefined();
expect(input.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
expect(input.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("override-oauth-token");
@@ -619,6 +628,29 @@ describe("runCliAgent spawn path", () => {
expect(input.env?.OTEL_SDK_DISABLED).toBeUndefined();
});
it("formats CLI auth env diagnostics as key names without secret values", () => {
vi.stubEnv("ANTHROPIC_API_KEY", "sk-ant-host");
vi.stubEnv("ANTHROPIC_API_TOKEN", "token-host");
vi.stubEnv("OPENAI_API_KEY", "sk-openai-host");
const log = buildCliEnvAuthLog({
ANTHROPIC_API_TOKEN: "token-child",
CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1",
OPENAI_API_KEY: "sk-openai-child",
});
expect(log).toMatch(/host=.*ANTHROPIC_API_KEY/);
expect(log).toMatch(/host=.*ANTHROPIC_API_TOKEN/);
expect(log).toMatch(/host=.*OPENAI_API_KEY/);
expect(log).toMatch(/child=.*ANTHROPIC_API_TOKEN/);
expect(log).toMatch(/child=.*CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST/);
expect(log).toMatch(/child=.*OPENAI_API_KEY/);
expect(log).toMatch(/cleared=.*ANTHROPIC_API_KEY/);
expect(log).not.toContain("sk-ant-host");
expect(log).not.toContain("token-child");
expect(log).not.toContain("sk-openai-child");
});
it("prepends bootstrap warnings to the CLI prompt body", async () => {
supervisorSpawnMock.mockResolvedValueOnce(
createManagedRun({

View File

@@ -156,8 +156,11 @@ function buildAnthropicCliBackendFixture(): CliBackendPlugin {
const clearEnv = [
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD",
"ANTHROPIC_API_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_OAUTH_TOKEN",
"ANTHROPIC_UNIX_SOCKET",
"CLAUDE_CONFIG_DIR",
"CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR",

View File

@@ -93,6 +93,47 @@ function buildCliLogArgs(params: {
return logArgs;
}
const CLI_ENV_AUTH_LOG_KEYS = [
"AI_GATEWAY_API_KEY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY_OLD",
"ANTHROPIC_API_TOKEN",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_CUSTOM_HEADERS",
"ANTHROPIC_OAUTH_TOKEN",
"ANTHROPIC_UNIX_SOCKET",
"AZURE_OPENAI_API_KEY",
"CLAUDE_CODE_OAUTH_TOKEN",
"CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST",
"OPENAI_API_KEY",
"OPENAI_STEIPETE_API_KEY",
"OPENROUTER_API_KEY",
] as const;
function listPresentCliAuthEnvKeys(env: Record<string, string | undefined>): string[] {
return CLI_ENV_AUTH_LOG_KEYS.filter((key) => {
const value = env[key];
return typeof value === "string" && value.length > 0;
});
}
function formatCliEnvKeyList(keys: readonly string[]): string {
return keys.length > 0 ? keys.join(",") : "none";
}
export function buildCliEnvAuthLog(childEnv: Record<string, string>): string {
const hostKeys = listPresentCliAuthEnvKeys(process.env);
const childKeys = listPresentCliAuthEnvKeys(childEnv);
const childKeySet = new Set(childKeys);
const clearedKeys = hostKeys.filter((key) => !childKeySet.has(key));
return [
`host=${formatCliEnvKeyList(hostKeys)}`,
`child=${formatCliEnvKeyList(childKeys)}`,
`cleared=${formatCliEnvKeyList(clearedKeys)}`,
].join(" ");
}
export async function executePreparedCliRun(
context: PreparedCliRunContext,
cliSessionIdToUse?: string,
@@ -174,18 +215,6 @@ export async function executePreparedCliRun(
const logOutputText =
isTruthyEnvValue(process.env[CLI_BACKEND_LOG_OUTPUT_ENV]) ||
isTruthyEnvValue(process.env[LEGACY_CLAUDE_CLI_LOG_OUTPUT_ENV]);
if (logOutputText) {
const logArgs = buildCliLogArgs({
args,
systemPromptArg: backend.systemPromptArg,
sessionArg: backend.sessionArg,
modelArg: backend.modelArg,
imageArg: backend.imageArg,
argsPrompt,
});
cliBackendLog.info(`cli argv: ${backend.command} ${logArgs.join(" ")}`);
}
const env = (() => {
const next = sanitizeHostExecEnv({
baseEnv: process.env,
@@ -207,6 +236,19 @@ export async function executePreparedCliRun(
Object.assign(next, context.preparedBackend.env);
return next;
})();
if (logOutputText) {
const logArgs = buildCliLogArgs({
args,
systemPromptArg: backend.systemPromptArg,
sessionArg: backend.sessionArg,
modelArg: backend.modelArg,
imageArg: backend.imageArg,
argsPrompt,
});
cliBackendLog.info(`cli argv: ${backend.command} ${logArgs.join(" ")}`);
cliBackendLog.info(`cli env auth: ${buildCliEnvAuthLog(env)}`);
}
const noOutputTimeoutMs = resolveCliNoOutputTimeoutMs({
backend,
timeoutMs: params.timeoutMs,