fix: align runtime auth evidence with config trust

This commit is contained in:
Shakker
2026-04-29 19:59:23 +01:00
parent 10b9adb010
commit 9307affe59
3 changed files with 125 additions and 20 deletions

View File

@@ -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<Record<string, string>>;
candidateMap?: Readonly<Record<string, readonly string[]>>;
authEvidenceMap?: Readonly<Record<string, readonly ProviderAuthEvidence[]>>;
@@ -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]);

View File

@@ -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<string, readonly unknown[]>;
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(
{

View File

@@ -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 })) {