diff --git a/src/gateway/server-methods/config-write-flow.ts b/src/gateway/server-methods/config-write-flow.ts index 7663814f40e..ea6eda4b361 100644 --- a/src/gateway/server-methods/config-write-flow.ts +++ b/src/gateway/server-methods/config-write-flow.ts @@ -12,6 +12,7 @@ import { writeRestartSentinel, } from "../../infra/restart-sentinel.js"; import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js"; +import { getActiveSecretsRuntimeSnapshot } from "../../secrets/runtime.js"; import { resolveEffectiveSharedGatewayAuth } from "../auth.js"; import { buildGatewayReloadPlan, resolveGatewayReloadSettings } from "../config-reload.js"; import { formatControlPlaneActor, type ControlPlaneActor } from "../control-plane-audit.js"; @@ -46,6 +47,16 @@ export function didSharedGatewayAuthChange(prev: OpenClawConfig, next: OpenClawC return prevAuth.mode !== nextAuth.mode || !isDeepStrictEqual(prevAuth.secret, nextAuth.secret); } +export function didActiveSharedGatewayAuthChange(params: { + fallbackPrev: OpenClawConfig; + next: OpenClawConfig; +}): boolean { + return didSharedGatewayAuthChange( + getActiveSecretsRuntimeSnapshot()?.config ?? params.fallbackPrev, + params.next, + ); +} + function queueSharedGatewayAuthDisconnect( shouldDisconnect: boolean, context?: GatewayRequestContext, diff --git a/src/gateway/server-methods/config.shared-auth.test.ts b/src/gateway/server-methods/config.shared-auth.test.ts index f8e5924cab5..cffa90c73ce 100644 --- a/src/gateway/server-methods/config.shared-auth.test.ts +++ b/src/gateway/server-methods/config.shared-auth.test.ts @@ -41,6 +41,7 @@ vi.mock("../../config/runtime-schema.js", () => ({ })); vi.mock("../../secrets/runtime.js", () => ({ + getActiveSecretsRuntimeSnapshot: () => null, prepareSecretsRuntimeSnapshot: prepareSecretsRuntimeSnapshotMock, })); @@ -69,7 +70,11 @@ beforeEach(() => { ok: true, config, })); - prepareSecretsRuntimeSnapshotMock.mockResolvedValue(undefined); + prepareSecretsRuntimeSnapshotMock.mockImplementation( + async ({ config }: { config: OpenClawConfig }) => ({ + config, + }), + ); restartSentinelMocks.writeRestartSentinel.mockClear(); }); diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 34e672b1e71..9ff87500120 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -18,7 +18,10 @@ import { loadGatewayRuntimeConfigSchema } from "../../config/runtime-schema.js"; import { lookupConfigSchema, type ConfigSchemaResponse } from "../../config/schema.js"; import type { ConfigValidationIssue, OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; -import { prepareSecretsRuntimeSnapshot } from "../../secrets/runtime.js"; +import { + prepareSecretsRuntimeSnapshot, + type PreparedSecretsRuntimeSnapshot, +} from "../../secrets/runtime.js"; import { diffConfigPaths } from "../config-reload.js"; import { formatControlPlaneActor, @@ -40,6 +43,7 @@ import { import { resolveBaseHashParam } from "./base-hash.js"; import { commitGatewayConfigWrite, + didActiveSharedGatewayAuthChange, didSharedGatewayAuthChange, resolveGatewayConfigPath, resolveGatewayConfigRestartWriteResult, @@ -234,13 +238,12 @@ function summarizeConfigValidationIssues(issues: ReadonlyArray { +}): Promise { try { - await prepareSecretsRuntimeSnapshot({ + return await prepareSecretsRuntimeSnapshot({ config: params.config, includeAuthStoreRefs: false, }); - return true; } catch (error) { const details = formatErrorMessage(error); params.respond( @@ -251,7 +254,7 @@ async function ensureResolvableSecretRefsOrRespond(params: { `invalid config: active SecretRef resolution failed (${details})`, ), ); - return false; + return null; } } @@ -415,7 +418,11 @@ export const configHandlers: GatewayRequestHandlers = { ); return; } - if (!(await ensureResolvableSecretRefsOrRespond({ config: validated.config, respond }))) { + const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({ + config: validated.config, + respond, + }); + if (!preparedSecretsSnapshot) { return; } const changedPaths = diffConfigPaths(snapshot.config, validated.config); @@ -447,10 +454,12 @@ export const configHandlers: GatewayRequestHandlers = { ); // Compare before the write so we invalidate clients authenticated against the // previous shared secret immediately after the config update succeeds. - const disconnectSharedAuthClients = didSharedGatewayAuthChange( - snapshot.config, - validated.config, - ); + const disconnectSharedAuthClients = + didSharedGatewayAuthChange(snapshot.config, validated.config) || + didActiveSharedGatewayAuthChange({ + fallbackPrev: snapshot.config, + next: preparedSecretsSnapshot.config, + }); const writeResult = await commitGatewayConfigWrite({ snapshot, writeOptions, @@ -497,7 +506,11 @@ export const configHandlers: GatewayRequestHandlers = { if (!parsed) { return; } - if (!(await ensureResolvableSecretRefsOrRespond({ config: parsed.config, respond }))) { + const preparedSecretsSnapshot = await ensureResolvableSecretRefsOrRespond({ + config: parsed.config, + respond, + }); + if (!preparedSecretsSnapshot) { return; } const changedPaths = diffConfigPaths(snapshot.config, parsed.config); @@ -507,7 +520,12 @@ export const configHandlers: GatewayRequestHandlers = { ); // Compare before the write so we invalidate clients authenticated against the // previous shared secret immediately after the config update succeeds. - const disconnectSharedAuthClients = didSharedGatewayAuthChange(snapshot.config, parsed.config); + const disconnectSharedAuthClients = + didSharedGatewayAuthChange(snapshot.config, parsed.config) || + didActiveSharedGatewayAuthChange({ + fallbackPrev: snapshot.config, + next: preparedSecretsSnapshot.config, + }); const writeResult = await commitGatewayConfigWrite({ snapshot, writeOptions,