From 9307affe595f1008e9d4e903dca186a604c577c7 Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 29 Apr 2026 19:59:23 +0100 Subject: [PATCH] fix: align runtime auth evidence with config trust --- src/agents/model-auth-env.ts | 12 ++- src/agents/model-auth.profiles.test.ts | 115 ++++++++++++++++++++++--- src/agents/model-auth.ts | 18 ++-- 3 files changed, 125 insertions(+), 20 deletions(-) diff --git a/src/agents/model-auth-env.ts b/src/agents/model-auth-env.ts index 6739fbe4d75..850c3d117ff 100644 --- a/src/agents/model-auth-env.ts +++ b/src/agents/model-auth-env.ts @@ -1,5 +1,6 @@ import fs from "node:fs"; import os from "node:os"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; import { getShellEnvAppliedKeys } from "../infra/shell-env.js"; import { resolvePluginSetupProvider } from "../plugins/setup-registry.js"; import type { ProviderAuthEvidence } from "../secrets/provider-env-vars.js"; @@ -18,6 +19,8 @@ export type EnvApiKeyResult = { }; export type EnvApiKeyLookupOptions = { + config?: OpenClawConfig; + workspaceDir?: string; aliasMap?: Readonly>; candidateMap?: Readonly>; authEvidenceMap?: Readonly>; @@ -90,8 +93,13 @@ export function resolveEnvApiKey( const normalized = options.aliasMap ? (options.aliasMap[normalizedProvider] ?? normalizedProvider) : resolveProviderIdForAuth(provider, { env }); - const candidateMap = options.candidateMap ?? resolveProviderEnvApiKeyCandidates({ env }); - const authEvidenceMap = options.authEvidenceMap ?? resolveProviderEnvAuthEvidence({ env }); + const lookupParams = { + config: options.config, + workspaceDir: options.workspaceDir, + env, + }; + const candidateMap = options.candidateMap ?? resolveProviderEnvApiKeyCandidates(lookupParams); + const authEvidenceMap = options.authEvidenceMap ?? resolveProviderEnvAuthEvidence(lookupParams); const applied = new Set(getShellEnvAppliedKeys()); const pick = (envVar: string): EnvApiKeyResult | null => { const value = normalizeOptionalSecretInput(env[envVar]); diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 976287a5ec4..e598261aa30 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -15,6 +15,7 @@ import { hasAvailableAuthForProvider, resolveApiKeyForProvider, resolveEnvApiKey, + resolveModelAuthMode, } from "./model-auth.js"; async function expectVertexAdcEnvApiKey(params: { @@ -90,6 +91,17 @@ vi.mock("./provider-auth-aliases.js", () => ({ })); vi.mock("./model-auth-env-vars.js", () => { + const hasAllowedPlugin = (config: unknown, pluginId: string): boolean => { + if (!config || typeof config !== "object") { + return false; + } + const plugins = (config as { plugins?: unknown }).plugins; + if (!plugins || typeof plugins !== "object") { + return false; + } + const allow = (plugins as { allow?: unknown }).allow; + return Array.isArray(allow) && allow.includes(pluginId); + }; const candidates = { anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"], google: ["GEMINI_API_KEY", "GOOGLE_API_KEY"], @@ -110,19 +122,35 @@ vi.mock("./model-auth-env-vars.js", () => { PROVIDER_ENV_API_KEY_CANDIDATES: candidates, listKnownProviderEnvApiKeyNames: () => [...new Set(Object.values(candidates).flat())], resolveProviderEnvApiKeyCandidates: () => candidates, - resolveProviderEnvAuthEvidence: () => ({ - "google-vertex": [ - { - type: "local-file-with-env", - fileEnvVar: "GOOGLE_APPLICATION_CREDENTIALS", - fallbackPaths: ["${HOME}/.config/gcloud/application_default_credentials.json"], - requiresAnyEnv: ["GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"], - requiresAllEnv: ["GOOGLE_CLOUD_LOCATION"], - credentialMarker: "gcp-vertex-credentials", - source: "gcloud adc", - }, - ], - }), + resolveProviderEnvAuthEvidence: (params?: { config?: OpenClawConfig }) => { + const evidence = { + "google-vertex": [ + { + type: "local-file-with-env", + fileEnvVar: "GOOGLE_APPLICATION_CREDENTIALS", + fallbackPaths: ["${HOME}/.config/gcloud/application_default_credentials.json"], + requiresAnyEnv: ["GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"], + requiresAllEnv: ["GOOGLE_CLOUD_LOCATION"], + credentialMarker: "gcp-vertex-credentials", + source: "gcloud adc", + }, + ], + } satisfies Record; + if (!hasAllowedPlugin(params?.config, "workspace-cloud")) { + return evidence; + } + return { + ...evidence, + "workspace-cloud": [ + { + type: "local-file-with-env", + fileEnvVar: "WORKSPACE_CLOUD_CREDENTIALS", + credentialMarker: "workspace-cloud-local-credentials", + source: "workspace cloud credentials", + }, + ], + }; + }, }; }); @@ -436,6 +464,67 @@ describe("getApiKeyForModel", () => { }); }); + it("uses trusted workspace manifest auth evidence in runtime auth checks", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-cloud-auth-")); + const credentialsPath = path.join(tempDir, "credentials.json"); + await fs.writeFile(credentialsPath, "{}", "utf8"); + + const cfg: OpenClawConfig = { + plugins: { + allow: ["workspace-cloud"], + }, + }; + + try { + await withEnvAsync({ WORKSPACE_CLOUD_CREDENTIALS: credentialsPath }, async () => { + const store = { version: 1 as const, profiles: {} }; + const resolved = await resolveApiKeyForProvider({ + provider: "workspace-cloud", + cfg, + store, + }); + + expect(resolved).toEqual({ + apiKey: "workspace-cloud-local-credentials", + source: "workspace cloud credentials", + mode: "api-key", + }); + expect(resolveModelAuthMode("workspace-cloud", cfg, store)).toBe("api-key"); + await expect( + hasAvailableAuthForProvider({ + provider: "workspace-cloud", + cfg, + store, + }), + ).resolves.toBe(true); + }); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + + it("ignores untrusted workspace manifest auth evidence in runtime auth checks", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-cloud-auth-")); + const credentialsPath = path.join(tempDir, "credentials.json"); + await fs.writeFile(credentialsPath, "{}", "utf8"); + + try { + await withEnvAsync({ WORKSPACE_CLOUD_CREDENTIALS: credentialsPath }, async () => { + const store = { version: 1 as const, profiles: {} }; + expect(resolveModelAuthMode("workspace-cloud", { plugins: {} }, store)).toBe("unknown"); + await expect( + hasAvailableAuthForProvider({ + provider: "workspace-cloud", + cfg: { plugins: {} }, + store, + }), + ).resolves.toBe(false); + }); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); + it("hasAvailableAuthForProvider('google') accepts GOOGLE_API_KEY fallback", async () => { await withEnvAsync( { diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 6817ff91e3f..71095633264 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -48,6 +48,14 @@ export type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js"; export type ProviderCredentialPrecedence = "profile-first" | "env-first"; const log = createSubsystemLogger("model-auth"); + +function resolveConfigAwareEnvApiKey( + cfg: OpenClawConfig | undefined, + provider: string, +): EnvApiKeyResult | null { + return resolveEnvApiKey(provider, process.env, { config: cfg }); +} + function resolveProviderConfig( cfg: OpenClawConfig | undefined, provider: string, @@ -545,7 +553,7 @@ export async function resolveApiKeyForProvider(params: { } if (params.credentialPrecedence === "env-first") { - const envResolved = resolveEnvApiKey(provider); + const envResolved = resolveConfigAwareEnvApiKey(cfg, provider); if (envResolved) { const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN") ? "oauth" @@ -567,7 +575,7 @@ export async function resolveApiKeyForProvider(params: { mode: "api-key", }; } - const localMarkerEnv = resolveEnvApiKey(provider); + const localMarkerEnv = resolveConfigAwareEnvApiKey(cfg, provider); if (localMarkerEnv && isNonSecretApiKeyMarker(localMarkerEnv.apiKey)) { return { apiKey: localMarkerEnv.apiKey, @@ -618,7 +626,7 @@ export async function resolveApiKeyForProvider(params: { } } - const envResolved = resolveEnvApiKey(provider); + const envResolved = resolveConfigAwareEnvApiKey(cfg, provider); if (envResolved) { const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN") ? "oauth" @@ -731,7 +739,7 @@ export function resolveModelAuthMode( return "aws-sdk"; } - const envKey = resolveEnvApiKey(resolved); + const envKey = resolveConfigAwareEnvApiKey(cfg, resolved); if (envKey?.apiKey) { return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key"; } @@ -763,7 +771,7 @@ export async function hasAvailableAuthForProvider(params: { if (authOverride === "aws-sdk") { return true; } - if (resolveEnvApiKey(provider)) { + if (resolveConfigAwareEnvApiKey(cfg, provider)) { return true; } if (resolveUsableCustomProviderApiKey({ cfg, provider })) {