diff --git a/extensions/google/openclaw.plugin.json b/extensions/google/openclaw.plugin.json index 3065bb0a47b..03874e0208d 100644 --- a/extensions/google/openclaw.plugin.json +++ b/extensions/google/openclaw.plugin.json @@ -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"] diff --git a/src/commands/models/list.auth-index.test.ts b/src/commands/models/list.auth-index.test.ts index 364e43667ec..74443b312bf 100644 --- a/src/commands/models/list.auth-index.test.ts +++ b/src/commands/models/list.auth-index.test.ts @@ -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(); + 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); + }); }); diff --git a/src/commands/models/list.auth-index.ts b/src/commands/models/list.auth-index.ts index fab861a31a8..39cf25de8e1 100644 --- a/src/commands/models/list.auth-index.ts +++ b/src/commands/models/list.auth-index.ts @@ -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); } diff --git a/src/commands/models/list.list-command.forward-compat.test.ts b/src/commands/models/list.list-command.forward-compat.test.ts index 27f320cbfeb..5ac253c78db 100644 --- a/src/commands/models/list.list-command.forward-compat.test.ts +++ b/src/commands/models/list.list-command.forward-compat.test.ts @@ -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(); + 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();