From 911fcb47f18049322dc3288046233a5c6bfba241 Mon Sep 17 00:00:00 2001 From: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:22:08 -0400 Subject: [PATCH] fix(models): reflect Claude CLI auth status --- CHANGELOG.md | 1 + extensions/anthropic/index.test.ts | 27 ++++++++++++++++++++++ extensions/anthropic/openclaw.plugin.json | 1 + extensions/anthropic/register.runtime.ts | 21 +++++++++++++++++ src/commands/models/list.status-command.ts | 2 +- src/commands/models/list.status.test.ts | 6 ++++- 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 855bbbf1a80..dca77e4f810 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -370,6 +370,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Models/status: report fresh Claude CLI native auth instead of stale stored `anthropic:claude-cli` profile expiry when local Claude credentials are current. Fixes #71256. Thanks @matthiasjanke. - Packaged installs: preserve package-root runtime dependencies and their exported subpaths when bundled plugin runtime mirrors fall back to copying shared chunks, fixing Windows npm updates that could fail to load copied `dist` modules. - Heartbeat: clamp oversized scheduler delays through the shared safe timer helper, preventing `every` values over Node's timeout cap from becoming a 1 ms crash loop. Fixes #71414. (#71478) Thanks @hclsys. - 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. diff --git a/extensions/anthropic/index.test.ts b/extensions/anthropic/index.test.ts index f7df1c72bad..79a49852b79 100644 --- a/extensions/anthropic/index.test.ts +++ b/extensions/anthropic/index.test.ts @@ -332,6 +332,33 @@ describe("anthropic provider replay hooks", () => { }); }); + it("exposes Claude CLI auth as a runtime-only external profile", async () => { + readClaudeCliCredentialsForRuntimeMock.mockReset(); + readClaudeCliCredentialsForRuntimeMock.mockReturnValue({ + type: "oauth", + provider: "anthropic", + access: "fresh-cli-access", + refresh: "fresh-cli-refresh", + expires: 123, + }); + + const provider = await registerSingleProviderPlugin(anthropicPlugin); + + expect(provider.resolveExternalAuthProfiles?.({} as never)).toEqual([ + { + profileId: "anthropic:claude-cli", + credential: { + type: "oauth", + provider: "claude-cli", + access: "fresh-cli-access", + refresh: "fresh-cli-refresh", + expires: 123, + }, + persistence: "runtime-only", + }, + ]); + }); + it("stores a claude-cli auth profile during anthropic cli migration", async () => { readClaudeCliCredentialsForSetupMock.mockReset(); readClaudeCliCredentialsForSetupMock.mockReturnValue({ diff --git a/extensions/anthropic/openclaw.plugin.json b/extensions/anthropic/openclaw.plugin.json index 27ba950e472..93ae2875e32 100644 --- a/extensions/anthropic/openclaw.plugin.json +++ b/extensions/anthropic/openclaw.plugin.json @@ -49,6 +49,7 @@ } ], "contracts": { + "externalAuthProviders": ["claude-cli"], "mediaUnderstandingProviders": ["anthropic"] }, "mediaUnderstandingProviderMetadata": { diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index 3e5b2074d4a..3896319df62 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -422,6 +422,26 @@ function resolveClaudeCliSyntheticAuth() { }; } +function resolveClaudeCliExternalAuthProfiles() { + const credential = claudeCliAuth.readClaudeCliCredentialsForRuntime(); + if (!credential || credential.type !== "oauth") { + return []; + } + return [ + { + profileId: "anthropic:claude-cli", + credential: { + type: "oauth" as const, + provider: CLAUDE_CLI_BACKEND_ID, + access: credential.access, + refresh: credential.refresh, + expires: credential.expires, + }, + persistence: "runtime-only" as const, + }, + ]; +} + async function runAnthropicCliMigration(ctx: ProviderAuthContext): Promise { const credential = claudeCliAuth.readClaudeCliCredentialsForSetup(); if (!credential) { @@ -587,6 +607,7 @@ export function buildAnthropicProvider(): ProviderPlugin { normalizeLowercaseStringOrEmpty(provider) === CLAUDE_CLI_BACKEND_ID ? resolveClaudeCliSyntheticAuth() : undefined, + resolveExternalAuthProfiles: () => resolveClaudeCliExternalAuthProfiles(), buildReplayPolicy: buildAnthropicReplayPolicy, isModernModelRef: ({ modelId }) => matchesAnthropicModernModel(modelId), resolveReasoningOutputMode: () => "native", diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index ed5ac8b1ad9..db30426f33b 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -11,7 +11,7 @@ import { formatRemainingShort, } from "../../agents/auth-health.js"; import { resolveAuthStorePathForDisplay } from "../../agents/auth-profiles/paths.js"; -import { ensureAuthProfileStoreWithoutExternalProfiles as ensureAuthProfileStore } from "../../agents/auth-profiles/store.js"; +import { ensureAuthProfileStore } from "../../agents/auth-profiles/store.js"; import { resolveProfileUnusableUntilForDisplay } from "../../agents/auth-profiles/usage.js"; import { resolveProviderEnvApiKeyCandidates } from "../../agents/model-auth-env-vars.js"; import { resolveEnvApiKey } from "../../agents/model-auth.js"; diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index 16ba9c450a2..85de69c5d0a 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -43,6 +43,7 @@ const mocks = vi.hoisted(() => { resolveAgentModelFallbacksOverride: vi.fn().mockReturnValue(undefined), listAgentIds: vi.fn().mockReturnValue(["main", "jeremiah"]), ensureAuthProfileStore: vi.fn().mockReturnValue(store), + ensureAuthProfileStoreWithoutExternalProfiles: vi.fn().mockReturnValue(store), listProfilesForProvider: vi.fn((s: typeof store, provider: string) => { return Object.entries(s.profiles) .filter(([, cred]) => cred.provider === provider) @@ -146,7 +147,8 @@ vi.mock("../../agents/auth-profiles/profiles.js", () => ({ })); vi.mock("../../agents/auth-profiles/store.js", () => ({ ensureAuthProfileStore: mocks.ensureAuthProfileStore, - ensureAuthProfileStoreWithoutExternalProfiles: mocks.ensureAuthProfileStore, + ensureAuthProfileStoreWithoutExternalProfiles: + mocks.ensureAuthProfileStoreWithoutExternalProfiles, })); vi.mock("../../agents/auth-profiles/usage.js", () => ({ resolveProfileUnusableUntilForDisplay: mocks.resolveProfileUnusableUntilForDisplay, @@ -284,6 +286,8 @@ describe("modelsStatusCommand auth overview", () => { const payload = JSON.parse(String((runtime.log as Mock).mock.calls[0]?.[0])); expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled(); + expect(mocks.ensureAuthProfileStore).toHaveBeenCalled(); + expect(mocks.ensureAuthProfileStoreWithoutExternalProfiles).not.toHaveBeenCalled(); expect(payload.defaultModel).toBe("anthropic/claude-opus-4-6"); expect(payload.configPath).toBe("/tmp/openclaw-dev/openclaw.json"); expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json");