mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix: align runtime auth evidence with config trust
This commit is contained in:
@@ -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]);
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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 })) {
|
||||
|
||||
Reference in New Issue
Block a user