mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:01:01 +00:00
fix: scope usage auth credential gates
This commit is contained in:
@@ -6,17 +6,19 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const resolveProviderUsageAuthWithPluginMock = vi.fn(
|
||||
async (..._args: unknown[]): Promise<unknown> => null,
|
||||
);
|
||||
const hasAnyAuthProfileStoreSourceMock = vi.fn(() => false);
|
||||
const ensureAuthProfileStoreMock = vi.fn(() => ({
|
||||
profiles: {},
|
||||
}));
|
||||
const resolveAuthProfileOrderMock = vi.fn((_params: unknown): string[] => []);
|
||||
|
||||
vi.mock("../agents/auth-profiles.js", () => ({
|
||||
dedupeProfileIds: (profileIds: string[]) => [...new Set(profileIds)],
|
||||
ensureAuthProfileStore: () => ensureAuthProfileStoreMock(),
|
||||
hasAnyAuthProfileStoreSource: () => false,
|
||||
hasAnyAuthProfileStoreSource: () => hasAnyAuthProfileStoreSourceMock(),
|
||||
listProfilesForProvider: () => [],
|
||||
resolveApiKeyForProfile: async () => null,
|
||||
resolveAuthProfileOrder: () => [],
|
||||
resolveAuthProfileOrder: (params: unknown) => resolveAuthProfileOrderMock(params),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/provider-runtime.js", async () => {
|
||||
@@ -46,7 +48,14 @@ describe("resolveProviderAuths plugin boundary", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
hasAnyAuthProfileStoreSourceMock.mockReset();
|
||||
hasAnyAuthProfileStoreSourceMock.mockReturnValue(false);
|
||||
ensureAuthProfileStoreMock.mockClear();
|
||||
ensureAuthProfileStoreMock.mockReturnValue({
|
||||
profiles: {},
|
||||
});
|
||||
resolveAuthProfileOrderMock.mockReset();
|
||||
resolveAuthProfileOrderMock.mockReturnValue([]);
|
||||
resolveProviderUsageAuthWithPluginMock.mockReset();
|
||||
resolveProviderUsageAuthWithPluginMock.mockResolvedValue(null);
|
||||
});
|
||||
@@ -116,6 +125,84 @@ describe("resolveProviderAuths plugin boundary", () => {
|
||||
expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps legacy plugin credential sources provider-specific", 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: ["anthropic", "zai"],
|
||||
skipPluginAuthWithoutCredentialSource: true,
|
||||
env: { HOME: homeDir },
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
provider: "zai",
|
||||
token: "legacy-zai-token",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledTimes(1);
|
||||
expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "zai",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps auth-profile credential sources provider-specific", async () => {
|
||||
hasAnyAuthProfileStoreSourceMock.mockReturnValue(true);
|
||||
ensureAuthProfileStoreMock.mockReturnValue({
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
key: "sk-ant",
|
||||
},
|
||||
},
|
||||
});
|
||||
resolveAuthProfileOrderMock.mockImplementation((params: unknown) => {
|
||||
const provider =
|
||||
params && typeof params === "object" && "provider" in params
|
||||
? (params as { provider?: unknown }).provider
|
||||
: undefined;
|
||||
return provider === "anthropic" ? ["anthropic:default"] : [];
|
||||
});
|
||||
resolveProviderUsageAuthWithPluginMock.mockResolvedValueOnce({
|
||||
token: "plugin-anthropic-token",
|
||||
});
|
||||
|
||||
await withTempHome(async (homeDir) => {
|
||||
await expect(
|
||||
resolveProviderAuths({
|
||||
providers: ["anthropic", "zai"],
|
||||
skipPluginAuthWithoutCredentialSource: true,
|
||||
env: { HOME: homeDir },
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
provider: "anthropic",
|
||||
token: "plugin-anthropic-token",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledTimes(1);
|
||||
expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider: "anthropic",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips plugin usage auth per provider when only another provider has direct credentials", async () => {
|
||||
await withTempHome(async (homeDir) => {
|
||||
await expect(
|
||||
|
||||
@@ -13,7 +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 { resolveLegacyPiAgentAccessToken } from "./provider-usage.shared.js";
|
||||
import type { UsageProviderId } from "./provider-usage.types.js";
|
||||
|
||||
export type ProviderAuth = {
|
||||
@@ -226,6 +226,26 @@ async function resolveProviderUsageAuthFallback(params: {
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasAuthProfileCredentialSource(params: {
|
||||
state: UsageAuthState;
|
||||
provider: UsageProviderId;
|
||||
}): boolean {
|
||||
const store = resolveUsageAuthStore(params.state);
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: params.state.cfg,
|
||||
store,
|
||||
provider: params.provider,
|
||||
});
|
||||
return dedupeProfileIds(order).some((profileId) => {
|
||||
const cred = store.profiles[profileId];
|
||||
return cred?.type === "api_key" || cred?.type === "oauth" || cred?.type === "token";
|
||||
});
|
||||
}
|
||||
|
||||
function resolveLegacyPiAgentProviderIds(provider: UsageProviderId): string[] {
|
||||
return provider === "zai" ? ["z-ai", "zai"] : [provider];
|
||||
}
|
||||
|
||||
export async function resolveProviderAuths(params: {
|
||||
providers: UsageProviderId[];
|
||||
auth?: ProviderAuth[];
|
||||
@@ -244,7 +264,10 @@ export async function resolveProviderAuths(params: {
|
||||
agentDir: params.agentDir,
|
||||
};
|
||||
const hasAuthProfileStoreSource = hasAnyAuthProfileStoreSource(params.agentDir);
|
||||
const hasSharedPluginCredentialSource = hasLegacyPiAgentAuthSource(stateBase.env);
|
||||
const authProfileSourceState: UsageAuthState = {
|
||||
...stateBase,
|
||||
allowAuthProfileStore: true,
|
||||
};
|
||||
const auths: ProviderAuth[] = [];
|
||||
|
||||
for (const provider of params.providers) {
|
||||
@@ -257,13 +280,20 @@ export async function resolveProviderAuths(params: {
|
||||
const allowAuthProfileStore =
|
||||
!params.skipPluginAuthWithoutCredentialSource ||
|
||||
hasDirectCredentialSource ||
|
||||
hasAuthProfileStoreSource;
|
||||
(hasAuthProfileStoreSource &&
|
||||
hasAuthProfileCredentialSource({
|
||||
state: authProfileSourceState,
|
||||
provider,
|
||||
}));
|
||||
const state: UsageAuthState = {
|
||||
...stateBase,
|
||||
allowAuthProfileStore,
|
||||
};
|
||||
const hasLegacyPiAgentCredentialSource = Boolean(
|
||||
resolveLegacyPiAgentAccessToken(stateBase.env, resolveLegacyPiAgentProviderIds(provider)),
|
||||
);
|
||||
const hasPluginCredentialSource =
|
||||
hasDirectCredentialSource || hasAuthProfileStoreSource || hasSharedPluginCredentialSource;
|
||||
hasDirectCredentialSource || allowAuthProfileStore || hasLegacyPiAgentCredentialSource;
|
||||
|
||||
if (!params.skipPluginAuthWithoutCredentialSource || hasPluginCredentialSource) {
|
||||
const pluginAuth = await resolveProviderUsageAuthViaPlugin({
|
||||
|
||||
@@ -75,14 +75,6 @@ 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[],
|
||||
|
||||
Reference in New Issue
Block a user