From 5719c653877e5fe6e28ab38e11ddab3c151bcacb Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 20 Apr 2026 21:20:05 -0400 Subject: [PATCH] fix: keep legacy usage auth plugin fallback --- src/infra/provider-usage.auth.plugin.test.ts | 89 +++++++++++++++----- src/infra/provider-usage.auth.ts | 6 +- src/infra/provider-usage.shared.ts | 19 +++-- 3 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/infra/provider-usage.auth.plugin.test.ts b/src/infra/provider-usage.auth.plugin.test.ts index bda61836e72..49d791fbcf5 100644 --- a/src/infra/provider-usage.auth.plugin.test.ts +++ b/src/infra/provider-usage.auth.plugin.test.ts @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveProviderUsageAuthWithPluginMock = vi.fn( @@ -28,6 +31,15 @@ vi.mock("../plugins/provider-runtime.js", async () => { let resolveProviderAuths: typeof import("./provider-usage.auth.js").resolveProviderAuths; +async function withTempHome(fn: (homeDir: string) => Promise): Promise { + const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-provider-usage-")); + try { + return await fn(homeDir); + } finally { + fs.rmSync(homeDir, { recursive: true, force: true }); + } +} + describe("resolveProviderAuths plugin boundary", () => { beforeAll(async () => { ({ resolveProviderAuths } = await import("./provider-usage.auth.js")); @@ -58,33 +70,70 @@ describe("resolveProviderAuths plugin boundary", () => { }); it("skips plugin usage auth when requested and no direct credential source exists", async () => { - await expect( - resolveProviderAuths({ - providers: ["zai"], - skipPluginAuthWithoutCredentialSource: true, - env: {}, - }), - ).resolves.toEqual([]); + await withTempHome(async (homeDir) => { + await expect( + resolveProviderAuths({ + providers: ["zai"], + skipPluginAuthWithoutCredentialSource: true, + env: { HOME: homeDir }, + }), + ).resolves.toEqual([]); + }); expect(resolveProviderUsageAuthWithPluginMock).not.toHaveBeenCalled(); expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled(); }); - it("skips plugin usage auth per provider when only another provider has direct credentials", async () => { - await expect( - resolveProviderAuths({ - providers: ["anthropic", "zai"], - skipPluginAuthWithoutCredentialSource: true, - env: { - ANTHROPIC_API_KEY: "sk-ant", + it("keeps plugin usage auth when a shared legacy plugin credential source exists", async () => { + await withTempHome(async (homeDir) => { + fs.mkdirSync(path.join(homeDir, ".pi", "agent"), { recursive: true }); + fs.writeFileSync( + path.join(homeDir, ".pi", "agent", "auth.json"), + `${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } })}\n`, + ); + resolveProviderUsageAuthWithPluginMock.mockResolvedValueOnce({ + token: "legacy-zai-token", + }); + await expect( + resolveProviderAuths({ + providers: ["zai"], + skipPluginAuthWithoutCredentialSource: true, + env: { HOME: homeDir }, + }), + ).resolves.toEqual([ + { + provider: "zai", + token: "legacy-zai-token", }, + ]); + }); + + expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "zai", }), - ).resolves.toEqual([ - { - provider: "anthropic", - token: "sk-ant", - }, - ]); + ); + expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled(); + }); + + it("skips plugin usage auth per provider when only another provider has direct credentials", async () => { + await withTempHome(async (homeDir) => { + await expect( + resolveProviderAuths({ + providers: ["anthropic", "zai"], + skipPluginAuthWithoutCredentialSource: true, + env: { + HOME: homeDir, + ANTHROPIC_API_KEY: "sk-ant", + }, + }), + ).resolves.toEqual([ + { + provider: "anthropic", + token: "sk-ant", + }, + ]); + }); expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledTimes(1); expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledWith( diff --git a/src/infra/provider-usage.auth.ts b/src/infra/provider-usage.auth.ts index 6df11a2e09b..65beed09964 100644 --- a/src/infra/provider-usage.auth.ts +++ b/src/infra/provider-usage.auth.ts @@ -13,6 +13,7 @@ import { normalizeProviderId } from "../agents/model-selection.js"; import { loadConfig, type OpenClawConfig } from "../config/config.js"; import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js"; import { normalizeSecretInput } from "../utils/normalize-secret-input.js"; +import { hasLegacyPiAgentAuthSource } from "./provider-usage.shared.js"; import type { UsageProviderId } from "./provider-usage.types.js"; export type ProviderAuth = { @@ -243,6 +244,7 @@ export async function resolveProviderAuths(params: { agentDir: params.agentDir, }; const hasAuthProfileStoreSource = hasAnyAuthProfileStoreSource(params.agentDir); + const hasSharedPluginCredentialSource = hasLegacyPiAgentAuthSource(stateBase.env); const auths: ProviderAuth[] = []; for (const provider of params.providers) { @@ -260,8 +262,10 @@ export async function resolveProviderAuths(params: { ...stateBase, allowAuthProfileStore, }; + const hasPluginCredentialSource = + hasDirectCredentialSource || hasAuthProfileStoreSource || hasSharedPluginCredentialSource; - if (!params.skipPluginAuthWithoutCredentialSource || allowAuthProfileStore) { + if (!params.skipPluginAuthWithoutCredentialSource || hasPluginCredentialSource) { const pluginAuth = await resolveProviderUsageAuthViaPlugin({ state, provider, diff --git a/src/infra/provider-usage.shared.ts b/src/infra/provider-usage.shared.ts index 5f38ad9f30d..2a0fdda225b 100644 --- a/src/infra/provider-usage.shared.ts +++ b/src/infra/provider-usage.shared.ts @@ -71,17 +71,24 @@ export const withTimeout = async (work: Promise, ms: number, fallback: T): } }; +function resolveLegacyPiAgentAuthPath(env: NodeJS.ProcessEnv): string { + return path.join(resolveRequiredHomeDir(env, os.homedir), ".pi", "agent", "auth.json"); +} + +export function hasLegacyPiAgentAuthSource(env: NodeJS.ProcessEnv): boolean { + try { + return fs.existsSync(resolveLegacyPiAgentAuthPath(env)); + } catch { + return false; + } +} + export function resolveLegacyPiAgentAccessToken( env: NodeJS.ProcessEnv, providerIds: string[], ): string | undefined { try { - const authPath = path.join( - resolveRequiredHomeDir(env, os.homedir), - ".pi", - "agent", - "auth.json", - ); + const authPath = resolveLegacyPiAgentAuthPath(env); if (!fs.existsSync(authPath)) { return undefined; }