fix: honor provider alias usage auth

This commit is contained in:
Gustavo Madeira Santana
2026-04-20 23:06:24 -04:00
parent 6e8f0f8d2a
commit 0d9d60eae8
2 changed files with 145 additions and 12 deletions

View File

@@ -213,6 +213,56 @@ describe("resolveProviderAuths plugin boundary", () => {
expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled();
});
it("keeps plugin usage auth when an owned alias provider has auth-profile credentials", async () => {
hasAnyAuthProfileStoreSourceMock.mockReturnValue(true);
ensureAuthProfileStoreWithoutExternalProfilesMock.mockReturnValue({
profiles: {
"minimax-portal:default": {
type: "oauth",
provider: "minimax-portal",
accessToken: "portal-oauth-token",
},
},
});
resolveAuthProfileOrderMock.mockImplementation((params: unknown) => {
const provider =
params && typeof params === "object" && "provider" in params
? (params as { provider?: unknown }).provider
: undefined;
return provider === "minimax-portal" ? ["minimax-portal:default"] : [];
});
resolveProviderUsageAuthWithPluginMock.mockResolvedValueOnce({
token: "plugin-minimax-token",
});
await withTempHome(async (homeDir) => {
await expect(
resolveProviderAuths({
providers: ["minimax"],
skipPluginAuthWithoutCredentialSource: true,
env: { HOME: homeDir },
}),
).resolves.toEqual([
{
provider: "minimax",
token: "plugin-minimax-token",
},
]);
});
expect(resolveAuthProfileOrderMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "minimax-portal",
}),
);
expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "minimax",
}),
);
expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled();
});
it("does not overlay external auth profiles while checking the skip gate", async () => {
hasAnyAuthProfileStoreSourceMock.mockReturnValue(true);

View File

@@ -12,6 +12,15 @@ import { isNonSecretApiKeyMarker } from "../agents/model-auth-markers.js";
import { resolveUsableCustomProviderApiKey } from "../agents/model-auth.js";
import { normalizeProviderId } from "../agents/model-selection.js";
import { loadConfig, type OpenClawConfig } from "../config/config.js";
import { normalizePluginsConfig } from "../plugins/config-state.js";
import {
isActivatedManifestOwner,
passesManifestOwnerBasePolicy,
} from "../plugins/manifest-owner-policy.js";
import {
loadPluginManifestRegistry,
type PluginManifestRecord,
} from "../plugins/manifest-registry.js";
import { resolveProviderUsageAuthWithPlugin } from "../plugins/provider-runtime.js";
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
import { resolveLegacyPiAgentAccessToken } from "./provider-usage.shared.js";
@@ -110,6 +119,69 @@ function resolveProviderApiKeyFromConfigAndStore(params: {
return undefined;
}
function normalizeProviderIds(providerIds: Iterable<string | undefined>): string[] {
return [
...new Set(
[...providerIds]
.map((providerId) => (providerId ? normalizeProviderId(providerId) : undefined))
.filter((providerId): providerId is string => Boolean(providerId)),
),
];
}
function isUsageProviderManifestEligible(params: {
plugin: PluginManifestRecord;
state: UsageAuthState;
}): boolean {
const normalizedConfig = normalizePluginsConfig(params.state.cfg.plugins);
if (
!passesManifestOwnerBasePolicy({
plugin: params.plugin,
normalizedConfig,
})
) {
return false;
}
if (params.plugin.origin !== "workspace") {
return true;
}
return isActivatedManifestOwner({
plugin: params.plugin,
normalizedConfig,
rootConfig: params.state.cfg,
});
}
function resolveUsageCredentialProviderIds(params: {
state: UsageAuthState;
provider: UsageProviderId;
}): string[] {
const providerIds = new Set(normalizeProviderIds([params.provider]));
const providerIdSet = new Set(providerIds);
try {
const registry = loadPluginManifestRegistry({
config: params.state.cfg,
env: params.state.env,
});
for (const plugin of registry.plugins) {
const pluginProviderIds = normalizeProviderIds(plugin.providers);
if (!pluginProviderIds.some((providerId) => providerIdSet.has(providerId))) {
continue;
}
if (!isUsageProviderManifestEligible({ plugin, state: params.state })) {
continue;
}
for (const providerId of pluginProviderIds) {
providerIds.add(providerId);
}
}
} catch {
// Credential-source checks are an optimization gate; preserve usage fallback
// behavior if manifest discovery is unavailable in a constrained environment.
}
return [...providerIds];
}
async function resolveOAuthToken(params: {
state: UsageAuthState;
provider: string;
@@ -229,20 +301,27 @@ async function resolveProviderUsageAuthFallback(params: {
function hasAuthProfileCredentialSource(params: {
state: UsageAuthState;
provider: UsageProviderId;
providerIds: string[];
}): boolean {
const store = ensureAuthProfileStoreWithoutExternalProfiles(params.state.agentDir, {
allowKeychainPrompt: false,
});
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";
});
for (const provider of params.providerIds) {
const order = resolveAuthProfileOrder({
cfg: params.state.cfg,
store,
provider,
});
if (
dedupeProfileIds(order).some((profileId) => {
const cred = store.profiles[profileId];
return cred?.type === "api_key" || cred?.type === "oauth" || cred?.type === "token";
})
) {
return true;
}
}
return false;
}
function resolveLegacyPiAgentProviderIds(provider: UsageProviderId): string[] {
@@ -274,10 +353,14 @@ export async function resolveProviderAuths(params: {
const auths: ProviderAuth[] = [];
for (const provider of params.providers) {
const credentialProviderIds = resolveUsageCredentialProviderIds({
state: { ...stateBase, allowAuthProfileStore: false },
provider,
});
const hasDirectCredentialSource = Boolean(
resolveProviderApiKeyFromConfig({
state: { ...stateBase, allowAuthProfileStore: false },
providerIds: [provider],
providerIds: credentialProviderIds,
}),
);
const allowAuthProfileStore =
@@ -286,7 +369,7 @@ export async function resolveProviderAuths(params: {
(hasAuthProfileStoreSource &&
hasAuthProfileCredentialSource({
state: authProfileSourceState,
provider,
providerIds: credentialProviderIds,
}));
const state: UsageAuthState = {
...stateBase,