メインコンテンツへスキップ

ChatGPT RemoteはCodex専用じゃない。Pi Coding Agentを繋いでみた

ChatGPT Remoteが見ているのはCodexではなく、app-serverのJSON-RPCだけでした。Pi Coding Agentにアダプタを被せて、iOSのChatGPTアプリから直接動かせるようにするまでの実装記録です。

ChatGPT Remoteは便利です。iPhoneから自宅のマシンに繋いで、電車の中でコードを書かせられる。あの体験だけは手放したくない、という人は多いと思います。

でも接続先はCodexだけです。Codexは重いし、モデルの選択肢も限られる。普段はPi Coding AgentやOpenCodeを使っているのに、RemoteのためだけにCodexを立ち上げるのは本末転倒です。

調べてみると、ChatGPT Remoteが見ているのはCodexというアプリケーションではなく、Codex app-serverのJSON-RPCプロトコルだけでした。そのプロトコルさえ喋れれば、向こう側にいるのは何でもいい。

そこで pi-codex-app-server というPi Coding Agent向けの拡張機能を作り、ChatGPTのiOSアプリからPiを直接動かせるようにしました。試すだけなら「使い方」まで飛ばしてください。

Remoteが見ているのはCodexではなくJSON-RPCだった

ChatGPT Remoteの仕組みを追って最初に分かったのは、モバイルアプリがローカルのapp-serverへ直接繋ぎに来るわけではない、ということでした。

流れはこうなっています。

  1. ホスト(自分のマシン)が chatgpt.com/backend-api/wham/remote/control/... に「接続済みホスト」として登録する
  2. QRコードまたは手動コードで、スマホと一対一にペアリングする
  3. 以降、OpenAIのリレーを経由してapp-serverのJSON-RPCがそのまま運ばれる

Remote Control protocol version 3、期限付きサーバートークン、シーケンスIDによる配送管理、未確認メッセージの再送、大きなメッセージの分割と再構築。仕様としてはこのあたりが乗っていますが、運ばれている中身はCodex app-serverのJSON-RPCそのものです。

つまり、リレーの向こう側にいるプロセスがCodexである必要はありません。ホスト登録とペアリングを済ませ、Codexと同じJSON-RPCを喋る何かがそこにいればいい。

Codexを作り直したわけではない

pi-codex-app-server は、Pi Coding AgentをCodex app-serverとして公開するアダプタです。

境界をはっきりさせておくと、こうなります。

誰の仕事か

何を担当するか

Pi Coding Agent

モデル選択、ツール実行、権限、セッション履歴

pi-codex-app-server

Codex JSON-RPCの受け答え、Remote Control登録、Piへの変換

SQLiteサイドカー

プロジェクト一覧、アーカイブ状態など、Piに概念がないメタデータのみ

Codexのサンドボックスや承認ポリシーは再現していません。再現できない安全保証を実装したふりで返さない、というのはこの設計で最初に決めた方針です。エージェントの挙動はPiのものであって、Codexのものではない。ここを曖昧にすると、ユーザーが持っていないはずの保証を信じてしまいます。

環境は以下です。

  • Node.js 22.5以降(node:sqlite を使うため。Node 24なら実験的警告も消えます)
  • Bun 1.2以降(依存インストールとビルド)
  • Pi Coding Agent(ChatGPT OAuthでログイン済みであること)

トランスポートはポータブルなので、OSは選びません。動作確認がWindows x64に寄っているのは、単に私の開発環境がそうだからです。

対象にした公式プロトコルは、Codex 0.149.0 時点のapp-server protocol v2です。

公式実装を読まずに書かせない

実装はCodex(GPT-5)に全部書かせる、いわゆるバイブコーディングで進めました。ただし放し飼いにはせず、いくつかルールを最初に固定しています。

ひとつめが、自分の知識で実装せず、必ず公式実装をクローンして参照してから書くこと。openai/codex は公開されているので、これを一次情報にできます。

これは効きました。プロトコルのスキーマは、公式リポジトリの generate-ts / generate-json-schema を実際にビルドして生成したものをvendorしています。手書きの型定義や、記憶から書いたスキーマは一つもありません。

途中で分かったのですが、公式が普通に生成するスキーマはstable版のみでした。ところがiOSアプリは initialize.capabilities.experimentalApi でopt-inして、collaborationMode/list のようなexperimental RPCを実際に送ってきます。--experimental 付きで生成し直して、ようやく全メソッドが揃いました。推測では絶対に辿り着けなかった部分です。

