mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 13:11:40 +00:00
fix(claude-cli): harden gateway auth env
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user