From 2e53033f221de76b697221790cae98d4a73dc215 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Sun, 22 Feb 2026 14:41:26 -0800 Subject: [PATCH] Gateway: serialize secrets activation across reload paths --- src/gateway/server.impl.ts | 86 +++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 133c79e2f..b3e6a9b3c 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -270,49 +270,59 @@ export async function startGatewayServer( contextKey: code, }); }; + let secretsActivationTail: Promise = Promise.resolve(); + const runWithSecretsActivationLock = async (operation: () => Promise): Promise => { + const run = secretsActivationTail.then(operation, operation); + secretsActivationTail = run.then( + () => undefined, + () => undefined, + ); + return await run; + }; const activateRuntimeSecrets = async ( config: OpenClawConfig, params: { reason: "startup" | "reload" | "restart-check"; activate: boolean }, - ) => { - try { - const prepared = await prepareSecretsRuntimeSnapshot({ config }); - if (params.activate) { - activateSecretsRuntimeSnapshot(prepared); - } - for (const warning of prepared.warnings) { - logSecrets.warn(`[${warning.code}] ${warning.message}`); - } - if (secretsDegraded) { - const recoveredMessage = - "Secret resolution recovered; runtime remained on last-known-good during the outage."; - logSecrets.info(`[SECRETS_RELOADER_RECOVERED] ${recoveredMessage}`); - emitSecretsStateEvent("SECRETS_RELOADER_RECOVERED", recoveredMessage, prepared.config); - } - secretsDegraded = false; - return prepared; - } catch (err) { - const details = String(err); - if (!secretsDegraded) { - logSecrets.error(`[SECRETS_RELOADER_DEGRADED] ${details}`); - if (params.reason !== "startup") { - emitSecretsStateEvent( - "SECRETS_RELOADER_DEGRADED", - `Secret resolution failed; runtime remains on last-known-good snapshot. ${details}`, - config, - ); + ) => + await runWithSecretsActivationLock(async () => { + try { + const prepared = await prepareSecretsRuntimeSnapshot({ config }); + if (params.activate) { + activateSecretsRuntimeSnapshot(prepared); } - } else { - logSecrets.warn(`[SECRETS_RELOADER_DEGRADED] ${details}`); + for (const warning of prepared.warnings) { + logSecrets.warn(`[${warning.code}] ${warning.message}`); + } + if (secretsDegraded) { + const recoveredMessage = + "Secret resolution recovered; runtime remained on last-known-good during the outage."; + logSecrets.info(`[SECRETS_RELOADER_RECOVERED] ${recoveredMessage}`); + emitSecretsStateEvent("SECRETS_RELOADER_RECOVERED", recoveredMessage, prepared.config); + } + secretsDegraded = false; + return prepared; + } catch (err) { + const details = String(err); + if (!secretsDegraded) { + logSecrets.error(`[SECRETS_RELOADER_DEGRADED] ${details}`); + if (params.reason !== "startup") { + emitSecretsStateEvent( + "SECRETS_RELOADER_DEGRADED", + `Secret resolution failed; runtime remains on last-known-good snapshot. ${details}`, + config, + ); + } + } else { + logSecrets.warn(`[SECRETS_RELOADER_DEGRADED] ${details}`); + } + secretsDegraded = true; + if (params.reason === "startup") { + throw new Error(`Startup failed: required secrets are unavailable. ${details}`, { + cause: err, + }); + } + throw err; } - secretsDegraded = true; - if (params.reason === "startup") { - throw new Error(`Startup failed: required secrets are unavailable. ${details}`, { - cause: err, - }); - } - throw err; - } - }; + }); // Fail fast before startup if required refs are unresolved. let cfgAtStart: OpenClawConfig;