最終的に、client→server 155メソッド、server→client 11メソッドを型マップ化しています。

リントを緩めさせない

途中、unknown の扱いでリントに引っかかったエージェントが、ルールを無効化して回避しようとした場面がありました。ここは止めています。

oxlintは無効化しないでください。できる限り厳しいリンティングの中で実装することでAI Slopを拒否します。ルールを無効化してしまうと、リントの意味がありません。

使っているのはUltracite(oxlint + oxfmt)で、独自の anti-slop ルールが入っています。終了コードが0でも警告を出してくるので、警告ゼロを完了条件にしました。

この制約は想像以上に設計を良い方向へ押しました。実際に拒否された例をいくつか挙げます。

  • unknown を判定するヘルパー関数を自作 → 境界検証の先送りと判定され、受信箇所で直接Zodパースする形に修正
  • ws のコールバックを手製Promiseで包む → 拒否され、公式のNode Duplexアダプタへ切り替え
  • await 式に直接 .map() → 中間変数へ分割
  • テスト1つに検証を詰め込みすぎ → 「保存と一覧」「アーカイブと削除」「leaseのfencing」などへ分割

どれも動くけれど雑なコードです。リントを緩めていたら全部残っていました。

ロガーもバリデータも自作させない

汎用バリデータやロガーを自作し始めたら止める、というのも明示的に指示した点です。

自分で全て実装するのではなく、メンテナンス性を考慮したり、意図しない動作を起こさないために積極的にパッケージを導入してください。

結果として採用した構成が以下です。

用途

採用

理由

JSON-RPC

json-rpc-2.0

メソッド表から引数・戻り値を相関させる TypedJSONRPCServerAndClient があり、双方向RPCとトランスポート分離に対応

ワイヤー検証

Ajv + 公式JSON Schema

スキーマを二重管理しない。公式が正本

設定・DB行・JWT

Zod

公式スキーマが存在しない外部境界だけに限定

ロギング

LogTape

構造化ログ。自作loggerを削除

補助処理

es-toolkit

標準APIで複雑になる箇所のみ

ORM

Drizzle(v1 RC)

後述

CLI

Commander

サブコマンドの手書きパースを排除

QR生成

qrcode

自作しない

JSON-RPCを全部Zodで書こうとしたので止めた

最初、エージェントはJSON-RPCのトランスポート、リクエストとレスポンスの相関、ランタイム検証を全部Zodで自作する計画を立てていました。

ここで「JSON-RPCを型安全に使う方法をよく調べてから実装してほしい。Zodより最適な方法があるかもしれない」と差し戻しています。

調べ直した結果、方針が変わりました。

  • OpenRPC系はクライアント中心で、今回必要な「同一接続上でのサーバー要求・クライアント応答」に弱い
  • vscode-jsonrpc はキャンセル機構が強いものの、Codex app-serverの中断は $/cancelRequest ではなく turn/interrupt というプロトコルレベルのリクエストとして定義されている。JSONLへのアダプタと型記述の負担が増える
  • json-rpc-2.0 は型付きメソッドマップを持ち、トランスポート非依存。Codex公式の生成型と組み合わせやすい

最終的に、json-rpc-2.0 で相関と双方向通信、公式JSON SchemaをAjvでコンパイルして実行時検証、Zodは公式スキーマがない境界だけ、という三層に落ち着きました。自作パーサーとpending管理は全部消えています。

ワイヤーを読むときに知っておくと良い点がひとつ。Codexは "jsonrpc":"2.0" メンバーを省略します。なのでライブラリの内部にいる間だけ付与し、送信前に剥がしています。

SQLiteにDrizzle v1 RCを入れた

メタデータ用のSQLiteは、当初 node:sqlite を直接叩いていました。動いてはいたのですが、カラム名と型がSQL、Zod、変換関数の三箇所に重複していたので、軽量ORMの導入を検討しました。

論点になったのはDrizzleのバージョンです。v1はまだRC、v0.xはこれから置き換わる。

今回は誰もまだ使っていないプロジェクトで、下位互換を気にする必要がありません。それなら短命なv0.xに合わせる理由がない、ということでv1 RCを採用しました。あわせて、今のコードをDrizzleに置き換えるのではなく、SQL周りを一度削除してドキュメント通りの書き方から作り直しています。既存コードをベースにすると、前の設計の癖が残るためです。

