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>
This commit is contained in:
Bharghav Srinivasan
2026-07-28 13:31:42 +05:30
committed by GitHub
parent 5d4ff17d7d
commit 7072ba50e2
3 changed files with 48 additions and 3 deletions

View File

@@ -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;

View File

@@ -619,7 +619,6 @@ export async function runGatewayClosePrelude(params: {
disposeBrowserAuthRateLimiter: () => void;
stopChannelHealthMonitor?: () => Promise<void>;
stopReadinessEventLoopHealth?: () => void;
clearSecretsRuntimeSnapshot?: () => void;
closeMcpServer?: () => Promise<void>;
}): Promise<void> {
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<void>) | null;
tailscaleCleanup: (() => Promise<void>) | null;
releasePluginRouteRegistry?: (() => void) | null;
clearSecretsRuntimeSnapshot?: (() => void) | null;
channelIds?: readonly ChannelId[];
stopChannel: (name: ChannelId, accountId?: string) => Promise<void>;
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;

View File

@@ -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,