From b6e9473e9f2bac7a9dca1d66ef1b7609b8e8cb67 Mon Sep 17 00:00:00 2001 From: zhang-guiping Date: Mon, 1 Jun 2026 01:26:03 +0800 Subject: [PATCH] fix(auth): skip Anthropic API keys for usage status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #85124. Anthropic standard API keys no longer resolve as provider usage auth for `openclaw status --usage`, so valid inference keys are not sent to Anthropic's OAuth usage endpoint and surfaced as misleading invalid bearer-token errors. The provider usage-auth SDK result now has an explicit handled/no-token shape so provider hooks can suppress generic fallback without widening the OAuth helper contract. Docs, Plugin SDK API baseline, and extension package-boundary cache inputs were updated with the new contract. Thanks @zhangguiping-xydt. Proof: - node scripts/run-vitest.mjs src/infra/provider-usage.auth.normalizes-keys.test.ts src/infra/provider-usage.auth.plugin.test.ts extensions/anthropic/index.test.ts - pnpm plugin-sdk:api:check - pnpm plugin-sdk:check-exports - git diff --check origin/main...HEAD - pnpm docs:list - pnpm run test:extensions:package-boundary:compile - autoreview clean: no accepted/actionable findings - PR CI rollup green: 131 success, 22 skipped, 1 neutral, 0 failures Co-authored-by: 张贵萍0668001030 --- .github/workflows/ci.yml | 2 +- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +-- docs/plugins/architecture-internals.md | 7 ++++ docs/plugins/sdk-provider-plugins.md | 7 ++++ extensions/anthropic/register.runtime.ts | 20 +++++++++++- ...e-extension-package-boundary-artifacts.mjs | 1 + ...rovider-usage.auth.normalizes-keys.test.ts | 32 +++++++++++++++++++ src/infra/provider-usage.auth.plugin.test.ts | 12 +++---- src/infra/provider-usage.auth.ts | 32 +++++++++++++------ src/plugin-sdk/core.ts | 1 + src/plugin-sdk/plugin-entry.ts | 2 ++ src/plugins/provider-runtime.ts | 10 +++++- src/plugins/types.ts | 19 +++++++---- 13 files changed, 119 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a2b6f7d36ac..715d17f8e13d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1404,7 +1404,7 @@ jobs: packages/plugin-sdk/dist extensions/*/dist/.boundary-tsc.tsbuildinfo extensions/*/dist/.boundary-tsc.stamp - key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }} + key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/plugins/types.ts', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-extension-package-boundary-v1- diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 16c065f34214..fd716f45d3fd 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -5a9934e0224ac39541bec1d69c350d490874a7c259f464f19a5f5adf46243820 plugin-sdk-api-baseline.json -60dd5b951ec3ce5520d98bdf0bfbfa27ec00f56cde8bb95edc5a6f89149c044a plugin-sdk-api-baseline.jsonl +f1da4b7930475e4be33cb05b8c239f728c7338eb1e8df9b7905bbae94d62da9e plugin-sdk-api-baseline.json +6fd007eede80893680d65c6f245eafb9e6301a1e4306530b0134fd5b3da0cddb plugin-sdk-api-baseline.jsonl diff --git a/docs/plugins/architecture-internals.md b/docs/plugins/architecture-internals.md index b7646470c6b9..259d7cc12a5d 100644 --- a/docs/plugins/architecture-internals.md +++ b/docs/plugins/architecture-internals.md @@ -312,6 +312,13 @@ If the provider needs a fully custom wire protocol or custom request executor, that is a different class of extension. These hooks are for provider behavior that still runs on OpenClaw's normal inference loop. +`resolveUsageAuth` decides whether OpenClaw should call `fetchUsageSnapshot` or +fall back to generic credential resolution for usage/status surfaces. Return +`{ token, accountId? }` when the provider has a usage credential, return +`{ handled: true }` when provider-owned usage auth has handled the request and +must suppress generic API-key/OAuth fallback, and return `null` or `undefined` +when the provider did not handle usage auth. + ### Provider example ```ts diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index cc47171a6a77..433e6dd8fdfc 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -448,6 +448,13 @@ API key auth, and dynamic model resolution. return await fetchAcmeUsage(ctx.token, ctx.timeoutMs); }, ``` + + `resolveUsageAuth` has three outcomes. Return `{ token, accountId? }` + when the provider has a usage/billing credential. Return + `{ handled: true }` only when the provider has definitively handled usage + auth but has no usable usage token, and OpenClaw must skip generic + API-key/OAuth fallback. Return `null` or `undefined` when the provider did + not handle the request and OpenClaw should continue with generic fallback. diff --git a/extensions/anthropic/register.runtime.ts b/extensions/anthropic/register.runtime.ts index 549c72c03420..18cbe2802494 100644 --- a/extensions/anthropic/register.runtime.ts +++ b/extensions/anthropic/register.runtime.ts @@ -6,6 +6,8 @@ import type { ProviderAuthMethodNonInteractiveContext, ProviderResolveDynamicModelContext, ProviderNormalizeResolvedModelContext, + ProviderResolveUsageAuthContext, + ProviderResolvedUsageAuth, ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; import { @@ -677,6 +679,22 @@ async function runAnthropicCliMigrationNonInteractive(ctx: { }; } +async function resolveAnthropicUsageAuth( + ctx: ProviderResolveUsageAuthContext, +): Promise { + const oauthToken = await ctx.resolveOAuthToken(); + if (oauthToken) { + return oauthToken; + } + + const apiKey = ctx.resolveApiKeyFromConfigAndStore(); + if (apiKey && validateAnthropicSetupToken(apiKey) === undefined) { + return { token: apiKey }; + } + + return { handled: true }; +} + export function buildAnthropicProvider(): ProviderPlugin { const providerId = "anthropic"; const defaultAnthropicModel = DEFAULT_ANTHROPIC_MODEL; @@ -802,7 +820,7 @@ export function buildAnthropicProvider(): ProviderPlugin { resolveReasoningOutputMode: () => "native", resolveThinkingProfile: ({ modelId }) => resolveClaudeThinkingProfile(modelId), wrapStreamFn: wrapAnthropicProviderStream, - resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(), + resolveUsageAuth: resolveAnthropicUsageAuth, fetchUsageSnapshot: async (ctx) => await fetchClaudeUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn), isCacheTtlEligible: () => true, diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs index 6e2ca5a03f89..7cecc9551fef 100644 --- a/scripts/prepare-extension-package-boundary-artifacts.mjs +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -37,6 +37,7 @@ function listPackageDtsOutputsFromExports({ packageDir, outputPrefix }) { const PLUGIN_SDK_TYPE_INPUTS = [ "tsconfig.json", "src/plugin-sdk", + "src/plugins/types.ts", "src/auto-reply", "packages/llm-core/src", "packages/markdown-core/src", diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 97bc0fca6457..c70cf419ce5c 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -164,6 +164,18 @@ const providerRuntimeMocks = vi.hoisted(() => ({ return token ? { token } : null; } + if (params.provider === "anthropic") { + const oauth = await params.context.resolveOAuthToken(); + if (oauth) { + return oauth; + } + const token = resolveToken({ + providerIds: ["anthropic"], + envDirect: [params.context.env?.ANTHROPIC_API_KEY], + }); + return token?.startsWith("sk-ant-oat01-") ? { token } : { handled: true }; + } + if (params.provider === "minimax") { const token = resolveToken({ providerIds: ["minimax"], @@ -726,6 +738,26 @@ describe("resolveProviderAuths key normalization", () => { }); }); + it("does not use standard Anthropic API keys for provider usage auth", async () => { + await expectResolvedAuthsFromSuiteHome({ + providers: ["anthropic"], + env: { + ANTHROPIC_API_KEY: "sk-ant-api03-status-key", // pragma: allowlist secret + }, + expected: [], + }); + }); + + it("allows Anthropic setup tokens from API-key sources for provider usage auth", async () => { + await expectResolvedAuthsFromSuiteHome({ + providers: ["anthropic"], + env: { + ANTHROPIC_API_KEY: `sk-ant-oat01-${"a".repeat(80)}`, + }, + expected: [{ provider: "anthropic", token: `sk-ant-oat01-${"a".repeat(80)}` }], + }); + }); + it("ignores marker-backed config keys for provider usage auth resolution", async () => { const auths = await resolveMinimaxAuthFromConfiguredKey(NON_ENV_SECRETREF_MARKER); expect(auths).toStrictEqual([]); diff --git a/src/infra/provider-usage.auth.plugin.test.ts b/src/infra/provider-usage.auth.plugin.test.ts index fccbadfce0e6..1c6137b2a75d 100644 --- a/src/infra/provider-usage.auth.plugin.test.ts +++ b/src/infra/provider-usage.auth.plugin.test.ts @@ -276,7 +276,8 @@ describe("resolveProviderAuths plugin boundary", () => { expect(resolveProviderUsageAuthWithPluginMock).not.toHaveBeenCalled(); }); - it("skips plugin usage auth per provider when only another provider has direct credentials", async () => { + it("does not fall back to standard Anthropic API keys for usage auth", async () => { + resolveProviderUsageAuthWithPluginMock.mockResolvedValueOnce({ handled: true }); await withTempHome(async (homeDir) => { await expect( resolveProviderAuthsForTest({ @@ -284,15 +285,10 @@ describe("resolveProviderAuths plugin boundary", () => { skipPluginAuthWithoutCredentialSource: true, env: { HOME: homeDir, - ANTHROPIC_API_KEY: "sk-ant", + ANTHROPIC_API_KEY: "sk-ant-api03-status-key", // pragma: allowlist secret }, }), - ).resolves.toEqual([ - { - provider: "anthropic", - token: "sk-ant", - }, - ]); + ).resolves.toEqual([]); }); expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledTimes(1); diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index c24e28115024..71ed662ad585 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -258,7 +258,7 @@ async function resolveOAuthToken(params: { async function resolveProviderUsageAuthViaPlugin(params: { state: UsageAuthState; provider: UsageProviderId; -}): Promise { +}): Promise<{ handled: boolean; auth: ProviderAuth | null }> { const resolved = await resolveProviderUsageAuthWithPlugin({ provider: params.provider, config: params.state.cfg, @@ -290,13 +290,19 @@ async function resolveProviderUsageAuthViaPlugin(params: { }, }, }); - if (!resolved?.token) { - return null; + if (!resolved) { + return { handled: false, auth: null }; + } + if ("handled" in resolved) { + return { handled: true, auth: null }; } return { - provider: params.provider, - token: resolved.token, - ...(resolved.accountId ? { accountId: resolved.accountId } : {}), + handled: true, + auth: { + provider: params.provider, + token: resolved.token, + ...(resolved.accountId ? { accountId: resolved.accountId } : {}), + }, }; } @@ -392,8 +398,11 @@ export async function resolveProviderAuths(params: { state: authProfileSourceState, provider, }); - if (pluginAuth) { - auths.push(pluginAuth); + if (pluginAuth.auth) { + auths.push(pluginAuth.auth); + continue; + } + if (pluginAuth.handled) { continue; } const fallbackAuth = await resolveProviderUsageAuthFallback({ @@ -442,8 +451,11 @@ export async function resolveProviderAuths(params: { state, provider, }); - if (pluginAuth) { - auths.push(pluginAuth); + if (pluginAuth.auth) { + auths.push(pluginAuth.auth); + continue; + } + if (pluginAuth.handled) { continue; } } diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 129ad06b8dec..d7c777d65a06 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -114,6 +114,7 @@ export type { ProviderResolveTransportTurnStateContext, ProviderResolveWebSocketSessionPolicyContext, ProviderResolvedUsageAuth, + ProviderUsageAuthToken, RealtimeTranscriptionProviderPlugin, ProviderSanitizeReplayHistoryContext, ProviderTransportTurnState, diff --git a/src/plugin-sdk/plugin-entry.ts b/src/plugin-sdk/plugin-entry.ts index 52e4617f01eb..84d0a80a4a6c 100644 --- a/src/plugin-sdk/plugin-entry.ts +++ b/src/plugin-sdk/plugin-entry.ts @@ -70,6 +70,7 @@ import type { ProviderReplaySessionState, RealtimeTranscriptionProviderPlugin, ProviderResolvedUsageAuth, + ProviderUsageAuthToken, ProviderResolveDynamicModelContext, ProviderResolveTransportTurnStateContext, ProviderResolveWebSocketSessionPolicyContext, @@ -207,6 +208,7 @@ export type { ProviderReasoningOutputMode, ProviderReasoningOutputModeContext, ProviderResolvedUsageAuth, + ProviderUsageAuthToken, ProviderToolSchemaDiagnostic, ProviderPrepareExtraParamsContext, ProviderPrepareDynamicModelContext, diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 322fda35a66d..eb1c990a74d5 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -614,7 +614,15 @@ export async function resolveProviderUsageAuthWithPlugin(params: { env?: NodeJS.ProcessEnv; context: ProviderResolveUsageAuthContext; }) { - return await resolveProviderRuntimePlugin(params)?.resolveUsageAuth?.(params.context); + const plugin = resolveProviderRuntimePlugin(params); + if (!plugin?.resolveUsageAuth) { + return undefined; + } + const result = await plugin.resolveUsageAuth(params.context); + if (!result) { + return undefined; + } + return result; } export async function resolveProviderUsageSnapshotWithPlugin(params: { diff --git a/src/plugins/types.ts b/src/plugins/types.ts index b7ec7b32cd6a..ce9cd9d1bb03 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -640,19 +640,24 @@ export type ProviderResolveUsageAuthContext = { providerIds?: string[]; envDirect?: Array; }) => string | undefined; - resolveOAuthToken: (params?: { provider?: string }) => Promise; + resolveOAuthToken: (params?: { provider?: string }) => Promise; }; +export type ProviderUsageAuthToken = { token: string; accountId?: string }; + /** * Result of `resolveUsageAuth`. * - * `token` is the credential used for provider usage/billing endpoints. - * `accountId` is optional provider-specific metadata used by some usage APIs. + * Two shapes are supported: + * - `{ token: string; accountId?: string }` — use this token for provider usage endpoints. + * - `{ handled: true }` — this provider handled the request but has no usable + * usage token; core must skip further fallback (generic API-key/OAuth fallback + * must not run). + * + * Returning `null` or `undefined` means "not handled by this provider"; core + * proceeds to generic fallback resolution. */ -export type ProviderResolvedUsageAuth = { - token: string; - accountId?: string; -}; +export type ProviderResolvedUsageAuth = ProviderUsageAuthToken | { handled: true }; /** * Usage/quota snapshot input for providers that own their usage endpoint