Skip to content

RPC-Methodentabelle und Anfrageverteilung

源码版本v2026.6.11

Verantwortung

server-methods.ts ist das RPC-Zentrum (RPC hub) des Gateways (gateway): Es verteilt eingehende JSON-RPC-Anfragen nach Methodennamen an Handler und übernimmt Autorisierung (authorization), Ratenbegrenzung (rate limiting) und Anfrage-Scope-Isolation. Die tatsächliche Arbeit machen Handler, die über server-methods/ nach Familien aufgeteilt sind — chat, agents, cron, channels, device, artifacts, connect.

Diese Schicht macht vier Dinge: Kernmethoden in einer Tabelle zusammenführen (coreGatewayHandlers), pro Anfrage eine Registry zusammenbauen, nach Autorisierung an den Handler übergeben und den Handler schließlich im Plugin-Request-Scope (plugin runtime request scope) laufen lassen, sodass ein darin gespawnter Subagent noch Gateway-Methoden aufrufen kann.

Designmotivation

Warum nicht einfach ein großes switch(method)? Weil das Gateway heiße Plugin-Methoden-Registrierung unterstützen muss, Test-Overriding von Kernmethoden und eine frühe Methodenwerbung (advertise) beim Start (viele Handler-Module sind dann noch nicht geladen). Diese drei Einschränkungen zusammen verlangen: deklarativ voranstellen, laden verzögern, pro Anfrage zusammenführen.

Deklarativ voranstellen heißt: coreGatewayHandlers ist nur eine {Methodenname: Handler}-Map; Handler werden mit createLazyCoreHandlers um einen dynamischen Import gewrappt — erst beim ersten Methodenaufruf wird das Modul geladen, spätere Aufrufe nutzen dasselbe import promise (in lazyHandlerModule über handlersPromise ??= gecacht), um mehrfaches Laden bei parallelen Aufrufen zu vermeiden. Pro Anfrage zusammenführen heißt: createRequestGatewayMethodRegistry holt bei Anfrage from der globalen Plugin-State die aktuell aktiven Plugin-Handler und führt sie mit der Kern-Tabelle und den Extra-Handlern des Aufrufers zusammen — so sind heiß registrierte Plugin-Methoden sofort sichtbar, ohne Neustart.

Schlüsseldateien

Datenfluss

Die Kernmethoden-Tabelle ist ein gewaltiges Objekt-Literal (coreGatewayHandlers:269), nach Familien gruppiert und jeweils mit createLazyCoreHandlers umwickelt:

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

Man sieht: Methodennamen sind hierarchisch punktiert: chat.* für Chat-Sitzungen, cron.* für geplante Aufgaben, channels.* für Kanal-Start/Stopp, device.pair.* für Geräte-Pairing. wake ist eine einzelne Top-Level-Wachmethode ohne Familien-Präfix.

createLazyCoreHandlers (createLazyCoreHandlers:44) erzeugt pro Methodennamen einen Wrapper, der loadHandlers() nur beim ersten Aufruf triggert und im Closure cacht. Entscheidend: Descriptor Drift wirft sofort:

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

Wird ein Methodenname deklariert, aber das geladene Familienmodul hat keinen passenden Handler, ist das ein Konfigurationsversatz und muss sofort werfen, nicht stillschweigend „unknown method" zurückgeben.

Bei Anfrage entscheidet handleGatewayRequest (handleGatewayRequest:638) zuerst, welche Registry verwendet wird, dann autorisieren, dann den Handler ausführen:

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

Das behebt #94127: Beim Start wurde ein Methoden-Snapshot gemacht, aber danach heiß registrierte Plugin-Methoden fehlen im Snapshot. Die Lösung: Zuerst prüfen, ob der Snapshot die Methode hält; wenn nicht, fällt createRequestGatewayMethodRegistry zurück und baut aus dem Live-Plugin-State neu auf — der Neuaufbau ist billig und passiert nur bei Snapshot-Miss.

Autorisierung läuft in zwei Schichten: zuerst die Role (operator / node / admin); node und Verbindungen mit ADMIN_SCOPE werden direkt durchgewunken; bei anderen Rollen wird die von der Methode registrierte Scope über authorizeOperatorScopesForMethod bzw. authorizeOperatorScopesForRequiredScope geprüft.