「デスクトップアプリを更新してください」から進まない

ペアリングは成功するのに、この表示から先に進めない。原因はバージョンの名乗り方でした。

最初は独自の pi-codex-app-server/0.149.0 を送っていたのですが、OpenAI側はUser-Agentをちゃんとパースしていました。公式の形式に完全に揃えたところ、CLIとして正しく認識されます。

codex_cli_rs/0.149.0 (Windows 10.0.26200; x86_64) dumb

OS名、OSリリース、アーキテクチャ名、terminal tokenまで公式と同じ規則で動的生成します。Remote HTTPヘッダーと initialize.userAgent は同じ生成関数を通しているので、今後ずれることはありません。

「タスク設定を読み込めませんでした」

次に出たのがこれです。ログを見ると、iOSアプリは設定画面を開くときに permissionProfile/listconfig/readcollaborationMode/listthreadSection/list などをまとめて投げていて、未実装のものが -32601 Method not found を返していました。

ここで方針を変えています。当初は観測された画面に必要なメソッドだけ実装するつもりでしたが、それだと画面を開くたびに未知のメソッドが増えます。最新の openai/codex が公開する全JSON-RPCを互換対象にする、へ広げました。

ただし、Pi Coding Agentに機能は追加しません。分類はこうです。

  • Piに同等機能がある → Piへ変換
  • アダプタ側のメタデータで再現できる → 実装
  • Piに機能がない → 公式スキーマを満たす中立値を返す

これで Method not found は消え、ログの結果がすべて outcome: result に変わりました。設定画面も問題なく開きます。

QRコードが「Invalid array length」で落ちる

/codex-server pair でQRを出そうとしたらエラー。qrcode パッケージのUTF-8レンダラーは内部で Array((margin / 2) + 1) を作るため、margin: 1 だと配列長が 1.5 になってNodeが落ちます。marginを2にして解決しました。

既存のテストがモックを使っていて実際のレンダラーを通っていなかったので、QR描画を小さな関数に切り出し、実ライブラリを通す回帰テストを追加しています。

使い方

Piパッケージとしてインストールします。

pi install npm:pi-codex-app-server

Piを起動すると拡張機能がデーモンを自動起動し、フッターに状態が出ます。

Codex server: running · ws://127.0.0.1:53412

ペアリングはPiの中から。

/codex-server pair

QRコードが出るのでChatGPTアプリで読み取るか、下に表示される手動コードを入力します。

ローカルのCodexクライアントから使う場合は、stdioで直接繋げます。

pi-codex-app-server app-server

/codex-serverstart / stop / status / pair に対応していて、サブコマンドの補完も効きます。

つまずきやすい点を先に書いておきます。

  • ペアリングにはPiの openai-codex OAuthログインが必須です。APIキーでは通りません。登録時にトークンのクレームからアカウントIDを読むためです
  • ポートが既に埋まっているとデーモンが起動しません。/codex-server status で確認できます
  • Codex固有のサンドボックス設定や承認ポリシーは受理しますが無視されます。Piの実行セマンティクスで動くので、Codexのサンドボックスに依存しているならドロップイン置換にはなりません
  • 1つのPiセッションに書き込めるのは常に1プロセスだけで、今はデーモンがその役割です。Pi TUIとリモートから同時に同じセッションを操作することは想定していません

OpenCodeでも同じことができるはず

必要だったのは次の3つだけでした。

  1. Codex app-serverのJSON-RPCを喋る
  2. Remote Controlのホスト登録とペアリングを通す
  3. 公式と同じ形式でUser-Agentを名乗る

エージェント本体には一切手を入れていません。OpenCodeでも他のエージェントでも、同じアダプタを被せればChatGPT Remoteから認識させられるはずです。まだ検証していないので仮説ですが、構造上そうなるだろうと思っています。

Codex専用に見えるものが、実はプロトコル互換の問題でしかなかった。これが今回いちばん強く感じたことでした。UIやブランドが特定の実装に縛られているように見えても、境界にあるのが公開されたプロトコルなら、そこは差し替えられます。

なお、この実装はOpenAIとの提携によるものではなく、公開されているプロトコルを参照した互換実装です。利用にあたっては各サービスの規約を各自で確認してください。

リポジトリはこちらです。IssueやPRは歓迎します。

Lqm1/pi-codex-app-server