Skip to content

Tabla de métodos RPC y despacho de peticiones

源码版本v2026.6.11

Responsabilidad

server-methods.ts es el centro RPC del gateway: despacha las peticiones JSON-RPC entrantes por nombre de método a un handler, y se encarga de la autorización (authorization), rate limiting y aislamiento del ámbito de petición. Los handlers que de verdad hacen el trabajo están repartidos en submódulos de server-methods/ divididos por familia — chat, agents, cron, channels, device, artifacts, connect, etc.

Esta capa hace cuatro cosas: agrega los métodos núcleo en una tabla (coreGatewayHandlers), construye temporalmente un registry por petición, autoriza y entrega al handler, y por último ejecuta el handler dentro del ámbito de petición del plugin (plugin runtime request scope), para que los subagent que el handler spawnee puedan seguir invocando métodos del gateway.

Motivación de diseño

¿Por qué no escribir directamente una gran tabla switch(method)? Porque el gateway debe soportar registro de métodos por parte de plugins en caliente, debe permitir que las pruebas sobrescriban métodos núcleo y debe anunciar la tabla de métodos temprano en el arranque (muchos módulos de handler no se han cargado todavía en ese punto). Estas tres restricciones juntas exigen que la tabla sea declaración primero, carga diferida, fusión por petición.

Declaración primero significa que coreGatewayHandlers es un simple mapa {nombreMétodo: handler}; el handler se envuelve con createLazyCoreHandlers que hace dynamic import — solo cuando se invoca el método por primera vez se carga el módulo; las llamadas siguientes reutilizan la misma promesa de import (lazyHandlerModule hace handlersPromise ??= cache), evitando cargas duplicadas bajo concurrencia. Fusión por petición significa que createRequestGatewayMethodRegistry, al llegar una petición, toma los handlers del plugin activo en ese momento desde el estado global, y los fusiona con la tabla núcleo y los handlers extra del llamador — así los métodos registrados hot por los plugins son visibles de inmediato para las peticiones en curso, sin necesidad de reiniciar.

Archivos clave

Flujo de datos

La tabla núcleo de métodos es un objeto literal enorme (coreGatewayHandlers:269) agrupado por familias, cada una envuelta en createLazyCoreHandlers:

typescript
export const coreGatewayHandlers: GatewayRequestHandlers = {
  ...createLazyCoreHandlers({
    methods: ["chat.history", "chat.startup", "chat.metadata",
             "chat.message.get", "chat.abort", "chat.send", "chat.inject"],
    loadHandlers: loadChatHandlers,
  }),
  ...createLazyCoreHandlers({
    methods: ["wake", "cron.list", "cron.status", "cron.get", "cron.add",
             "cron.update", "cron.remove", "cron.run", "cron.runs"],
    loadHandlers: loadCronHandlers,
  }),
  // ...channels/device/agents/artifacts/sessions...
};

Se ve que los nombres de método siguen una jerarquía con puntos: chat.* sesiones de chat, cron.* tareas programadas, channels.* arranque/parada de canales, device.pair.* emparejamiento de dispositivos. wake es el método de wake de nivel superior, no bajo prefijo de familia.

createLazyCoreHandlers (createLazyCoreHandlers:44) genera un wrapper por cada nombre de método; solo la primera invocación dispara loadHandlers() y se cachea en el closure. El punto crítico es que cualquier descriptor drift lanza error obligatoriamente:

typescript
async (opts: GatewayRequestHandlerOptions) => {
  const handlers = await params.loadHandlers();
  const handler = handlers[method];
  if (!handler) {
    // Descriptor drift should fail loudly: advertised core methods must exist in the
    // loaded family module once the lazy boundary resolves.
    throw new Error(`lazy gateway handler not found: ${method}`);
  }
  await handler(opts);
}

Si se declara un nombre de método pero el módulo de familia cargado no tiene el handler correspondiente, es un error de configuración: debe lanzar error, no devolver silenciosamente un unknown method.

Al llegar una petición, handleGatewayRequest (handleGatewayRequest:638) primero decide qué registry usar, luego autoriza y por último ejecuta el handler:

typescript
// When the attached snapshot does not own the method, rebuild from the live plugin registry
// so plugin RPC methods registered after the startup snapshot stay reachable (#94127).
const methodRegistry =
  opts.methodRegistry?.getHandler(req.method) !== undefined
    ? opts.methodRegistry
    : createRequestGatewayMethodRegistry(opts.extraHandlers);
const authError = authorizeGatewayMethod(req.method, client, req.params, methodRegistry);
if (authError) {
  respond(false, undefined, authError);
  return;
}

