fix: sync Claude CLI OAuth credentials (#70902) (thanks @starvex)

This commit is contained in:
Roman Godz
2026-04-25 16:54:25 +05:30
committed by Ayaan Zaidi
parent 84dc9f12f1
commit 150f3e472b
13 changed files with 67 additions and 1 deletions

View File

@@ -79,6 +79,7 @@ Docs: https://docs.openclaw.ai
- Daemon/service-env: add Nix Home Manager profile bin directories to generated gateway service PATHs on macOS and Linux, honoring `NIX_PROFILES` right-to-left precedence and falling back to `~/.nix-profile/bin` when unset. Fixes #44402. (#59935) Thanks @jerome-benoit.
- Agents/heartbeat: stop injecting the heartbeat system prompt into non-heartbeat runs, preventing ordinary user replies from being suppressed as `HEARTBEAT_OK` acknowledgments. Fixes #69079. (#69278) Thanks @stainlu.
- Active Memory: keep silent recall sub-agent billing/auth failures out of shared auth-profile cooldown state, so a Claude CLI extra-usage rejection cannot disable normal Claude-backed turns. Fixes #71284. (#71539) Thanks @vishutdhar and @obviyus.
- Auth/Claude CLI: sync refreshed Claude CLI OAuth credentials into the managed auth profile so long-running Claude CLI runs stop falling back to stale OpenClaw snapshots. (#70902) Thanks @starvex.
## 2026.4.25 (Unreleased)

View File

@@ -6,6 +6,7 @@ const { readCodexCliCredentialsCachedMock } = vi.hoisted(() => ({
}));
vi.mock("./cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: readCodexCliCredentialsCachedMock,
readMiniMaxCliCredentialsCached: () => null,
resetCliCredentialCachesForTest: () => undefined,

View File

@@ -21,6 +21,7 @@ vi.mock("../plugins/provider-runtime.js", () => ({
}));
vi.mock("./cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: () => {
const codexHome = process.env.CODEX_HOME;
if (!codexHome) {

View File

@@ -1,7 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { AuthProfileStore, OAuthCredential } from "./auth-profiles/types.js";
import type { ClaudeCliCredential } from "./cli-credentials.js";
const mocks = vi.hoisted(() => ({
readClaudeCliCredentialsCached: vi.fn<() => ClaudeCliCredential | null>(() => null),
readCodexCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
readMiniMaxCliCredentialsCached: vi.fn<() => OAuthCredential | null>(() => null),
}));
@@ -12,6 +14,7 @@ let hasUsableOAuthCredential: typeof import("./auth-profiles/external-cli-sync.j
let isSafeToUseExternalCliCredential: typeof import("./auth-profiles/external-cli-sync.js").isSafeToUseExternalCliCredential;
let shouldBootstrapFromExternalCliCredential: typeof import("./auth-profiles/external-cli-sync.js").shouldBootstrapFromExternalCliCredential;
let shouldReplaceStoredOAuthCredential: typeof import("./auth-profiles/external-cli-sync.js").shouldReplaceStoredOAuthCredential;
let CLAUDE_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").CLAUDE_CLI_PROFILE_ID;
let OPENAI_CODEX_DEFAULT_PROFILE_ID: typeof import("./auth-profiles/constants.js").OPENAI_CODEX_DEFAULT_PROFILE_ID;
let MINIMAX_CLI_PROFILE_ID: typeof import("./auth-profiles/constants.js").MINIMAX_CLI_PROFILE_ID;
@@ -42,9 +45,11 @@ describe("external cli oauth resolution", () => {
beforeEach(async () => {
vi.resetModules();
vi.doMock("./cli-credentials.js", () => ({
readClaudeCliCredentialsCached: mocks.readClaudeCliCredentialsCached,
readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached,
readMiniMaxCliCredentialsCached: mocks.readMiniMaxCliCredentialsCached,
}));
mocks.readClaudeCliCredentialsCached.mockReset().mockReturnValue(null);
mocks.readCodexCliCredentialsCached.mockReset().mockReturnValue(null);
mocks.readMiniMaxCliCredentialsCached.mockReset().mockReturnValue(null);
({
@@ -55,7 +60,7 @@ describe("external cli oauth resolution", () => {
shouldBootstrapFromExternalCliCredential,
shouldReplaceStoredOAuthCredential,
} = await import("./auth-profiles/external-cli-sync.js"));
({ OPENAI_CODEX_DEFAULT_PROFILE_ID, MINIMAX_CLI_PROFILE_ID } =
({ CLAUDE_CLI_PROFILE_ID, OPENAI_CODEX_DEFAULT_PROFILE_ID, MINIMAX_CLI_PROFILE_ID } =
await import("./auth-profiles/constants.js"));
});
@@ -302,6 +307,43 @@ describe("external cli oauth resolution", () => {
expect(credential).toBeNull();
});
it("normalizes Claude CLI oauth credentials into the managed Claude profile", () => {
mocks.readClaudeCliCredentialsCached.mockReturnValue({
type: "oauth",
provider: "anthropic",
access: "claude-cli-access",
refresh: "claude-cli-refresh",
expires: Date.now() + 5 * 24 * 60 * 60_000,
});
const profiles = resolveExternalCliAuthProfiles(makeStore());
expect(profiles).toEqual([
{
profileId: CLAUDE_CLI_PROFILE_ID,
credential: expect.objectContaining({
type: "oauth",
provider: "claude-cli",
access: "claude-cli-access",
refresh: "claude-cli-refresh",
}),
},
]);
});
it("ignores Claude CLI token credentials", () => {
mocks.readClaudeCliCredentialsCached.mockReturnValue({
type: "token",
provider: "anthropic",
token: "claude-cli-token",
expires: Date.now() + 5 * 24 * 60 * 60_000,
});
const profiles = resolveExternalCliAuthProfiles(makeStore());
expect(profiles).toEqual([]);
});
it("resolves fresher minimax external oauth profiles as runtime overlays", () => {
mocks.readMiniMaxCliCredentialsCached.mockReturnValue(
makeOAuthCredential({

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("./cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: () => null,
readMiniMaxCliCredentialsCached: () => null,
}));

View File

@@ -1,8 +1,10 @@
import {
readClaudeCliCredentialsCached,
readCodexCliCredentialsCached,
readMiniMaxCliCredentialsCached,
} from "../cli-credentials.js";
import {
CLAUDE_CLI_PROFILE_ID,
EXTERNAL_CLI_SYNC_TTL_MS,
MINIMAX_CLI_PROFILE_ID,
OPENAI_CODEX_DEFAULT_PROFILE_ID,
@@ -91,6 +93,17 @@ const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [
readCredentials: () => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
bootstrapOnly: true,
},
{
profileId: CLAUDE_CLI_PROFILE_ID,
provider: "claude-cli",
readCredentials: () => {
const credential = readClaudeCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS });
if (credential?.type !== "oauth") {
return null;
}
return { ...credential, provider: "claude-cli" };
},
},
{
profileId: MINIMAX_CLI_PROFILE_ID,
provider: "minimax-portal",

View File

@@ -15,6 +15,7 @@ const readCodexCliCredentialsCachedMock = vi.hoisted(() =>
);
vi.mock("../cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: readCodexCliCredentialsCachedMock,
readMiniMaxCliCredentialsCached: () => null,
}));

View File

@@ -13,6 +13,7 @@ export function getOAuthProviderRuntimeMocks() {
}
vi.mock("../cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: () => null,
readMiniMaxCliCredentialsCached: () => null,
resetCliCredentialCachesForTest: () => undefined,

View File

@@ -19,6 +19,7 @@ vi.mock("@mariozechner/pi-ai/oauth", () => ({
}));
vi.mock("../cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: () => null,
readMiniMaxCliCredentialsCached: () => null,
resetCliCredentialCachesForTest: () => undefined,

View File

@@ -37,6 +37,7 @@ const {
}));
vi.mock("../cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: readCodexCliCredentialsCachedMock,
readMiniMaxCliCredentialsCached: () => null,
resetCliCredentialCachesForTest: () => undefined,

View File

@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../../config/config.js";
import type { AuthProfileStore } from "./types.js";
vi.mock("../cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: () => null,
readMiniMaxCliCredentialsCached: () => null,
resetCliCredentialCachesForTest: () => undefined,

View File

@@ -23,6 +23,7 @@ vi.mock("./model-auth.js", () => ({
}));
vi.mock("./cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: mocks.readCodexCliCredentialsCached,
}));

View File

@@ -160,6 +160,7 @@ vi.mock("../plugins/providers.js", () => ({
}));
vi.mock("./cli-credentials.js", () => ({
readClaudeCliCredentialsCached: () => null,
readCodexCliCredentialsCached: () => null,
readMiniMaxCliCredentialsCached: () => null,
}));