Skip to content

RPC メソッド表とリクエスト振り分け

源码版本v2026.6.11

責務

server-methods.ts はゲートウェイ (gateway) の RPC 中枢です:入ってきた JSON-RPC リクエストをメソッド名で handler に振り分け,認可 (authorization)、レート制限、リクエストスコープ隔離を担当します。実際に処理する handler は server-methods/ 配下でファミリ別に分割されたサブモジュール——chat、agents、cron、channels、device、artifacts、connect など——に散らばっています。

この層が行うのは 4 件事:コアメソッドを集めて表にする(coreGatewayHandlers)、リクエストごとに臨時で registry をマージ、認可後に handler に渡す、最後に handler をプラグインリクエストスコープ (plugin runtime request scope) 内で実行し,handler 内部で spawn した subagent がゲートウェイメソッドを呼び戻せるようにします。

設計動機

なぜ直接 switch(method) の大きな表を書かないのか?ゲートウェイはプラグイン (plugin) の hot 登録メソッドをサポートし,テストでコアメソッドをカバーし,さらに起動早期にメソッド表を advertise(この時点で多くの handler モジュールは未読み込み)する必要があるからです。3 つの制約が重なり,メソッド表は宣言優先、読み込み延後、リクエストごとマージでなければなりません。

宣言優先とは,coreGatewayHandlers が単なる {メソッド名: handler} マッピングであり,handler は createLazyCoreHandlers で包んで動的 import すること——初めてメソッドが呼ばれたときにモジュールを読み込み,以降の呼び出しは同じ import promise を再利用(lazyHandlerModulehandlersPromise ??= キャッシュ)し,並発で繰り返し読み込まないようにします。リクエストごとマージとは,createRequestGatewayMethodRegistry がリクエスト到達時にグローバルプラグイン状態から現在アクティブな plugin handlers を取り,コア表、呼び出し側 extra handlers とマージします——これでプラグインが hot 登録したメソッドが実行中のリクエストに即時可见となり,再起動不要です。

主要ファイル

データフロー

コアメソッド表は巨大なオブジェクトリテラルで(coreGatewayHandlers:269) ファミリごとに集まり,各ファミリは 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...
};

メソッド命名がドット階層になっているのが分かります:chat.* はチャットセッション,cron.* は定時タスク,channels.* はチャネルの起動停止,device.pair.* はデバイスペアリング。wake は単数トップレベルの唤醒メソッドで,ファミリプレフィックスの下にありません。

createLazyCoreHandlers(createLazyCoreHandlers:44)は各メソッド名に wrapper を生成し,初回呼び出し時だけ loadHandlers() をトリガーし,クロージャにキャッシュします。重要なのは descriptor drift は必ずエラーをスローすることです:

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

メソッド名を宣言したのにロードされたファミリモジュールに対応 handler がなければ,設定ミスであり,必ずエラーを投げなければならず,黙って unknown method を返してはいけません。

リクエスト到達時,handleGatewayRequest(handleGatewayRequest:638)はまずどの registry を使うかを決定し,次に認可,最後に 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;
}

この部分は #94127 を修正:起動時にメソッドスナップショットを撮りましたが,その後プラグインが hot 登録した新メソッドはスナップショットにありません。解法は,まずスナップショットが該当メソッドを持つかを見て,持たなければ createRequestGatewayMethodRegistry で live plugin state から再構築します——再構築は安価で,スナップショット miss 時にだけ発生します。

認可は 2 層で走ります:まず role(operator / node / admin)を見て,node ロールと ADMIN_SCOPE を持つ接続は直接通します。他のロールはさらにメソッド登録の scope を調べ,authorizeOperatorScopesForMethod または authorizeOperatorScopesForRequiredScope でクライアントの scopes がカバーしているかを検証します。

認可通過後,もしコントロールプレーン書き操作 (isControlPlaneWrite) なら,まずレート制限(control-plane rate limit:669)を通します。デフォルトクォータは「60 秒につき 3 回」,超過すると UNAVAILABLE + retryAfterMs を返します。レート制限は handler lookup の前に置き,プラグインや aux が登録した書き込みメソッドも同じ関門を通すようにします。

最後に handler はプラグインリクエストスコープ内で実行されます(withPluginRuntimeGatewayRequestScope:706):withPluginRuntimeGatewayRequestScope({ context, client, isWebchatConnect }, invokeHandler)handler({ req, params, client, isWebchatConnect, respond, context }) を包みます。スコープの役割は,handler 内部で spawn した subagent がゲートウェイメソッドを呼び戻すとき(例:tool 実行時に chat.send を呼び戻す),ネスト呼び出しが呼び出し側の身元 (caller identity) を引き継ぎ,プラグイン登録の RPC メソッドも現在の client コンテキストを取得できるようにすることです。スコープがないと,ネスト呼び出しで身元が失われ,isWebchatConnect マークも失われ,下流の認可判定が間違います。

境界と失敗

  • descriptor drift 必ずスロー:ファミリモジュール読み込み完了後に宣言メソッドが見つからないとき,直接 throw new Error。設定ミスを黙って飲み込んではいけません,さもなくばクライアントは unknown method を受け取るのにサーバ側ログに異常がありません。
  • startup 段階のメソッド到達性:起動早期にメソッド表は advertise されますが,handler モジュールは読み込み完了しているとは限りません。handleGatewayRequestcontext.unavailableGatewayMethods をチェックし,ヒットすれば UNAVAILABLE + retryable: true + retryAfterMs: GATEWAY_STARTUP_RETRY_AFTER_MS を返し,クライアントがプロトコルに従って退避します(startup unavailable:655)。
  • #94127 hot 登録 handler 到達性:methodRegistry 選択ロジック——スナップショットがメソッドを持てばスナップショット,さもなくば live plugin registry から再構築。壊すと起動後に登録されたプラグインメソッドがすべて見えなくなります。
  • プラグイン handler 優先順位:createRequestGatewayMethodRegistry でプラグイン handler は常に extra handler より優先され(if (!pluginMethodNames.has(method))) 呼び出し側の extra handler が読み込み済みプラグインメソッドを shadow 化するのを防ぎます——プラグインはユーザが明示的にインストールしたもので,優先度は harness-local 注入より高いです。
  • コントロールプレーンレート制限の位置:レート制限は handler lookup の前,後ではありません。handler 内部に置くとプラグインや aux の書き込みメソッドが関門をバイパスします。
  • スコープ失敗:withPluginRuntimeGatewayRequestScope がエラーをスローするとリクエストは fail しますが,handler が半分実行された状態のロールバックは handler 自身が担当し,スコープはトランザクション性を提供しません。

まとめ

メソッド表は宣言優先:coreGatewayHandlers 1 枚の表でどのメソッドがあるか、ファミリ別にクラスタ化。読み込みは延後:createLazyCoreHandlers が動的 import をキャッシュ wrapper に包む。リクエストごとマージ:createRequestGatewayMethodRegistry がコア、プラグイン、extra の 3 層を一つにまとめ,プラグイン優先。認可 (authorization) は role + scope の 2 層で,コントロールプレーン書き操作は追加でレート制限を通り,handler はプラグインリクエストスコープ内で実行されネスト呼び戻しをサポートします。

リクエストがどうこの層に入るか,放送プリミティブがどう注入されるかは ゲートウェイコア,handler 実行後の agent イベントがどうクライアントに配送されるかは チャット放送とイベント配送,handler 内部でどう agent メインループを駆動するかは Embedded Runner を参照。

公式資料:Gateway ドキュメント · README