SecretRef: harden custom/provider secret persistence and reuse (#42554)

* Models: gate custom provider keys by usable secret semantics

* Config: project runtime writes onto source snapshot

* Models: prevent stale apiKey preservation for marker-managed providers

* Runner: strip SecretRef marker headers from resolved models

* Secrets: scan active agent models.json path in audit

* Config: guard runtime-source projection for unrelated configs

* Extensions: fix onboarding type errors in CI

* Tests: align setup helper account-enabled expectation

* Secrets audit: harden models.json file reads

* fix: harden SecretRef custom/provider secret persistence (#42554) (thanks @joshavant)
This commit is contained in:
Josh Avant
2026-03-10 18:46:47 -05:00
committed by Peter Steinberger
parent 20237358d9
commit 36d2ae2a22
40 changed files with 651 additions and 73 deletions

View File

@@ -18,7 +18,11 @@ import {
resolveAuthStorePathForDisplay,
} from "./auth-profiles.js";
import { PROVIDER_ENV_API_KEY_CANDIDATES } from "./model-auth-env-vars.js";
import { OLLAMA_LOCAL_AUTH_MARKER } from "./model-auth-markers.js";
import {
isKnownEnvApiKeyMarker,
isNonSecretApiKeyMarker,
OLLAMA_LOCAL_AUTH_MARKER,
} from "./model-auth-markers.js";
import { normalizeProviderId } from "./model-selection.js";
export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js";
@@ -60,6 +64,49 @@ export function getCustomProviderApiKey(
return normalizeOptionalSecretInput(entry?.apiKey);
}
type ResolvedCustomProviderApiKey = {
apiKey: string;
source: string;
};
export function resolveUsableCustomProviderApiKey(params: {
cfg: OpenClawConfig | undefined;
provider: string;
env?: NodeJS.ProcessEnv;
}): ResolvedCustomProviderApiKey | null {
const customKey = getCustomProviderApiKey(params.cfg, params.provider);
if (!customKey) {
return null;
}
if (!isNonSecretApiKeyMarker(customKey)) {
return { apiKey: customKey, source: "models.json" };
}
if (!isKnownEnvApiKeyMarker(customKey)) {
return null;
}
const envValue = normalizeOptionalSecretInput((params.env ?? process.env)[customKey]);
if (!envValue) {
return null;
}
const applied = new Set(getShellEnvAppliedKeys());
return {
apiKey: envValue,
source: resolveEnvSourceLabel({
applied,
envVars: [customKey],
label: `${customKey} (models.json marker)`,
}),
};
}
export function hasUsableCustomProviderApiKey(
cfg: OpenClawConfig | undefined,
provider: string,
env?: NodeJS.ProcessEnv,
): boolean {
return Boolean(resolveUsableCustomProviderApiKey({ cfg, provider, env }));
}
function resolveProviderAuthOverride(
cfg: OpenClawConfig | undefined,
provider: string,
@@ -238,9 +285,9 @@ export async function resolveApiKeyForProvider(params: {
};
}
const customKey = getCustomProviderApiKey(cfg, provider);
const customKey = resolveUsableCustomProviderApiKey({ cfg, provider });
if (customKey) {
return { apiKey: customKey, source: "models.json", mode: "api-key" };
return { apiKey: customKey.apiKey, source: customKey.source, mode: "api-key" };
}
const syntheticLocalAuth = resolveSyntheticLocalProviderAuth({ cfg, provider });
@@ -360,7 +407,7 @@ export function resolveModelAuthMode(
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
}
if (getCustomProviderApiKey(cfg, resolved)) {
if (hasUsableCustomProviderApiKey(cfg, resolved)) {
return "api-key";
}