refactor: thread config runtime env through models config

This commit is contained in:
Peter Steinberger
2026-03-08 16:51:43 +00:00
parent 64d4d9aabb
commit 749eb4efea
5 changed files with 76 additions and 27 deletions

View File

@@ -1,7 +1,7 @@
import fs from "node:fs/promises";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import {
CUSTOM_PROXY_MODELS_CONFIG,
installModelsConfigTestHooks,
unsetEnv,
withModelsTempHome as withTempHome,
@@ -14,33 +14,55 @@ installModelsConfigTestHooks();
const TEST_ENV_VAR = "OPENCLAW_MODELS_CONFIG_TEST_ENV";
describe("models-config", () => {
it("applies config env.vars entries while ensuring models.json", async () => {
it("uses config env.vars entries for implicit provider discovery without mutating process.env", async () => {
await withTempHome(async () => {
await withTempEnv([TEST_ENV_VAR], async () => {
unsetEnv([TEST_ENV_VAR]);
await withTempEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR], async () => {
unsetEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR]);
const cfg: OpenClawConfig = {
...CUSTOM_PROXY_MODELS_CONFIG,
env: { vars: { [TEST_ENV_VAR]: "from-config" } },
models: { providers: {} },
env: {
vars: {
OPENROUTER_API_KEY: "from-config",
[TEST_ENV_VAR]: "from-config",
},
},
};
await ensureOpenClawModelsJson(cfg);
const { agentDir } = await ensureOpenClawModelsJson(cfg);
expect(process.env[TEST_ENV_VAR]).toBe("from-config");
expect(process.env.OPENROUTER_API_KEY).toBeUndefined();
expect(process.env[TEST_ENV_VAR]).toBeUndefined();
const modelsJson = JSON.parse(await fs.readFile(`${agentDir}/models.json`, "utf8")) as {
providers?: { openrouter?: { apiKey?: string } };
};
expect(modelsJson.providers?.openrouter?.apiKey).toBe("OPENROUTER_API_KEY");
});
});
});
it("does not overwrite already-set host env vars", async () => {
it("does not overwrite already-set host env vars while ensuring models.json", async () => {
await withTempHome(async () => {
await withTempEnv([TEST_ENV_VAR], async () => {
await withTempEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR], async () => {
process.env.OPENROUTER_API_KEY = "from-host";
process.env[TEST_ENV_VAR] = "from-host";
const cfg: OpenClawConfig = {
...CUSTOM_PROXY_MODELS_CONFIG,
env: { vars: { [TEST_ENV_VAR]: "from-config" } },
models: { providers: {} },
env: {
vars: {
OPENROUTER_API_KEY: "from-config",
[TEST_ENV_VAR]: "from-config",
},
},
};
await ensureOpenClawModelsJson(cfg);
const { agentDir } = await ensureOpenClawModelsJson(cfg);
const modelsJson = JSON.parse(await fs.readFile(`${agentDir}/models.json`, "utf8")) as {
providers?: { openrouter?: { apiKey?: string } };
};
expect(modelsJson.providers?.openrouter?.apiKey).toBe("OPENROUTER_API_KEY");
expect(process.env.OPENROUTER_API_KEY).toBe("from-host");
expect(process.env[TEST_ENV_VAR]).toBe("from-host");
});
});

View File

@@ -415,8 +415,8 @@ function resolveEnvApiKeyVarName(
return match ? match[1] : undefined;
}
function resolveAwsSdkApiKeyVarName(): string {
return resolveAwsSdkEnvVarName() ?? "AWS_PROFILE";
function resolveAwsSdkApiKeyVarName(env: NodeJS.ProcessEnv = process.env): string {
return resolveAwsSdkEnvVarName(env) ?? "AWS_PROFILE";
}
function normalizeHeaderValues(params: {
@@ -603,6 +603,7 @@ function normalizeAntigravityProvider(provider: ProviderConfig): ProviderConfig
export function normalizeProviders(params: {
providers: ModelsConfig["providers"];
agentDir: string;
env?: NodeJS.ProcessEnv;
secretDefaults?: {
env?: string;
file?: string;
@@ -614,6 +615,7 @@ export function normalizeProviders(params: {
if (!providers) {
return providers;
}
const env = params.env ?? process.env;
const authStore = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
@@ -646,6 +648,7 @@ export function normalizeProviders(params: {
const profileApiKey = resolveApiKeyFromProfiles({
provider: normalizedKey,
store: authStore,
env,
});
if (configuredApiKeyRef && configuredApiKeyRef.id.trim()) {
@@ -687,8 +690,8 @@ export function normalizeProviders(params: {
currentApiKey.trim() &&
!ENV_VAR_NAME_RE.test(currentApiKey.trim())
) {
const envVarName = resolveEnvApiKeyVarName(normalizedKey);
if (envVarName && process.env[envVarName] === currentApiKey) {
const envVarName = resolveEnvApiKeyVarName(normalizedKey, env);
if (envVarName && env[envVarName] === currentApiKey) {
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey: envVarName };
}
@@ -704,11 +707,11 @@ export function normalizeProviders(params: {
const authMode =
normalizedProvider.auth ?? (normalizedKey === "amazon-bedrock" ? "aws-sdk" : undefined);
if (authMode === "aws-sdk") {
const apiKey = resolveAwsSdkApiKeyVarName();
const apiKey = resolveAwsSdkApiKeyVarName(env);
mutated = true;
normalizedProvider = { ...normalizedProvider, apiKey };
} else {
const fromEnv = resolveEnvApiKeyVarName(normalizedKey);
const fromEnv = resolveEnvApiKeyVarName(normalizedKey, env);
const apiKey = fromEnv ?? profileApiKey?.apiKey;
if (apiKey?.trim()) {
if (profileApiKey && profileApiKey.source !== "plaintext") {

View File

@@ -6,7 +6,7 @@ import {
type OpenClawConfig,
loadConfig,
} from "../config/config.js";
import { applyConfigEnvVars } from "../config/env-vars.js";
import { createConfigRuntimeEnv } from "../config/env-vars.js";
import { isRecord } from "../utils.js";
import { resolveOpenClawAgentDir } from "./agent-paths.js";
import {
@@ -46,12 +46,14 @@ async function readExistingModelsFile(pathname: string): Promise<{
async function resolveProvidersForModelsJson(params: {
cfg: OpenClawConfig;
agentDir: string;
env: NodeJS.ProcessEnv;
}): Promise<Record<string, ProviderConfig>> {
const { cfg, agentDir } = params;
const { cfg, agentDir, env } = params;
const explicitProviders = cfg.models?.providers ?? {};
const implicitProviders = await resolveImplicitProviders({
agentDir,
config: cfg,
env,
explicitProviders,
});
const providers: Record<string, ProviderConfig> = mergeProviders({
@@ -143,12 +145,10 @@ export async function ensureOpenClawModelsJson(
return await withModelsJsonWriteLock(targetPath, async () => {
// Ensure config env vars (e.g. AWS_PROFILE, AWS_ACCESS_KEY_ID) are
// available in process.env before implicit provider discovery. Some
// callers (agent runner, tools) pass config objects that haven't gone
// through the full loadConfig() pipeline which applies these.
applyConfigEnvVars(cfg);
// are available to provider discovery without mutating process.env.
const env = createConfigRuntimeEnv(cfg);
const providers = await resolveProvidersForModelsJson({ cfg, agentDir });
const providers = await resolveProvidersForModelsJson({ cfg, agentDir, env });
if (Object.keys(providers).length === 0) {
return { agentDir, wrote: false };
@@ -170,6 +170,7 @@ export async function ensureOpenClawModelsJson(
normalizeProviders({
providers,
agentDir,
env,
secretDefaults: cfg.secrets?.defaults,
secretRefManagedProviders,
}) ?? providers;

View File

@@ -3,7 +3,11 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { loadDotEnv } from "../infra/dotenv.js";
import { resolveConfigEnvVars } from "./env-substitution.js";
import { applyConfigEnvVars, collectConfigRuntimeEnvVars } from "./env-vars.js";
import {
applyConfigEnvVars,
collectConfigRuntimeEnvVars,
createConfigRuntimeEnv,
} from "./env-vars.js";
import { withEnvOverride, withTempHome } from "./test-helpers.js";
import type { OpenClawConfig } from "./types.js";
@@ -29,6 +33,16 @@ describe("config env vars", () => {
});
});
it("can build a merged runtime env without mutating process.env", async () => {
await withEnvOverride({ OPENROUTER_API_KEY: undefined }, async () => {
const merged = createConfigRuntimeEnv({
env: { vars: { OPENROUTER_API_KEY: "config-key" } },
} as OpenClawConfig);
expect(merged.OPENROUTER_API_KEY).toBe("config-key");
expect(process.env.OPENROUTER_API_KEY).toBeUndefined();
});
});
it("blocks dangerous startup env vars from config env", async () => {
await withEnvOverride(
{

View File

@@ -67,6 +67,15 @@ export function collectConfigEnvVars(cfg?: OpenClawConfig): Record<string, strin
return collectConfigRuntimeEnvVars(cfg);
}
export function createConfigRuntimeEnv(
cfg: OpenClawConfig,
baseEnv: NodeJS.ProcessEnv = process.env,
): NodeJS.ProcessEnv {
const env = { ...baseEnv };
applyConfigEnvVars(cfg, env);
return env;
}
export function applyConfigEnvVars(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv = process.env,