From 7072ba50e2ffc09cfa1cc87b8be05c27ea131ff3 Mon Sep 17 00:00:00 2001 From: Bharghav Srinivasan <81548305+BharZInstein@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:31:42 +0530 Subject: [PATCH] fix(gateway): shutdown reports unresolved SecretRef warnings when channel credentials use exec/file refs (#112717) * fix(gateway): clear secrets runtime snapshot only after channel teardown Shutdown cleared the secrets runtime snapshot in the close prelude, which also drops the pinned runtime config. The channels close step then lazily re-pins the raw config, so stopChannel's resolveAccount hit unresolved SecretRefs and every shutdown of a gateway with an exec/file SecretRef credential logged an unresolved-SecretRef WARN and completed with warnings. The failed resolveAccount also skipped the per-account stop bookkeeping (graceful wait, task/abort cleanup, stopped-runtime state) and the plugin stopAccount hook. Move the scrub into the close handler's finally block so it still always runs, but only after every step that can read credentials. Fixes #112681 * chore: drop release-owned changelog entry --------- Co-authored-by: FullerStackDev <263060202+fuller-stack-dev@users.noreply.github.com> --- src/gateway/server-close.test.ts | 39 ++++++++++++++++++++++++++++++++ src/gateway/server-close.ts | 10 ++++++-- src/gateway/server-lifecycle.ts | 2 +- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/gateway/server-close.test.ts b/src/gateway/server-close.test.ts index 1634ae56c33b..04fa3580fc4d 100644 --- a/src/gateway/server-close.test.ts +++ b/src/gateway/server-close.test.ts @@ -281,6 +281,45 @@ describe("createGatewayCloseHandler", () => { expect(stopChannel).toHaveBeenCalledWith("discord"); }); + it("clears the secrets runtime snapshot only after channels stop (#112681)", async () => { + const events: string[] = []; + const stopChannel = vi.fn(async (channelId: string) => { + events.push(`channel:${channelId}`); + }); + const clearSecretsRuntimeSnapshot = vi.fn(() => { + events.push("clear-secrets"); + }); + const close = createGatewayCloseHandler( + createGatewayCloseTestDeps({ + channelIds: ["telegram"], + stopChannel, + clearSecretsRuntimeSnapshot, + }), + ); + + await close({ reason: "test" }); + + expect(events).toEqual(["channel:telegram", "clear-secrets"]); + }); + + it("clears the secrets runtime snapshot even when a channel stop fails", async () => { + const clearSecretsRuntimeSnapshot = vi.fn(); + const close = createGatewayCloseHandler( + createGatewayCloseTestDeps({ + channelIds: ["telegram"], + stopChannel: vi.fn(async () => { + throw new Error("stop failed"); + }), + clearSecretsRuntimeSnapshot, + }), + ); + + const result = await close({ reason: "test" }); + + expect(clearSecretsRuntimeSnapshot).toHaveBeenCalledTimes(1); + expect(result.warnings).toContain("channel/telegram"); + }); + it("awaits post-ready sidecars before plugin services and channels", async () => { const events: string[] = []; let releaseSidecar!: () => void; diff --git a/src/gateway/server-close.ts b/src/gateway/server-close.ts index 2dd9fa6ec5f3..a27f6d09477a 100644 --- a/src/gateway/server-close.ts +++ b/src/gateway/server-close.ts @@ -619,7 +619,6 @@ export async function runGatewayClosePrelude(params: { disposeBrowserAuthRateLimiter: () => void; stopChannelHealthMonitor?: () => Promise; stopReadinessEventLoopHealth?: () => void; - clearSecretsRuntimeSnapshot?: () => void; closeMcpServer?: () => Promise; }): Promise { params.stopDiagnostics?.(); @@ -629,7 +628,6 @@ export async function runGatewayClosePrelude(params: { params.disposeBrowserAuthRateLimiter(); await params.stopChannelHealthMonitor?.(); params.stopReadinessEventLoopHealth?.(); - params.clearSecretsRuntimeSnapshot?.(); await params.closeMcpServer?.().catch(() => {}); } @@ -674,6 +672,7 @@ export function createGatewayCloseHandler( bonjourStop: (() => Promise) | null; tailscaleCleanup: (() => Promise) | null; releasePluginRouteRegistry?: (() => void) | null; + clearSecretsRuntimeSnapshot?: (() => void) | null; channelIds?: readonly ChannelId[]; stopChannel: (name: ChannelId, accountId?: string) => Promise; pluginServices: PluginServicesHandle | null; @@ -1043,6 +1042,13 @@ export function createGatewayCloseHandler( } catch { /* ignore */ } + // Channel and plugin teardown still resolve account credentials. Keep the + // active snapshot until every teardown owner is done, then always scrub it. + try { + params.clearSecretsRuntimeSnapshot?.(); + } catch { + /* ignore */ + } } const durationMs = Date.now() - start; diff --git a/src/gateway/server-lifecycle.ts b/src/gateway/server-lifecycle.ts index f78cbcacf3ff..ae54d03f7240 100644 --- a/src/gateway/server-lifecycle.ts +++ b/src/gateway/server-lifecycle.ts @@ -370,7 +370,6 @@ export async function prepareGatewayLifecycle(params: { await monitor?.waitForIdle(); }, stopReadinessEventLoopHealth: readinessEventLoopHealth.stop, - clearSecretsRuntimeSnapshot, closeMcpServer: closeMcpLoopbackServerOnDemand, }); }; @@ -407,6 +406,7 @@ export async function prepareGatewayLifecycle(params: { bonjourStop: runtimeState.bonjourStop, tailscaleCleanup: runtimeState.tailscaleCleanup, releasePluginRouteRegistry, + clearSecretsRuntimeSnapshot, channelIds, stopChannel, pluginServices: runtimeState.pluginServices,