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

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

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

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

ChatGPT Remoteは本当に便利です。iPhoneから自宅のマシンに繋ぎ、移動中や電車の中でもコーディングエージェントに指示を出してコードを書かせることができる。あの身軽な開発体験は、一度味わうとなかなか手放せません。

しかし、公式の接続先はCodex(OpenAI Codex CLI)に限定されています。普段はPi Coding AgentやOpenCodeを愛用している身からすると、「Remoteを使いたいがためだけにCodexを立ち上げなければならない」というのはいかにも不自由です。

そこで通信の仕組みを調べてみたところ、ChatGPT Remoteが見ているのはCodex本体ではなく、裏側で動いているapp-serverのJSON-RPCプロトコルだけであることがわかりました。つまり、プロトコルさえ正しく解釈・応答できれば、背後のエージェントはCodexである必要はありません。

この気づきから、Pi Coding AgentをCodex app-serverとして振る舞わせるアダプタ「pi-codex-app-server」を実装しました。これにより、ChatGPT公式モバイルアプリから自宅のPiを直接コントロールできるようになりました。

この記事では、ChatGPT Remoteのプロトコル解析から、完全互換アダプタを実装するまでに得た知見とハマりどころをまとめます。 (手っ取り早く動かしてみたい方は、後半の「使い方」をご覧ください)

ChatGPT Remoteの裏側:スマホはローカルに直接繋いでいない

ChatGPT Remoteの挙動を追ってまず判明したのは、「モバイルアプリが自宅PCのローカルサーバーへ直接通信しているわけではない」という点です。

実際の通信フローは以下のようになっています。

  1. ホスト登録: 自宅マシンが chatgpt.com/backend-api/wham/remote/control/... へ自身を「接続済みホスト」として登録する
  2. ペアリング: 画面に表示されるQRコード(またはペアリングコード)をスマホで読み取り、1対1の紐付けを確立する
  3. リレー通信: 以降はOpenAIのリレーサーバーを経由し、app-serverのJSON-RPCメッセージがWebSocket上でそのまま中継される

内部的には Remote Control protocol v3 によるセッション管理(有効期限付きトークン、シーケンスIDによる欠落防止、未達メッセージの再送、大きな電文のチャンク分割など)が走っていますが、やり取りされている中身そのものはCodex app-serverの標準的なJSON-RPCです。

したがって、リレーと通信し、適切なJSON-RPCを返せるプロセスを用意すれば、どんなエージェントでもChatGPT Remoteのホストとして成立します。

設計方針:Codexを作り直すのではなく「薄いアダプタ」に徹する

今回開発した pi-codex-app-server は、Codexの互換クローンではなく、Pi Coding AgentをCodex app-serverプロトコルへ適応させるトランスレータです。

各層の責務は明確に分離しています。

レイヤー

担当する役割

Pi Coding Agent

モデル選定、ツール実行、パーミッション管理、セッション履歴の保持

pi-codex-app-server

Codex JSON-RPCの送受信、Remote Control登録・ペアリング、Piプロトコルへの変換

SQLite(サイドカー)

プロジェクト一覧やスレッドのアーカイブ状態など、Codex固有かつPi側に概念のないメタデータの保持

ここで最も重視した設計方針が、「再現できないセキュリティ保証を見せかけで返さない」ということです。

Codexには独自のサンドボックス機構や承認ポリシーがありますが、本アダプタではそれらを無理にエミュレートしていません。実際にコードを実行するのはあくまでPiであり、パーミッションや安全性もPiの実行モデル(Pi execution semantics)に準拠します。下手に「Codexのサンドボックスが効いている」かのような見せかけのレスポンスを返してしまうと、ユーザーに存在しない安全性を誤認させるリスクがあるためです。

なお、動作環境は以下の通りです。

  • Node.js 22.5以降(メタデータ管理に組み込みの node:sqlite を利用。Node 24であれば実験的警告も出ません)
  • Bun 1.2以降(パッケージインストールとビルド)
  • Pi Coding Agent(ChatGPT OAuthでログイン済みであること)

実装戦略:AIコーディングを暴走させないためのルール

今回の実装はCodex(GPT-5)に書かせる、いわゆるバイブコーディングを活用しました。ただし、AIに丸投げするとすぐに設計が破綻するため、あらかじめ厳格なガードレールを敷いて進めました。

1. 公式実装(openai/codex)を唯一の一次情報にする

AIのあやふやな事前知識で型やスキーマを捏造させないよう、公式の openai/codex リポジトリをクローンし、必ずコードを直接参照させました。

プロトコルの型定義やスキーマは、公式リポジトリ内の generate-ts および generate-json-schema を実際にビルドして生成されたものをそのままプロジェクト内に配置(vendor)しています。手書きの型定義や記憶頼りのスキーマは一切含んでいません。