Esto arregla #94127: al arrancar se toma un snapshot de métodos, pero los métodos que un plugin registre después no están en el snapshot. La solución es comprobar primero si el snapshot tiene el método; si no, caer a createRequestGatewayMethodRegistry para reconstruir desde el estado live de plugins — la reconstrucción es barata y solo ocurre en miss del snapshot.

La autorización va en dos capas: primero role (operator / node / admin); los roles node y las conexiones con ADMIN_SCOPE pasan directos; el resto consultan el scope registrado del método y validan con authorizeOperatorScopesForMethod o authorizeOperatorScopesForRequiredScope si los scopes del cliente cubren el requerido.

Tras la autorización, si la operación es de escritura del plano de control (isControlPlaneWrite), primero pasa por el rate limit (control-plane rate limit:669), con cuota por defecto «3 por 60 segundos»; si se excede, retorna UNAVAILABLE + retryAfterMs. El rate limit se coloca antes del lookup del handler, para que los métodos de escritura registrados por plugins y auxiliares pasen por el mismo filtro.

Por último, el handler se ejecuta dentro del ámbito de petición del plugin (withPluginRuntimeGatewayRequestScope:706): withPluginRuntimeGatewayRequestScope({ context, client, isWebchatConnect }, invokeHandler) envuelve handler({ req, params, client, isWebchatConnect, respond, context }). El propósito del ámbito es que, cuando el handler spawnee un subagent y este a su vez invoque un método del gateway (por ejemplo, chat.send al ejecutar una herramienta), las llamadas anidadas hereden la identidad del llamador (caller identity), y los métodos RPC registrados por plugins también puedan acceder al contexto del cliente actual. Sin este ámbito, las llamadas anidadas perderían la identidad y el flag isWebchatConnect, lo que llevaría a decisiones de autorización erróneas aguas abajo.

Límites y fallos

  • Descriptor drift lanza siempre: cuando el módulo de familia se carga pero no encuentra el método declarado, se throw new Error directamente. Un error de configuración no se traga silenciosamente — de lo contrario, el cliente vería unknown method pero el log del servidor no mostraría nada.
  • Alcance de métodos en startup: en el arranque temprano la tabla de métodos ya se anuncia, pero los módulos de handler pueden no haber terminado de cargar. handleGatewayRequest comprueba context.unavailableGatewayMethods y, si acierta, retorna UNAVAILABLE + retryable: true + retryAfterMs: GATEWAY_STARTUP_RETRY_AFTER_MS, para que el cliente haga backoff según el protocolo (startup unavailable:655).
  • #94127 alcance de handlers hot-registrados: la lógica de selección de methodRegistry — si el snapshot tiene el método, se usa el snapshot; si no, se reconstruye desde el registry live de plugins. Si se rompe, los métodos de plugin registrados tras el arranque quedarían invisibles.
  • Prioridad de handlers de plugin: en createRequestGatewayMethodRegistry, los handlers de plugin siempre ganan a los handlers extra (if (!pluginMethodNames.has(method))), para evitar que los handlers extra del harness sombreaden métodos de plugins ya cargados — los plugins los instala explícitamente el usuario, su prioridad es mayor que la de un handler local del harness.
  • Posición del rate limit del plano de control: el rate limit se aplica antes del lookup del handler, no después. Si se pusiera dentro del handler, los métodos de escritura de plugins y auxiliares eludirían el filtro.
  • Fallo del ámbito: un error lanzado por withPluginRuntimeGatewayRequestScope falla la petición, pero el rollback del estado ya ejecutado por el handler es responsabilidad del propio handler — el ámbito no ofrece transaccionalidad.

Resumen

La tabla de métodos es declaración primero: coreGatewayHandlers es una tabla que dice qué métodos hay, agrupados por familia; carga diferida: createLazyCoreHandlers envuelve el dynamic import en un wrapper con cache; fusión por petición: createRequestGatewayMethodRegistry combina las tres capas (núcleo, plugins, extra), con prioridad de plugins. La autorización (authorization) va en dos capas, role + scope; las escrituras del plano de control pasan además por rate limit; el handler se ejecuta dentro del ámbito de petición del plugin para soportar llamadas anidadas.

Cómo llegan las peticiones a esta capa y cómo se inyectan las primitivas de broadcast se ve en Núcleo del gateway; cómo se entregan los eventos del agent al cliente tras ejecutar el handler se ve en broadcast de chat y entrega de eventos; cómo el handler conduce el bucle principal del agent se ve en Runner embebido.

Referencias oficiales: Documentación del gateway · README.