Nach bestandener Autorisierung wird bei Control-Plane-Schreiboperation (isControlPlaneWrite) zuerst die Ratenbegrenzung (control-plane rate limit:669) geprüft — Standardkontingent „3 Mal pro 60 Sekunden", bei Überschreitung UNAVAILABLE + retryAfterMs. Die Ratenbegrenzung liegt vor dem Handler-Lookup, damit Plugin- und aux-registrierte Schreibmethoden dieselbe Hürde durchlaufen.

Schließlich läuft der Handler im Plugin-Request-Scope (withPluginRuntimeGatewayRequestScope:706): withPluginRuntimeGatewayRequestScope({ context, client, isWebchatConnect }, invokeHandler) umschließt handler({ req, params, client, isWebchatConnect, respond, context }). Der Scope sorgt dafür, dass verschachtelte Aufrufe — wenn der Handler einen Subagent spawn und dieser eine Gateway-Methode aufruft (z. B. chat.send während Tool-Ausführung) — die Aufrufer-Identität (caller identity) erben und auch die RPC-Methoden des Plugins den Client-Kontext sehen. Ohne Scope würden verschachtelte Aufrufe Identität und isWebchatConnect-Markierung verlieren, was zu fehlerhaften Autorisierungsentscheidungen downstream führt.

Grenzen und Fehler

  • Descriptor Drift wirft: Wenn das Familienmodul geladen ist, aber eine deklarierte Methode nicht findet, wirft es sofort throw new Error. Konfigurationsversatz darf nicht stillschweigend verschluckt werden, sonst sieht der Client unknown method, ohne dass der Server-Log etwas anzeigt.
  • Startup-Methoden-Erreichbarkeit: Beim frühen Start ist die Methodentabelle bereits advertised, aber Handler-Module sind eventuell noch nicht vollständig geladen. handleGatewayRequest prüft context.unavailableGatewayMethods und liefert bei Treffer UNAVAILABLE + retryable: true + retryAfterMs: GATEWAY_STARTUP_RETRY_AFTER_MS, damit der Client nach Protokoll backoffed (startup unavailable:655).
  • #94127 Heiß registrierte Handler-Erreichbarkeit: methodRegistry-Auswahllogik — hält der Snapshot die Methode, verwende ihn, sonst Neuaufbau aus Live-Plugin-Registry. Eine Verschlimmerung würde heiß registrierte Plugin-Methoden nach Start unsichtbar machen.
  • Plugin-Handler-Priorität: In createRequestGatewayMethodRegistry gewinnt der Plugin-Handler immer über den Extra-Handler (if (!pluginMethodNames.has(method))), damit Aufrufer-Extra-Handler bereits geladene Plugin-Methoden nicht overschatten — Plugins sind vom Nutzer explizit installiert und haben höhere Priorität als harness-lokale Injektionen.
  • Control-Plane-Ratenbegrenzung-Position: Ratenbegrenzung liegt vor dem Handler-Lookup, nicht danach. Innerhalb des Handlers würden Plugin- und aux-Schreibmethoden die Hürde umgehen.
  • Scope-Fehlschlag: Ein withPluginRuntimeGatewayRequestScope-Fehler lässt die Anfrage fehlschlagen; wie der halbausgeführte Handler-Zustand zurückgerollt wird, ist Sache des Handlers, der Scope bietet keine Transaktionalität.

Zusammenfassung

Methodentabelle deklarativ voranstellen: coreGatewayHandlers nennt in einer Tabelle alle Methoden und gruppiert nach Familien; laden verzögern: createLazyCoreHandlers wickelt dynamische Imports als gecachte Wrapper; pro Anfrage zusammenführen: createRequestGatewayMethodRegistry führt Kern, Plugin und Extra in einer Tabelle zusammen, Plugin zuerst. Autorisierung (authorization) läuft über Role + Scope in zwei Schichten, Control-Plane-Schreiboperationen bekommen zusätzliche Ratenbegrenzung, der Handler läuft im Plugin-Request-Scope und unterstützt verschachtelte Callbacks.

Wie Anfragen in diese Schicht gelangen und wie Broadcast-Primitive injiziert werden, siehe Gateway-Kern; wie Agent-Ereignisse nach Handler-Ausführung an Clients geliefert werden, siehe Chat-Broadcast und Ereignisauslieferung; wie der Handler intern die Agent-Hauptschleife antreibt, siehe Embedded Runner.

Vergleich mit offiziellen Ressourcen: Gateway-Doku · README.