From 3713b0e50690748cf4816c040f2d7a668696aa3c Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Fri, 3 Apr 2026 21:13:34 -0400 Subject: [PATCH] vertex: read ADC files without exists preflight (#60592) Merged via squash. Prepared head SHA: 72f7372e970ba4f7bece031ddab8118e39aa8bea Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + .../anthropic-vertex/region.adc.test.ts | 49 +++++++++++++++++++ extensions/anthropic-vertex/region.ts | 36 +++++++------- ...pic-vertex-auth-presence.preflight.test.ts | 48 ++++++++++++++++++ .../anthropic-vertex-auth-presence.ts | 44 ++++++++++------- 5 files changed, 143 insertions(+), 35 deletions(-) create mode 100644 extensions/anthropic-vertex/region.adc.test.ts create mode 100644 src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a25fd8e3bfe..110e55f47d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ Docs: https://docs.openclaw.ai - Agents/MCP: sort MCP tools deterministically by name so the tools block in API requests is stable across turns, preventing unnecessary prompt-cache busting from non-deterministic `listTools()` order. (#58037) Thanks @bcherny. - Infra/json-file: preserve symlink-backed JSON stores and Windows overwrite fallback when atomically saving small sync JSON state files. (#60589) Thanks @gumadeiras. - Matrix/credentials: read the current and legacy credential files directly during migration fallback so concurrent legacy rename races still resolve to the stored credentials. (#60591) Thanks @gumadeiras. +- Providers/Anthropic Vertex: read ADC files directly during auth discovery so explicit Google credentials and default ADC no longer depend on `existsSync` preflight checks. (#60592) Thanks @gumadeiras. ## 2026.4.2 diff --git a/extensions/anthropic-vertex/region.adc.test.ts b/extensions/anthropic-vertex/region.adc.test.ts new file mode 100644 index 00000000000..605820c9fce --- /dev/null +++ b/extensions/anthropic-vertex/region.adc.test.ts @@ -0,0 +1,49 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({ + existsSyncMock: vi.fn(), + readFileSyncMock: vi.fn(), +})); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname)); + readFileSyncMock.mockImplementation((pathname, options) => + String(pathname) === "/tmp/vertex-adc.json" + ? '{"project_id":"vertex-project"}' + : actual.readFileSync(pathname, options as never), + ); + return { + ...actual, + existsSync: existsSyncMock, + readFileSync: readFileSyncMock, + default: { + ...actual, + existsSync: existsSyncMock, + readFileSync: readFileSyncMock, + }, + }; +}); + +import { hasAnthropicVertexAvailableAuth, resolveAnthropicVertexProjectId } from "./region.js"; + +describe("anthropic-vertex ADC reads", () => { + afterEach(() => { + existsSyncMock.mockClear(); + readFileSyncMock.mockClear(); + }); + + it("reads explicit ADC credentials without an existsSync preflight", () => { + const env = { + GOOGLE_APPLICATION_CREDENTIALS: "/tmp/vertex-adc.json", + } as NodeJS.ProcessEnv; + + existsSyncMock.mockClear(); + readFileSyncMock.mockClear(); + + expect(resolveAnthropicVertexProjectId(env)).toBe("vertex-project"); + expect(hasAnthropicVertexAvailableAuth(env)).toBe(true); + expect(existsSyncMock).not.toHaveBeenCalled(); + expect(readFileSyncMock).toHaveBeenCalledWith("/tmp/vertex-adc.json", "utf8"); + }); +}); diff --git a/extensions/anthropic-vertex/region.ts b/extensions/anthropic-vertex/region.ts index bd8d7f2d0d8..298cb25c15a 100644 --- a/extensions/anthropic-vertex/region.ts +++ b/extensions/anthropic-vertex/region.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { homedir, platform } from "node:os"; import { join } from "node:path"; import { resolveProviderEndpoint } from "openclaw/plugin-sdk/provider-http"; @@ -77,26 +77,29 @@ function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.e : GCLOUD_DEFAULT_ADC_PATH; } -function resolveAnthropicVertexAdcCredentialsPath( +function resolveAnthropicVertexAdcCredentialsPathCandidate( env: NodeJS.ProcessEnv = process.env, -): string | undefined { - const explicitCredentialsPath = normalizeOptionalSecretInput(env.GOOGLE_APPLICATION_CREDENTIALS); - if (explicitCredentialsPath) { - return existsSync(explicitCredentialsPath) ? explicitCredentialsPath : undefined; - } +): string { + return ( + normalizeOptionalSecretInput(env.GOOGLE_APPLICATION_CREDENTIALS) ?? + resolveAnthropicVertexDefaultAdcPath(env) + ); +} - const defaultAdcPath = resolveAnthropicVertexDefaultAdcPath(env); - return existsSync(defaultAdcPath) ? defaultAdcPath : undefined; +function canReadAnthropicVertexAdc(env: NodeJS.ProcessEnv = process.env): boolean { + const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env); + try { + readFileSync(credentialsPath, "utf8"); + return true; + } catch { + return false; + } } function resolveAnthropicVertexProjectIdFromAdc( env: NodeJS.ProcessEnv = process.env, ): string | undefined { - const credentialsPath = resolveAnthropicVertexAdcCredentialsPath(env); - if (!credentialsPath) { - return undefined; - } - + const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env); try { const parsed = JSON.parse(readFileSync(credentialsPath, "utf8")) as AdcProjectFile; return ( @@ -109,10 +112,7 @@ function resolveAnthropicVertexProjectIdFromAdc( } export function hasAnthropicVertexCredentials(env: NodeJS.ProcessEnv = process.env): boolean { - return ( - hasAnthropicVertexMetadataServerAdc(env) || - resolveAnthropicVertexAdcCredentialsPath(env) !== undefined - ); + return hasAnthropicVertexMetadataServerAdc(env) || canReadAnthropicVertexAdc(env); } export function hasAnthropicVertexAvailableAuth(env: NodeJS.ProcessEnv = process.env): boolean { diff --git a/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts b/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts new file mode 100644 index 00000000000..cc79c2b712e --- /dev/null +++ b/src/plugin-sdk/anthropic-vertex-auth-presence.preflight.test.ts @@ -0,0 +1,48 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { existsSyncMock, readFileSyncMock } = vi.hoisted(() => ({ + existsSyncMock: vi.fn(), + readFileSyncMock: vi.fn(), +})); + +vi.mock("node:fs", async (importOriginal) => { + const actual = await importOriginal(); + existsSyncMock.mockImplementation((pathname) => actual.existsSync(pathname)); + readFileSyncMock.mockImplementation((pathname, options) => + String(pathname) === "/tmp/vertex-adc.json" + ? '{"client_id":"vertex-client"}' + : actual.readFileSync(pathname, options as never), + ); + return { + ...actual, + existsSync: existsSyncMock, + readFileSync: readFileSyncMock, + default: { + ...actual, + existsSync: existsSyncMock, + readFileSync: readFileSyncMock, + }, + }; +}); + +import { hasAnthropicVertexAvailableAuth } from "./anthropic-vertex-auth-presence.js"; + +describe("hasAnthropicVertexAvailableAuth ADC preflight", () => { + afterEach(() => { + existsSyncMock.mockClear(); + readFileSyncMock.mockClear(); + }); + + it("reads explicit ADC credentials without an existsSync preflight", () => { + existsSyncMock.mockClear(); + readFileSyncMock.mockClear(); + + expect( + hasAnthropicVertexAvailableAuth({ + GOOGLE_APPLICATION_CREDENTIALS: "/tmp/vertex-adc.json", + } as NodeJS.ProcessEnv), + ).toBe(true); + expect(existsSyncMock).not.toHaveBeenCalled(); + expect(readFileSyncMock).toHaveBeenCalledWith("/tmp/vertex-adc.json", "utf8"); + }); +}); diff --git a/src/plugin-sdk/anthropic-vertex-auth-presence.ts b/src/plugin-sdk/anthropic-vertex-auth-presence.ts index 436950da262..1e391331c0d 100644 --- a/src/plugin-sdk/anthropic-vertex-auth-presence.ts +++ b/src/plugin-sdk/anthropic-vertex-auth-presence.ts @@ -1,4 +1,4 @@ -import { existsSync } from "node:fs"; +import { readFileSync } from "node:fs"; import { homedir, platform } from "node:os"; import { join } from "node:path"; import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js"; @@ -15,6 +15,14 @@ function hasAnthropicVertexMetadataServerAdc(env: NodeJS.ProcessEnv = process.en return explicitMetadataOptIn === "1" || explicitMetadataOptIn?.toLowerCase() === "true"; } +function normalizeOptionalPathInput(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const trimmed = value.trim(); + return trimmed || undefined; +} + function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.env): string { return platform() === "win32" ? join( @@ -25,23 +33,25 @@ function resolveAnthropicVertexDefaultAdcPath(env: NodeJS.ProcessEnv = process.e : GCLOUD_DEFAULT_ADC_PATH; } -function resolveAnthropicVertexAdcCredentialsPath( +function resolveAnthropicVertexAdcCredentialsPathCandidate( env: NodeJS.ProcessEnv = process.env, -): string | undefined { - const explicitCredentialsPath = env.GOOGLE_APPLICATION_CREDENTIALS?.trim(); - if (explicitCredentialsPath) { - return existsSync(explicitCredentialsPath) ? explicitCredentialsPath : undefined; - } - - const defaultAdcPath = resolveAnthropicVertexDefaultAdcPath(env); - return existsSync(defaultAdcPath) ? defaultAdcPath : undefined; -} - -export function hasAnthropicVertexAvailableAuth( - env: NodeJS.ProcessEnv = process.env, -): boolean { +): string { return ( - hasAnthropicVertexMetadataServerAdc(env) || - resolveAnthropicVertexAdcCredentialsPath(env) !== undefined + normalizeOptionalPathInput(env.GOOGLE_APPLICATION_CREDENTIALS) ?? + resolveAnthropicVertexDefaultAdcPath(env) ); } + +function canReadAnthropicVertexAdc(env: NodeJS.ProcessEnv = process.env): boolean { + const credentialsPath = resolveAnthropicVertexAdcCredentialsPathCandidate(env); + try { + readFileSync(credentialsPath, "utf8"); + return true; + } catch { + return false; + } +} + +export function hasAnthropicVertexAvailableAuth(env: NodeJS.ProcessEnv = process.env): boolean { + return hasAnthropicVertexMetadataServerAdc(env) || canReadAnthropicVertexAdc(env); +}