mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:10:58 +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 fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
|
import { getShellEnvAppliedKeys } from "../infra/shell-env.js";
|
||||||
import { resolvePluginSetupProvider } from "../plugins/setup-registry.js";
|
import { resolvePluginSetupProvider } from "../plugins/setup-registry.js";
|
||||||
import type { ProviderAuthEvidence } from "../secrets/provider-env-vars.js";
|
import type { ProviderAuthEvidence } from "../secrets/provider-env-vars.js";
|
||||||
@@ -18,6 +19,8 @@ export type EnvApiKeyResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type EnvApiKeyLookupOptions = {
|
export type EnvApiKeyLookupOptions = {
|
||||||
|
config?: OpenClawConfig;
|
||||||
|
workspaceDir?: string;
|
||||||
aliasMap?: Readonly<Record<string, string>>;
|
aliasMap?: Readonly<Record<string, string>>;
|
||||||
candidateMap?: Readonly<Record<string, readonly string[]>>;
|
candidateMap?: Readonly<Record<string, readonly string[]>>;
|
||||||
authEvidenceMap?: Readonly<Record<string, readonly ProviderAuthEvidence[]>>;
|
authEvidenceMap?: Readonly<Record<string, readonly ProviderAuthEvidence[]>>;
|
||||||
@@ -90,8 +93,13 @@ export function resolveEnvApiKey(
|
|||||||
const normalized = options.aliasMap
|
const normalized = options.aliasMap
|
||||||
? (options.aliasMap[normalizedProvider] ?? normalizedProvider)
|
? (options.aliasMap[normalizedProvider] ?? normalizedProvider)
|
||||||
: resolveProviderIdForAuth(provider, { env });
|
: resolveProviderIdForAuth(provider, { env });
|
||||||
const candidateMap = options.candidateMap ?? resolveProviderEnvApiKeyCandidates({ env });
|
const lookupParams = {
|
||||||
const authEvidenceMap = options.authEvidenceMap ?? resolveProviderEnvAuthEvidence({ env });
|
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 applied = new Set(getShellEnvAppliedKeys());
|
||||||
const pick = (envVar: string): EnvApiKeyResult | null => {
|
const pick = (envVar: string): EnvApiKeyResult | null => {
|
||||||
const value = normalizeOptionalSecretInput(env[envVar]);
|
const value = normalizeOptionalSecretInput(env[envVar]);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
hasAvailableAuthForProvider,
|
hasAvailableAuthForProvider,
|
||||||
resolveApiKeyForProvider,
|
resolveApiKeyForProvider,
|
||||||
resolveEnvApiKey,
|
resolveEnvApiKey,
|
||||||
|
resolveModelAuthMode,
|
||||||
} from "./model-auth.js";
|
} from "./model-auth.js";
|
||||||
|
|
||||||
async function expectVertexAdcEnvApiKey(params: {
|
async function expectVertexAdcEnvApiKey(params: {
|
||||||
@@ -90,6 +91,17 @@ vi.mock("./provider-auth-aliases.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./model-auth-env-vars.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 = {
|
const candidates = {
|
||||||
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
anthropic: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||||
google: ["GEMINI_API_KEY", "GOOGLE_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,
|
PROVIDER_ENV_API_KEY_CANDIDATES: candidates,
|
||||||
listKnownProviderEnvApiKeyNames: () => [...new Set(Object.values(candidates).flat())],
|
listKnownProviderEnvApiKeyNames: () => [...new Set(Object.values(candidates).flat())],
|
||||||
resolveProviderEnvApiKeyCandidates: () => candidates,
|
resolveProviderEnvApiKeyCandidates: () => candidates,
|
||||||
resolveProviderEnvAuthEvidence: () => ({
|
resolveProviderEnvAuthEvidence: (params?: { config?: OpenClawConfig }) => {
|
||||||
"google-vertex": [
|
const evidence = {
|
||||||
{
|
"google-vertex": [
|
||||||
type: "local-file-with-env",
|
{
|
||||||
fileEnvVar: "GOOGLE_APPLICATION_CREDENTIALS",
|
type: "local-file-with-env",
|
||||||
fallbackPaths: ["${HOME}/.config/gcloud/application_default_credentials.json"],
|
fileEnvVar: "GOOGLE_APPLICATION_CREDENTIALS",
|
||||||
requiresAnyEnv: ["GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"],
|
fallbackPaths: ["${HOME}/.config/gcloud/application_default_credentials.json"],
|
||||||
requiresAllEnv: ["GOOGLE_CLOUD_LOCATION"],
|
requiresAnyEnv: ["GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT"],
|
||||||
credentialMarker: "gcp-vertex-credentials",
|
requiresAllEnv: ["GOOGLE_CLOUD_LOCATION"],
|
||||||
source: "gcloud adc",
|
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 () => {
|
it("hasAvailableAuthForProvider('google') accepts GOOGLE_API_KEY fallback", async () => {
|
||||||
await withEnvAsync(
|
await withEnvAsync(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ export type { ResolvedProviderAuth } from "./model-auth-runtime-shared.js";
|
|||||||
export type ProviderCredentialPrecedence = "profile-first" | "env-first";
|
export type ProviderCredentialPrecedence = "profile-first" | "env-first";
|
||||||
|
|
||||||
const log = createSubsystemLogger("model-auth");
|
const log = createSubsystemLogger("model-auth");
|
||||||
|
|
||||||
|
function resolveConfigAwareEnvApiKey(
|
||||||
|
cfg: OpenClawConfig | undefined,
|
||||||
|
provider: string,
|
||||||
|
): EnvApiKeyResult | null {
|
||||||
|
return resolveEnvApiKey(provider, process.env, { config: cfg });
|
||||||
|
}
|
||||||
|
|
||||||
function resolveProviderConfig(
|
function resolveProviderConfig(
|
||||||
cfg: OpenClawConfig | undefined,
|
cfg: OpenClawConfig | undefined,
|
||||||
provider: string,
|
provider: string,
|
||||||
@@ -545,7 +553,7 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (params.credentialPrecedence === "env-first") {
|
if (params.credentialPrecedence === "env-first") {
|
||||||
const envResolved = resolveEnvApiKey(provider);
|
const envResolved = resolveConfigAwareEnvApiKey(cfg, provider);
|
||||||
if (envResolved) {
|
if (envResolved) {
|
||||||
const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN")
|
const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN")
|
||||||
? "oauth"
|
? "oauth"
|
||||||
@@ -567,7 +575,7 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
mode: "api-key",
|
mode: "api-key",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const localMarkerEnv = resolveEnvApiKey(provider);
|
const localMarkerEnv = resolveConfigAwareEnvApiKey(cfg, provider);
|
||||||
if (localMarkerEnv && isNonSecretApiKeyMarker(localMarkerEnv.apiKey)) {
|
if (localMarkerEnv && isNonSecretApiKeyMarker(localMarkerEnv.apiKey)) {
|
||||||
return {
|
return {
|
||||||
apiKey: localMarkerEnv.apiKey,
|
apiKey: localMarkerEnv.apiKey,
|
||||||
@@ -618,7 +626,7 @@ export async function resolveApiKeyForProvider(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const envResolved = resolveEnvApiKey(provider);
|
const envResolved = resolveConfigAwareEnvApiKey(cfg, provider);
|
||||||
if (envResolved) {
|
if (envResolved) {
|
||||||
const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN")
|
const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN")
|
||||||
? "oauth"
|
? "oauth"
|
||||||
@@ -731,7 +739,7 @@ export function resolveModelAuthMode(
|
|||||||
return "aws-sdk";
|
return "aws-sdk";
|
||||||
}
|
}
|
||||||
|
|
||||||
const envKey = resolveEnvApiKey(resolved);
|
const envKey = resolveConfigAwareEnvApiKey(cfg, resolved);
|
||||||
if (envKey?.apiKey) {
|
if (envKey?.apiKey) {
|
||||||
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
||||||
}
|
}
|
||||||
@@ -763,7 +771,7 @@ export async function hasAvailableAuthForProvider(params: {
|
|||||||
if (authOverride === "aws-sdk") {
|
if (authOverride === "aws-sdk") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (resolveEnvApiKey(provider)) {
|
if (resolveConfigAwareEnvApiKey(cfg, provider)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (resolveUsableCustomProviderApiKey({ cfg, provider })) {
|
if (resolveUsableCustomProviderApiKey({ cfg, provider })) {
|
||||||
|
|||||||
Reference in New Issue
Block a user