fix: preserve manifest-backed model list auth

This commit is contained in:
Shakker
2026-04-29 17:54:05 +01:00
parent be1037fd34
commit 69181342e1
4 changed files with 111 additions and 9 deletions

View File

@@ -71,6 +71,15 @@
}
}
},
"setup": {
"providers": [
{
"id": "google-vertex",
"authMethods": ["api-key"],
"envVars": ["GOOGLE_CLOUD_API_KEY"]
}
]
},
"cliBackends": ["google-gemini-cli"],
"providerAuthEnvVars": {
"google": ["GEMINI_API_KEY", "GOOGLE_API_KEY"]

View File

@@ -2,10 +2,23 @@ import { describe, expect, it, vi } from "vitest";
import type { AuthProfileStore } from "../../agents/auth-profiles/types.js";
import { createModelListAuthIndex } from "./list.auth-index.js";
vi.mock("../../plugins/installed-plugin-index-store.js", () => ({
readPersistedInstalledPluginIndexSync: vi.fn(() => null),
const pluginRegistryMocks = vi.hoisted(() => ({
loadPluginRegistrySnapshotWithMetadata: vi.fn(() => ({
source: "persisted",
snapshot: { plugins: [] },
diagnostics: [],
})),
}));
vi.mock("../../plugins/plugin-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../plugins/plugin-registry.js")>();
return {
...actual,
loadPluginRegistrySnapshotWithMetadata:
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata,
};
});
const emptyStore: AuthProfileStore = {
version: 1,
profiles: {},
@@ -57,6 +70,18 @@ describe("createModelListAuthIndex", () => {
expect(index.hasProviderAuth("openai")).toBe(false);
});
it("uses manifest env metadata for google vertex auth", () => {
const index = createModelListAuthIndex({
cfg: {},
authStore: emptyStore,
env: {
GOOGLE_CLOUD_API_KEY: "gcp-test",
},
});
expect(index.hasProviderAuth("google-vertex")).toBe(true);
});
it("records configured provider API keys", () => {
const index = createModelListAuthIndex({
cfg: {
@@ -108,4 +133,38 @@ describe("createModelListAuthIndex", () => {
expect(index.hasProviderAuth("codex")).toBe(true);
});
it("ignores derived synthetic auth snapshots", () => {
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValueOnce({
source: "derived",
snapshot: {
plugins: [{ enabled: true, syntheticAuthRefs: ["codex"] }],
},
diagnostics: [],
});
const index = createModelListAuthIndex({
cfg: {},
authStore: emptyStore,
env: {},
});
expect(index.hasProviderAuth("codex")).toBe(false);
});
it("ignores disabled synthetic auth snapshot entries", () => {
pluginRegistryMocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValueOnce({
source: "persisted",
snapshot: {
plugins: [{ enabled: false, syntheticAuthRefs: ["codex"] }],
},
diagnostics: [],
});
const index = createModelListAuthIndex({
cfg: {},
authStore: emptyStore,
env: {},
});
expect(index.hasProviderAuth("codex")).toBe(false);
});
});

View File

@@ -9,7 +9,7 @@ import {
import { resolveProviderAuthAliasMap } from "../../agents/provider-auth-aliases.js";
import { normalizeProviderIdForAuth } from "../../agents/provider-id.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { readPersistedInstalledPluginIndexSync } from "../../plugins/installed-plugin-index-store.js";
import { loadPluginRegistrySnapshotWithMetadata } from "../../plugins/plugin-registry.js";
export type ModelListAuthIndex = {
hasProviderAuth(provider: string): boolean;
@@ -34,9 +34,20 @@ function normalizeAuthProvider(
return aliasMap[normalized] ?? normalized;
}
function listPersistedSyntheticAuthProviderRefs(): readonly string[] {
const index = readPersistedInstalledPluginIndexSync();
return index?.plugins.flatMap((plugin) => plugin.syntheticAuthRefs ?? []) ?? [];
function listValidatedSyntheticAuthProviderRefs(params: {
cfg: OpenClawConfig;
env: NodeJS.ProcessEnv;
}): readonly string[] {
const result = loadPluginRegistrySnapshotWithMetadata({
config: params.cfg,
env: params.env,
});
if (result.source !== "persisted" && result.source !== "provided") {
return [];
}
return result.snapshot.plugins
.filter((plugin) => plugin.enabled)
.flatMap((plugin) => plugin.syntheticAuthRefs ?? []);
}
export function createModelListAuthIndex(
@@ -62,6 +73,11 @@ export function createModelListAuthIndex(
addProvider(provider);
}
}
// Google Vertex ADC is still represented by resolveEnvApiKey's compatibility
// path. Move this into manifest auth signals once that contract exists.
if (resolveEnvApiKey("google-vertex", env, { aliasMap, candidateMap: envCandidateMap })) {
addProvider("google-vertex");
}
if (resolveAwsSdkEnvVarName(env)) {
addProvider("amazon-bedrock");
@@ -77,7 +93,7 @@ export function createModelListAuthIndex(
}
for (const provider of params.syntheticAuthProviderRefs ??
listPersistedSyntheticAuthProviderRefs()) {
listValidatedSyntheticAuthProviderRefs({ cfg: params.cfg, env })) {
addProvider(provider);
}

View File

@@ -57,6 +57,7 @@ const mocks = vi.hoisted(() => {
printModelTable: vi.fn(),
resolveModelWithRegistry: vi.fn(),
readPersistedInstalledPluginIndexSync: vi.fn(),
loadPluginRegistrySnapshotWithMetadata: vi.fn(),
};
});
@@ -95,6 +96,11 @@ function resetMocks() {
mocks.printModelTable.mockReset();
mocks.resolveModelWithRegistry.mockReturnValue({ ...OPENAI_CODEX_MODEL });
mocks.readPersistedInstalledPluginIndexSync.mockReturnValue(null);
mocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
source: "persisted",
snapshot: { plugins: [] },
diagnostics: [],
});
}
function createRuntime() {
@@ -215,6 +221,14 @@ function installModelsListCommandForwardCompatMocks() {
vi.doMock("../../plugins/installed-plugin-index-store.js", () => ({
readPersistedInstalledPluginIndexSync: mocks.readPersistedInstalledPluginIndexSync,
}));
vi.doMock("../../plugins/plugin-registry.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../plugins/plugin-registry.js")>();
return {
...actual,
loadPluginRegistrySnapshotWithMetadata: mocks.loadPluginRegistrySnapshotWithMetadata,
};
});
}
beforeAll(async () => {
@@ -507,8 +521,12 @@ describe("modelsListCommand forward-compat", () => {
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
},
]);
mocks.readPersistedInstalledPluginIndexSync.mockReturnValue({
plugins: [{ syntheticAuthRefs: ["codex"] }],
mocks.loadPluginRegistrySnapshotWithMetadata.mockReturnValueOnce({
source: "persisted",
snapshot: {
plugins: [{ enabled: true, syntheticAuthRefs: ["codex"] }],
},
diagnostics: [],
});
const runtime = createRuntime();