fix: harden gateway control-plane restart protections

This commit is contained in:
Peter Steinberger
2026-02-19 14:29:44 +01:00
parent 14b4c7fd56
commit ff74d89e86
11 changed files with 523 additions and 16 deletions

View File

@@ -1,3 +1,5 @@
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import {
@@ -19,7 +21,6 @@ import {
} from "../../config/redact-snapshot.js";
import { buildConfigSchema, type ConfigSchemaResponse } from "../../config/schema.js";
import { extractDeliveryInfo } from "../../config/sessions.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
formatDoctorNonInteractiveHint,
type RestartSentinelPayload,
@@ -27,6 +28,12 @@ import {
} from "../../infra/restart-sentinel.js";
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { loadOpenClawPlugins } from "../../plugins/loader.js";
import { diffConfigPaths } from "../config-reload.js";
import {
formatControlPlaneActor,
resolveControlPlaneActor,
summarizeChangedPaths,
} from "../control-plane-audit.js";
import {
ErrorCodes,
errorShape,
@@ -38,7 +45,6 @@ import {
} from "../protocol/index.js";
import { resolveBaseHashParam } from "./base-hash.js";
import { parseRestartRequestParams } from "./restart-request.js";
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
import { assertValidParams } from "./validation.js";
function requireConfigBaseHash(
@@ -275,7 +281,7 @@ export const configHandlers: GatewayRequestHandlers = {
undefined,
);
},
"config.patch": async ({ params, respond }) => {
"config.patch": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateConfigPatchParams, "config.patch", respond)) {
return;
}
@@ -349,6 +355,11 @@ export const configHandlers: GatewayRequestHandlers = {
);
return;
}
const changedPaths = diffConfigPaths(snapshot.config, validated.config);
const actor = resolveControlPlaneActor(client);
context?.logGateway?.info(
`config.patch write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.patch`,
);
await writeConfigFile(validated.config, writeOptions);
const { sessionKey, note, restartDelayMs, deliveryContext, threadId } =
@@ -365,7 +376,18 @@ export const configHandlers: GatewayRequestHandlers = {
const restart = scheduleGatewaySigusr1Restart({
delayMs: restartDelayMs,
reason: "config.patch",
audit: {
actor: actor.actor,
deviceId: actor.deviceId,
clientIp: actor.clientIp,
changedPaths,
},
});
if (restart.coalesced) {
context?.logGateway?.warn(
`config.patch restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`,
);
}
respond(
true,
{
@@ -381,7 +403,7 @@ export const configHandlers: GatewayRequestHandlers = {
undefined,
);
},
"config.apply": async ({ params, respond }) => {
"config.apply": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateConfigApplyParams, "config.apply", respond)) {
return;
}
@@ -393,6 +415,11 @@ export const configHandlers: GatewayRequestHandlers = {
if (!parsed) {
return;
}
const changedPaths = diffConfigPaths(snapshot.config, parsed.config);
const actor = resolveControlPlaneActor(client);
context?.logGateway?.info(
`config.apply write ${formatControlPlaneActor(actor)} changedPaths=${summarizeChangedPaths(changedPaths)} restartReason=config.apply`,
);
await writeConfigFile(parsed.config, writeOptions);
const { sessionKey, note, restartDelayMs, deliveryContext, threadId } =
@@ -409,7 +436,18 @@ export const configHandlers: GatewayRequestHandlers = {
const restart = scheduleGatewaySigusr1Restart({
delayMs: restartDelayMs,
reason: "config.apply",
audit: {
actor: actor.actor,
deviceId: actor.deviceId,
clientIp: actor.clientIp,
changedPaths,
},
});
if (restart.coalesced) {
context?.logGateway?.warn(
`config.apply restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`,
);
}
respond(
true,
{

View File

@@ -17,6 +17,7 @@ type SubsystemLogger = ReturnType<typeof createSubsystemLogger>;
export type GatewayClient = {
connect: ConnectParams;
connId?: string;
clientIp?: string;
};
export type RespondFn = (

View File

@@ -1,3 +1,4 @@
import type { GatewayRequestHandlers } from "./types.js";
import { loadConfig } from "../../config/config.js";
import { extractDeliveryInfo } from "../../config/sessions.js";
import { resolveOpenClawPackageRoot } from "../../infra/openclaw-root.js";
@@ -9,16 +10,17 @@ import {
import { scheduleGatewaySigusr1Restart } from "../../infra/restart.js";
import { normalizeUpdateChannel } from "../../infra/update-channels.js";
import { runGatewayUpdate } from "../../infra/update-runner.js";
import { formatControlPlaneActor, resolveControlPlaneActor } from "../control-plane-audit.js";
import { validateUpdateRunParams } from "../protocol/index.js";
import { parseRestartRequestParams } from "./restart-request.js";
import type { GatewayRequestHandlers } from "./types.js";
import { assertValidParams } from "./validation.js";
export const updateHandlers: GatewayRequestHandlers = {
"update.run": async ({ params, respond }) => {
"update.run": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateUpdateRunParams, "update.run", respond)) {
return;
}
const actor = resolveControlPlaneActor(client);
const { sessionKey, note, restartDelayMs } = parseRestartRequestParams(params);
const { deliveryContext, threadId } = extractDeliveryInfo(sessionKey);
const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs;
@@ -98,8 +100,22 @@ export const updateHandlers: GatewayRequestHandlers = {
? scheduleGatewaySigusr1Restart({
delayMs: restartDelayMs,
reason: "update.run",
audit: {
actor: actor.actor,
deviceId: actor.deviceId,
clientIp: actor.clientIp,
changedPaths: [],
},
})
: null;
context?.logGateway?.info(
`update.run completed ${formatControlPlaneActor(actor)} changedPaths=<n/a> restartReason=update.run status=${result.status}`,
);
if (restart?.coalesced) {
context?.logGateway?.warn(
`update.run restart coalesced ${formatControlPlaneActor(actor)} delayMs=${restart.delayMs}`,
);
}
respond(
true,