Gateway: add eager secrets runtime snapshot activation

This commit is contained in:
joshavant
2026-02-21 11:13:25 -08:00
committed by Peter Steinberger
parent 2f3b919b94
commit b50c4c2c44
12 changed files with 758 additions and 10 deletions

View File

@@ -17,7 +17,10 @@ export {
suggestOAuthProfileIdForLegacyDefault,
} from "./auth-profiles/repair.js";
export {
clearRuntimeAuthProfileStoreSnapshots,
ensureAuthProfileStore,
loadAuthProfileStoreForRuntime,
replaceRuntimeAuthProfileStoreSnapshots,
loadAuthProfileStore,
saveAuthProfileStore,
} from "./auth-profiles/store.js";

View File

@@ -14,6 +14,65 @@ type RejectedCredentialEntry = { key: string; reason: CredentialRejectReason };
const AUTH_PROFILE_TYPES = new Set<AuthProfileCredential["type"]>(["api_key", "oauth", "token"]);
const runtimeAuthStoreSnapshots = new Map<string, AuthProfileStore>();
function resolveRuntimeStoreKey(agentDir?: string): string {
return resolveAuthStorePath(agentDir);
}
function cloneAuthProfileStore(store: AuthProfileStore): AuthProfileStore {
return structuredClone(store);
}
function resolveRuntimeAuthProfileStore(agentDir?: string): AuthProfileStore | null {
if (runtimeAuthStoreSnapshots.size === 0) {
return null;
}
const mainKey = resolveRuntimeStoreKey(undefined);
const requestedKey = resolveRuntimeStoreKey(agentDir);
const mainStore = runtimeAuthStoreSnapshots.get(mainKey);
const requestedStore = runtimeAuthStoreSnapshots.get(requestedKey);
if (!agentDir || requestedKey === mainKey) {
if (!mainStore) {
return null;
}
return cloneAuthProfileStore(mainStore);
}
if (mainStore && requestedStore) {
return mergeAuthProfileStores(
cloneAuthProfileStore(mainStore),
cloneAuthProfileStore(requestedStore),
);
}
if (requestedStore) {
return cloneAuthProfileStore(requestedStore);
}
if (mainStore) {
return cloneAuthProfileStore(mainStore);
}
return null;
}
export function replaceRuntimeAuthProfileStoreSnapshots(
entries: Array<{ agentDir?: string; store: AuthProfileStore }>,
): void {
runtimeAuthStoreSnapshots.clear();
for (const entry of entries) {
runtimeAuthStoreSnapshots.set(
resolveRuntimeStoreKey(entry.agentDir),
cloneAuthProfileStore(entry.store),
);
}
}
export function clearRuntimeAuthProfileStoreSnapshots(): void {
runtimeAuthStoreSnapshots.clear();
}
export async function updateAuthProfileStoreWithLock(params: {
agentDir?: string;
updater: (store: AuthProfileStore) => boolean;
@@ -372,10 +431,30 @@ function loadAuthProfileStoreForAgent(
return store;
}
export function loadAuthProfileStoreForRuntime(
agentDir?: string,
options?: { allowKeychainPrompt?: boolean },
): AuthProfileStore {
const store = loadAuthProfileStoreForAgent(agentDir, options);
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();
if (!agentDir || authPath === mainAuthPath) {
return store;
}
const mainStore = loadAuthProfileStoreForAgent(undefined, options);
return mergeAuthProfileStores(mainStore, store);
}
export function ensureAuthProfileStore(
agentDir?: string,
options?: { allowKeychainPrompt?: boolean },
): AuthProfileStore {
const runtimeStore = resolveRuntimeAuthProfileStore(agentDir);
if (runtimeStore) {
return runtimeStore;
}
const store = loadAuthProfileStoreForAgent(agentDir, options);
const authPath = resolveAuthStorePath(agentDir);
const mainAuthPath = resolveAuthStorePath();

View File

@@ -1,10 +1,12 @@
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import type { OpenClawConfig } from "../../config/config.js";
import type { SecretRef } from "../../config/types.secrets.js";
export type ApiKeyCredential = {
type: "api_key";
provider: string;
key?: string;
keyRef?: SecretRef;
email?: string;
/** Optional provider-specific metadata (e.g., account IDs, gateway IDs). */
metadata?: Record<string, string>;
@@ -18,6 +20,7 @@ export type TokenCredential = {
type: "token";
provider: string;
token: string;
tokenRef?: SecretRef;
/** Optional expiry timestamp (ms since epoch). */
expires?: number;
email?: string;

View File

@@ -12,6 +12,7 @@ import {
KILOCODE_DEFAULT_MAX_TOKENS,
KILOCODE_MODEL_CATALOG,
} from "../providers/kilocode-shared.js";
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
import { discoverBedrockModels } from "./bedrock-discovery.js";
import {
@@ -405,16 +406,17 @@ export function normalizeProviders(params: {
for (const [key, provider] of Object.entries(providers)) {
const normalizedKey = key.trim();
let normalizedProvider = provider;
const configuredApiKey = normalizedProvider.apiKey;
// Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR".
if (
normalizedProvider.apiKey &&
normalizeApiKeyConfig(normalizedProvider.apiKey) !== normalizedProvider.apiKey
typeof configuredApiKey === "string" &&
normalizeApiKeyConfig(configuredApiKey) !== configuredApiKey
) {
mutated = true;
normalizedProvider = {
...normalizedProvider,
apiKey: normalizeApiKeyConfig(normalizedProvider.apiKey),
apiKey: normalizeApiKeyConfig(configuredApiKey),
};
}
@@ -422,7 +424,9 @@ export function normalizeProviders(params: {
// Fill it from the environment or auth profiles when possible.
const hasModels =
Array.isArray(normalizedProvider.models) && normalizedProvider.models.length > 0;
if (hasModels && !normalizedProvider.apiKey?.trim()) {
const normalizedApiKey = normalizeOptionalSecretInput(normalizedProvider.apiKey);
const hasConfiguredApiKey = Boolean(normalizedApiKey || normalizedProvider.apiKey);
if (hasModels && !hasConfiguredApiKey) {
const authMode =
normalizedProvider.auth ?? (normalizedKey === "amazon-bedrock" ? "aws-sdk" : undefined);
if (authMode === "aws-sdk") {