Skip to content

Table de méthodes RPC et répartition des requêtes

源码版本v2026.6.11

Responsabilités

server-methods.ts est le hub RPC de la passerelle (gateway): il répartit les requêtes JSON-RPC entrantes vers les handlers selon le nom de méthode, et gère l'autorisation (authorization), le rate limiting et l'isolation du scope de requête. Les handlers qui font vraiment le travail sont éclatés dans server-methods/ par famille — chat, agents, cron, channels, device, artifacts, connect, etc.

Cette couche fait quatre choses: agréger les méthodes centrales en une table (coreGatewayHandlers), fusionner à chaque requête un registry temporaire, autoriser puis passer au handler, et enfin exécuter le handler dans le scope de requête du runtime de plugin (plugin runtime request scope), pour que les subagents spawnés par le handler puissent rappeler les méthodes de la passerelle.

Motivation de conception

Pourquoi ne pas écrire un gros switch(method)? Parce que la passerelle doit supporter l'enregistrement à chaud (hot register) de méthodes par les plugins, le override par les tests, et l'advertise de la table de méthodes très tôt au démarrage (alors que de nombreux modules de handler ne sont pas encore chargés). Ces trois contraintes imposent une table déclarative d'abord, chargée tardivement, fusionnée à chaque requête.

Déclarative d'abord signifie que coreGatewayHandlers est juste un mapping {méthode: handler}, où le handler est enveloppé par createLazyCoreHandlers qui dynamic import — au premier appel de la méthode le module est chargé, les appels suivants réutilisent la même import promise (lazyHandlerModule fait handlersPromise ??= pour cacher). Fusionnée à chaque requête signifie que createRequestGatewayMethodRegistry, quand une requête arrive, prend l'état global des plugins pour récupérer les plugin handlers actifs et les fusionne avec la table centrale et les extra handlers de l'appelant — ainsi les méthodes enregistrées à chaud par les plugins sont visibles immédiatement, sans redémarrage.

Fichiers clés

Flux de données

La table centrale de méthodes est un gros objet litéral (coreGatewayHandlers:269), groupé par famille, chaque famille enveloppée par 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...
};

Les noms de méthodes sont hiérarchiques par points: chat.* sessions de chat, cron.* tâches planifiées, channels.* démarrage/arrêt de canaux, device.pair.* pairing d'appareil. wake est une méthode top-level singulière, hors préfixe de famille.

createLazyCoreHandlers (createLazyCoreHandlers:44) génère un wrapper par nom de méthode qui ne déclenche loadHandlers() qu'au premier appel, mis en cache dans la closure. Le point critique: descriptor drift lève toujours une erreur:

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);
}

Déclarer un nom de méthode sans que le module de famille chargé ne contienne le handler correspondant est une erreur de configuration qui doit jeter, pas un unknown method silencieux.

Quand une requête arrive, handleGatewayRequest (handleGatewayRequest:638) décide d'abord quel registry utiliser, puis autorise, puis exécute le 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;
}

Ce bloc fix de #94127: un snapshot des méthodes est pris au démarrage, mais les méthodes hot-enregistrées ensuite n'y sont pas. La solution: si le snapshot ne possède pas la méthode, on repli sur createRequestGatewayMethodRegistry qui reconstruit depuis le live plugin state — reconstruction bon marché, n'arrive que sur miss du snapshot.

L'autorisation suit deux couches: d'abord le role (operator / node / admin); le role node et les connexions avec ADMIN_SCOPE passent direct; les autres roles font vérifier le scope enregistré sur la méthode, via authorizeOperatorScopesForMethod ou authorizeOperatorScopesForRequiredScope qui contrôle que les scopes du client couvrent ceux requis.

Après l'autorisation, si c'est une écriture de contrôle (isControlPlaneWrite), on passe d'abord le rate limiter (control-plane rate limit:669), quota par défaut « 3 par 60 s »; au-delà on retourne UNAVAILABLE + retryAfterMs. Le rate limiter est placé avant le handler lookup pour que les méthodes enregistrées par plugin et aux passent par la même porte.

Enfin le handler s'exécute dans le scope de requête du plugin (withPluginRuntimeGatewayRequestScope:706): withPluginRuntimeGatewayRequestScope({ context, client, isWebchatConnect }, invokeHandler) enveloppe handler({ req, params, client, isWebchatConnect, respond, context }). L'objectif du scope est de permettre au handler de spawner un subagent, qui rappelle une méthode de la passerelle (par exemple un outil qui fait chat.send) — l'appel imbriqué hérite de l'identité de l'appelant (caller identity), et les méthodes RPC enregistrées par le plugin peuvent accéder au contexte client courant. Sans scope, les appels imbriqués perdraient l'identité et le flag isWebchatConnect, entraînant de mauvaises décisions d'autorisation en aval.

Limites et modes d'échec

  • Descriptor drift lève toujours: si le module de famille chargé ne contient pas la méthode déclarée, throw new Error. Une erreur de configuration ne doit pas être avalée silencieusement, sinon le client reçoit unknown method mais le log serveur n'a aucune trace.
  • Accessibilité des méthodes au démarrage: la table est advertised tôt, mais les modules de handler ne sont pas forcément chargés. handleGatewayRequest consulte context.unavailableGatewayMethods; si hit, retour UNAVAILABLE + retryable: true + retryAfterMs: GATEWAY_STARTUP_RETRY_AFTER_MS pour laisser le client backoff selon le protocole (startup unavailable:655).
  • Accessibilité des handlers hot-enregistrés #94127: la logique de choix du methodRegistry — snapshot si la méthode est présente, sinon reconstruction depuis le live plugin registry. Une régression casserait l'accès aux méthodes de plugins enregistrées après le démarrage.
  • Priorité des plugin handlers: dans createRequestGatewayMethodRegistry, les plugin handlers gagnent toujours contre les extra handlers (if (!pluginMethodNames.has(method))), pour empêcher les extra handlers de l'appelant de shadow-er des méthodes de plugins déjà chargés — les plugins sont installés explicitement par l'utilisateur, leur priorité est supérieure à l'injection locale du harness.
  • Position du rate limit contrôle-plane: avant le handler lookup, pas après. À l'intérieur du handler, les écritures des plugins et aux passeraient au travers.
  • Échec du scope: une erreur de withPluginRuntimeGatewayRequestScope fait échouer la requête, mais le rollback de l'état déjà modifié par le handler est à la charge du handler; le scope n'offre pas de transactionnalité.

Résumé

La table de méthodes est déclarative d'abord: coreGatewayHandlers est une seule table qui récapitule les méthodes, groupées par famille; chargement tardif: createLazyCoreHandlers enveloppe le dynamic import dans un wrapper caché; fusion par requête: createRequestGatewayMethodRegistry fusionne core, plugins, extra, plugins gagnants. L'autorisation (authorization) suit deux couches role + scope, les écritures contrôle-plane passent un rate limiter supplémentaire, et le handler s'exécute dans le scope de requête du plugin pour supporter les callbacks imbriqués.

Comment les requêtes entrent dans cette couche et comment les primitives de diffusion sont injectées, voir Cœur de passerelle; comment les événements de l'agent sont livrés aux clients après l'exécution du handler, voir Diffusion du chat et livraison d'événements; comment le handler pilote la boucle principale de l'agent, voir Runner embarqué.

Pour comparer avec la documentation officielle: Gateway docs · README.