Files
Moltbot/src/gateway/server-methods.ts
Ruslan Kharitonov 8d69251475 fix(doctor): use gateway health status for memory search key check (#22327)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 2f02ec94030754a2e2dfeb7d5a80f14747373ab5
Co-authored-by: therk <901920+therk@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
2026-02-23 14:07:16 -05:00

150 lines
5.3 KiB
TypeScript

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 { isRoleAuthorizedForMethod, parseGatewayRole } from "./role-policy.js";
import { agentHandlers } from "./server-methods/agent.js";
import { agentsHandlers } from "./server-methods/agents.js";
import { browserHandlers } from "./server-methods/browser.js";
import { channelsHandlers } from "./server-methods/channels.js";
import { chatHandlers } from "./server-methods/chat.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 { 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 { modelsHandlers } from "./server-methods/models.js";
import { nodeHandlers } from "./server-methods/nodes.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 { 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 { 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,
...healthHandlers,
...channelsHandlers,
...chatHandlers,
...cronHandlers,
...deviceHandlers,
...doctorHandlers,
...execApprovalsHandlers,
...webHandlers,
...modelsHandlers,
...configHandlers,
...wizardHandlers,
...talkHandlers,
...toolsCatalogHandlers,
...ttsHandlers,
...skillsHandlers,
...sessionsHandlers,
...systemHandlers,
...updateHandlers,
...nodeHandlers,
...pushHandlers,
...sendHandlers,
...usageHandlers,
...agentHandlers,
...agentsHandlers,
...browserHandlers,
};
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 (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;
}
await handler({
req,
params: (req.params ?? {}) as Record<string, unknown>,
client,
isWebchatConnect,
respond,
context,
});
}