ここで一つ重要な発見がありました。公式スクリプトがデフォルトで出力するのはstable版スキーマのみですが、実際のiOSアプリは initialize.capabilities.experimentalApi を有効化し、collaborationMode/list などの実験的RPCを平然と送ってきます。これらは --experimental フラグを付与してスキーマを再生成して初めて判明した仕様でした。一次情報を直接ビルドしていなければ、まず辿り着けなかった落とし穴です。

結果として、クライアント→サーバー間 155メソッド、サーバー→クライアント間 11メソッドの完全な型マップを構築できました。

2. 厳格なリントで「AIスロップ」を根本から排除する

AIにコードを書かせると、型エラーやリント警告が出た際に as any でごまかしたり、ルール無効化コメント(// oxlint-disable など)を勝手に挿入して回避しようとする傾向があります。

これを防ぐため、リンター/フォーマッターにはUltracite(Oxlint + Oxfmt)を採用し、 anti-slop ルールを有効にしたうえで「警告ゼロ(Warning 0件)」をタスクの完了条件に設定しました。

この制約を課したことで、AI特有の雑なコードが自然と淘汰され、アーキテクチャの質が劇的に向上しました。

  • 型ガード関数を自前で量産しようとする → 境界でのバリデーション先送りと判定されたため、通信の受信直後にZodでパースする設計へ矯正
  • WebSocketのコールバックを手動のPromiseでラップする → 拒否され、公式のNode Duplexストリームアダプタを利用する堅牢な形へ改善
  • 肥大化したテストケース → 「作成と取得」「アーカイブと削除」「リース競合の制御」といった責務ごとの独立したテストへ分割

アーキテクチャとライブラリ選定

自前で車輪の再発明を行わず、堅牢性と保守性を担保するために適材適所でライブラリを組み合わせています。

用途

採用技術

選定理由

JSON-RPC制御

json-rpc-2.0

型付きメソッドマップによる型安全な双方向RPCに対応し、トランスポート非依存であるため

電文バリデーション

Ajv + 公式JSON Schema

公式の正本スキーマをそのままコンパイルして実行時検証するため(二重管理を防止)

外部境界の検証

Zod

公式スキーマの存在しない環境変数、設定値、SQLite行のバリデーションに限定

構造化ロギング

LogTape

監査・デバッグに耐える安全な構造化ログ(JWTやメールアドレスの自動マスキング対応)

ORM

Drizzle ORM(v1 RC)

node:sqlite と連携し、スキーマ定義・型定義の一元化を図るため

CLI制御

Commander

サブコマンド解析の標準化

QRコード描画

qrcode

端末ペアリング用コードのターミナル出力

JSON-RPCスタックの最適化

当初エージェントはJSON-RPCのディスパッチや検証をすべてZodで自作しようとしましたが、責務が過剰になるため差し戻しました。

  • vscode-jsonrpc はキャンセル通知が $/cancelRequest 前提であり、Codex独自の中断プロトコル(turn/interrupt リクエスト)と相性が悪い。
  • json-rpc-2.0 はトランスポートから独立しており、公式のTypeScript型マップと綺麗に結合できる。

最終的に、「通信の相関・ディスパッチは json-rpc-2.0」「通信電文の実行時検証はコンパイル済み Ajv」「DBや設定等の外部境界のみ Zod」というクリーンな3層構造に落ち着きました。

通信電文(Wire)を追う際のTip
Codex app-serverは、JSON-RPC 2.0仕様で必須とされている "jsonrpc": "2.0" プロパティをなぜか電文上で省略して通信してきます。そのため、アダプタ内部(ライブラリ通過時)でのみこのプロパティを補完し、外部へ送信する直前に取り除くパッチ処理を入れています。

メタデータ管理にDrizzle v1 RCを採用

当初は node:sqlite を直接クエリしていましたが、カラム定義や型変換がSQL文字列・Zod・変換ロジックの3箇所に分散していたためORMを導入しました。

新規プロジェクトで後方互換性の制約がなかったことから、近々メジャーとなる Drizzle v1 RC を採用。既存コードのラッパーにするのではなく、Drizzle公式のベストプラクティスに沿ってSQLアクセス層を一度ゼロから書き直したことで、非常に見通しの良いスキーマ定義になりました。

実装中に直面したハマりどころと解決策

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

ペアリング自体は正常に通るのに、ChatGPTアプリ側で「デスクトップアプリを更新してください」というエラーモーダルが出て接続が遮断される問題が発生しました。

原因はUser-Agentヘッダーの検証でした。当初は独自に pi-codex-app-server/0.149.0 と名乗っていたのですが、OpenAIのバックエンドはUA文字列を厳密にパースしていました。

そこで、公式Codex CLIと完全に同一のフォーマットで動的生成するように修正しました。

codex_cli_rs/0.149.0 (Windows 10.0.26200; x86_64) dumb

OS名、カーネルバージョン、CPUアーキテクチャ、および端末トークン(末尾の dumb)の規則を公式に一致させ、Remote HTTPヘッダーとプロトコルの initialize.userAgent で共通の生成ロジックを通すようにしたところ、無事に公式クライアントとして認識されました。

2. 「タスク設定を読み込めませんでした」エラー

接続後、スマホ側でプロジェクトの設定画面を開こうとすると「タスク設定を読み込めませんでした」というエラーが発生しました。

通信ログを確認すると、iOSアプリは画面遷移時に permissionProfile/list、config/read、collaborationMode/list、threadSection/list といった各種設定RPCを並行して一斉にリクエストしていました。これらに対し、未実装メソッドとして -32601 Method not found を返していたことが原因でした。

画面ごとに必要なRPCを都度調べるのはキリがありません。そこで、公式スキーマに定義されている全JSON-RPCメソッドを互換対象として登録する方針に切り替えました。

  • Pi側に同等の機能がある場合: Piの内部API呼び出しへ変換
  • アダプタ側で管理可能な場合: SQLiteのメタデータを使って応答(プロジェクト一覧など)
  • Piに対応する機能がない場合: エラーにせず、公式スキーマに適合する「空の配列」や「デフォルト値」などの中立なレスポンス(Neutral Result)を返却

この対応により、未実装エラーは一掃され、設定画面もスムーズに表示されるようになりました。

3. QRコード生成時の「Invalid array length」クラッシュ

TUI上で /codex-server pair を実行した際、Node.jsが RangeError: Invalid array length でクラッシュする現象に遭遇しました。

調査したところ、qrcode パッケージのUTF-8ターミナルレンダラー内部で Array((margin / 2) + 1) という配列確保が行われており、余白設定に margin: 1 を渡すと配列長が 1.5(非整数)になって例外が投げられるというライブラリ側の落とし穴でした。余白を margin: 2 に変更することで解決しました。

ユニットテストで描画処理をモック化していたために検知が遅れた反省から、実ライブラリを通して正しく描画される回帰テストを追加しています。

使い方

1. インストール

Piのエクステンション(パッケージ)としてインストールします。

pi install npm:pi-codex-app-server

Piを起動するとバックグラウンドデーモンが自動的に立ち上がり、フッターに状態が表示されます。

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

2. ChatGPTアプリとペアリングする

PiのTUI画面から以下のコマンドを実行します。

/codex-server pair

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

また、ローカルのCodex対応クライアント(CLIやIDE拡張機能など)から直接stdio経由で接続することも可能です。

npm install -g pi-codex-app-server
pi-codex-app-server app-server

/codex-server コマンドは start / stop / status / pair に対応しており、サブコマンドの補完も効きます。

3. 利用上の注意点

  • ChatGPT OAuthログインが必須: ペアリングにはPiが openai-codex でOAuth認証されている必要があります(登録時にトークンのクレームからアカウントIDを取得するため。APIキー認証では動作しません)。
  • ポートの重複: 指定ポートがすでに使用中の場合デーモンは起動しません。/codex-server status で状態を確認できます。
  • セッションの排他制御: 1つのPiセッションに対する書き込みは単一プロセスに限定されます。現在はデーモンが書き込み権限を持ち、Pi TUIとスマホからの同時書き込みによるJSONL破損を防いでいます。
  • サンドボックスの制限: 前述の通り、Codex特有のサンドボックス環境や承認プロファイルは無視され、Pi自体のパーミッション設定(Pi execution semantics)で動作します。

おわりに:プロトコルが開かれていれば、エコシステムは自由になる

今回のアダプタ開発で必要だった要素は、突き詰めると以下の3点だけでした。

  1. Codex app-serverのJSON-RPCプロトコルを忠実に実装すること
  2. Remote Controlのリレー登録とペアリング手順を通すこと
  3. 公式CLIと同じUser-Agent形式で名乗ること

エージェント本体であるPiのコードには一行も手を加えていません。背後にあるのがプロトコルである以上、理屈の上ではOpenCodeなど他のあらゆるコーディングエージェントでも、同様のアダプタを介してChatGPT Remoteのエコシステムに乗せられるはずです。

一見すると特定の公式デスクトップアプリに強く縛られているように見える機能であっても、その境界がオープンなプロトコルで結ばれている限り、私たちは自分のお気に入りの開発環境を自由に繋ぎ替えることができます。

リポジトリはGitHubで公開しています。フィードバックやPRを歓迎します。

👉 GitHub: Lqm1/pi-codex-app-server


※本プロジェクトは非公式の互換実装であり、OpenAI社とは提携していません。ご利用にあたっては各サービスの利用規約をご確認ください。