Files
openclaw/src/gateway/server-methods.ts
Val Alexander 05c9492bff fix: reduce WebUI session latency churn (#76277) thanks @BunsDev
Reduce WebUI/Gateway latency churn by avoiding redundant session reloads, carrying session keys through transcript update events, and deferring explicit media provider discovery. Includes changelog attribution and closes the referenced runtime latency issues.
2026-05-02 18:39:06 -05:00

190 lines
7.2 KiB
TypeScript

import { withPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
import { formatControlPlaneActor, resolveControlPlaneActor } from "./control-plane-audit.js";
import { consumeControlPlaneWriteBudget } from "./control-plane-rate-limit.js";
import { ADMIN_SCOPE, authorizeOperatorScopesForMethod } from "./method-scopes.js";
import { ErrorCodes, errorShape } from "./protocol/index.js";
import {
gatewayStartupUnavailableDetails,
GATEWAY_STARTUP_RETRY_AFTER_MS,
} from "./protocol/startup-unavailable.js";
import { isRoleAuthorizedForMethod, parseGatewayRole } from "./role-policy.js";
import { agentHandlers } from "./server-methods/agent.js";
import { agentsHandlers } from "./server-methods/agents.js";
import { artifactsHandlers } from "./server-methods/artifacts.js";
import { channelsHandlers } from "./server-methods/channels.js";
import { chatHandlers } from "./server-methods/chat.js";
import { commandsHandlers } from "./server-methods/commands.js";
import { configHandlers } from "./server-methods/config.js";
import { connectHandlers } from "./server-methods/connect.js";
import { cronHandlers } from "./server-methods/cron.js";
import { deviceHandlers } from "./server-methods/devices.js";
import { diagnosticsHandlers } from "./server-methods/diagnostics.js";
import { doctorHandlers } from "./server-methods/doctor.js";
import { execApprovalsHandlers } from "./server-methods/exec-approvals.js";
import { healthHandlers } from "./server-methods/health.js";
import { logsHandlers } from "./server-methods/logs.js";
import { modelsAuthStatusHandlers } from "./server-methods/models-auth-status.js";
import { modelsHandlers } from "./server-methods/models.js";
import { nativeHookRelayHandlers } from "./server-methods/native-hook-relay.js";
import { nodePendingHandlers } from "./server-methods/nodes-pending.js";
import { nodeHandlers } from "./server-methods/nodes.js";
import { pluginHostHookHandlers } from "./server-methods/plugin-host-hooks.js";
import { pushHandlers } from "./server-methods/push.js";
import { sendHandlers } from "./server-methods/send.js";
import { sessionsHandlers } from "./server-methods/sessions.js";
import { skillsHandlers } from "./server-methods/skills.js";
import { systemHandlers } from "./server-methods/system.js";
import { talkHandlers } from "./server-methods/talk.js";
import { toolsCatalogHandlers } from "./server-methods/tools-catalog.js";
import { toolsEffectiveHandlers } from "./server-methods/tools-effective.js";
import { toolsInvokeHandlers } from "./server-methods/tools-invoke.js";
import { ttsHandlers } from "./server-methods/tts.js";
import type { GatewayRequestHandlers, GatewayRequestOptions } from "./server-methods/types.js";
import { updateHandlers } from "./server-methods/update.js";
import { usageHandlers } from "./server-methods/usage.js";
import { voicewakeRoutingHandlers } from "./server-methods/voicewake-routing.js";
import { voicewakeHandlers } from "./server-methods/voicewake.js";
import { webHandlers } from "./server-methods/web.js";
import { wizardHandlers } from "./server-methods/wizard.js";
const CONTROL_PLANE_WRITE_METHODS = new Set(["config.apply", "config.patch", "update.run"]);
function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["client"]) {
if (!client?.connect) {
return null;
}
if (method === "health") {
return null;
}
const roleRaw = client.connect.role ?? "operator";
const role = parseGatewayRole(roleRaw);
if (!role) {
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${roleRaw}`);
}
const scopes = client.connect.scopes ?? [];
if (!isRoleAuthorizedForMethod(role, method)) {
return errorShape(ErrorCodes.INVALID_REQUEST, `unauthorized role: ${role}`);
}
if (role === "node") {
return null;
}
if (scopes.includes(ADMIN_SCOPE)) {
return null;
}
const scopeAuth = authorizeOperatorScopesForMethod(method, scopes);
if (!scopeAuth.allowed) {
return errorShape(ErrorCodes.INVALID_REQUEST, `missing scope: ${scopeAuth.missingScope}`);
}
return null;
}
export const coreGatewayHandlers: GatewayRequestHandlers = {
...connectHandlers,
...logsHandlers,
...voicewakeHandlers,
...voicewakeRoutingHandlers,
...healthHandlers,
...channelsHandlers,
...chatHandlers,
...commandsHandlers,
...cronHandlers,
...deviceHandlers,
...diagnosticsHandlers,
...doctorHandlers,
...execApprovalsHandlers,
...webHandlers,
...modelsHandlers,
...modelsAuthStatusHandlers,
...nativeHookRelayHandlers,
...pluginHostHookHandlers,
...configHandlers,
...wizardHandlers,
...talkHandlers,
...toolsCatalogHandlers,
...toolsEffectiveHandlers,
...toolsInvokeHandlers,
...ttsHandlers,
...skillsHandlers,
...sessionsHandlers,
...systemHandlers,
...updateHandlers,
...nodeHandlers,
...nodePendingHandlers,
...pushHandlers,
...sendHandlers,
...usageHandlers,
...agentHandlers,
...agentsHandlers,
...artifactsHandlers,
};
export async function handleGatewayRequest(
opts: GatewayRequestOptions & { extraHandlers?: GatewayRequestHandlers },
): Promise<void> {
const { req, respond, client, isWebchatConnect, context } = opts;
const authError = authorizeGatewayMethod(req.method, client);
if (authError) {
respond(false, undefined, authError);
return;
}
if (context.unavailableGatewayMethods?.has(req.method)) {
respond(
false,
undefined,
errorShape(ErrorCodes.UNAVAILABLE, `${req.method} unavailable during gateway startup`, {
retryable: true,
retryAfterMs: GATEWAY_STARTUP_RETRY_AFTER_MS,
details: { ...gatewayStartupUnavailableDetails(), method: req.method },
}),
);
return;
}
if (CONTROL_PLANE_WRITE_METHODS.has(req.method)) {
const budget = consumeControlPlaneWriteBudget({ client });
if (!budget.allowed) {
const actor = resolveControlPlaneActor(client);
context.logGateway.warn(
`control-plane write rate-limited method=${req.method} ${formatControlPlaneActor(actor)} retryAfterMs=${budget.retryAfterMs} key=${budget.key}`,
);
respond(
false,
undefined,
errorShape(
ErrorCodes.UNAVAILABLE,
`rate limit exceeded for ${req.method}; retry after ${Math.ceil(budget.retryAfterMs / 1000)}s`,
{
retryable: true,
retryAfterMs: budget.retryAfterMs,
details: {
method: req.method,
limit: "3 per 60s",
},
},
),
);
return;
}
}
const handler = opts.extraHandlers?.[req.method] ?? coreGatewayHandlers[req.method];
if (!handler) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, `unknown method: ${req.method}`),
);
return;
}
const invokeHandler = () =>
handler({
req,
params: (req.params ?? {}) as Record<string, unknown>,
client,
isWebchatConnect,
respond,
context,
});
// All handlers run inside a request scope so that plugin runtime
// subagent methods (e.g. context engine tools spawning sub-agents
// during tool execution) can dispatch back into the gateway.
await withPluginRuntimeGatewayRequestScope({ context, client, isWebchatConnect }, invokeHandler);
}