fix: keep legacy usage auth plugin fallback

This commit is contained in:
Gustavo Madeira Santana
2026-04-20 21:20:05 -04:00
parent 1e4006b95e
commit 5719c65387
3 changed files with 87 additions and 27 deletions

View File

@@ -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<T>(fn: (homeDir: string) => Promise<T>): Promise<T> {
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(

View File

@@ -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,

View File

@@ -71,17 +71,24 @@ export const withTimeout = async <T>(work: Promise<T>, 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;
}