生成AIの活用ノウハウや、 注目の技術に関する記事を掲載
公開

Codex App Serverとは?Pythonでの使い方とレビュー機能の実装

はじめに

Codex App Serverは、自作アプリからCodexを操作するためのインターフェースです。会話履歴やコマンドの実行状況を扱えるため、社内ツールにコードレビューのボタンを追加するなど、独自の画面からCodexを利用できます。

この記事では、Codex CLI・SDKとの違い、Pythonから接続する手順、Flaskで「レビュー開始」ボタンを作る方法を解説します。Codex CLIとPythonの基本操作を知っている開発者を対象に、最小クライアントからWebアプリまで順に実装します。

扱うのは、ローカル環境で動作するデモです。複数ユーザー向けの認証設計や本番運用は対象に含めません。

ChatGPTのChat・Work・Codexの使い分けは、次の関連記事で紹介しています。

生成AI関連
ChatGPTのChat・Work・Codexの違い|5時間の利用上限を無駄にしない使い分け

動作検証はCodex CLI 0.154.0で、2026年9月12日に実施したものです。仕様の出典は2026年9月17日に再確認しました。CLIのヘルプでは実験的機能とされているため、再現する際は利用バージョンを確認してください。

Codex App Serverを介して自作アプリがCodexを動かす構成

自作アプリはCodex App Serverを介してCodexへ指示を出し、Codexがリポジトリの調査・編集・コマンド実行を担う

Codex App Serverとは:CLI・SDKとの使い分け

Codex App Serverは、Codex VS Code拡張などのクライアントにも使われる仕組みです。自作アプリから指示を送り、Codexが返す作業状況や回答を画面に表示します。認証状態、会話履歴、承認のやり取りも扱えます(公式ドキュメント)。

モデルのAPIを直接利用して同様のアプリを作る場合は、ファイル操作や実行権限、履歴管理などの設計も必要です。App Serverを使うと、Codexの仕組みを利用しながら、用途に合わせた画面や操作を実装できます。

対話・自動実行・独自UIで選ぶ

方法 主な利用者 主な用途
codex 人間 ターミナルで対話しながら開発する
codex exec スクリプト CIやバッチから非対話で1回実行する
Codex SDK アプリ・CI プログラムからタスクを制御する
codex app-server 自作アプリ・IDE拡張 Codexのクライアントを実装する

ジョブの自動化やCIでの利用には、公式もCodex SDKを案内しています。会話履歴や承認画面、作業の進捗まで独自UIで扱いたい場合に、App Serverが選択肢になります。

Pythonからの接続には標準入出力を使う

通信方式は –listen で選びます。既定の標準入出力(stdio)では、改行で区切ったJSONを送受信します。本記事のPythonクライアントもこの方式を使います。

UnixソケットやWebSocketにも対応しています。たとえば、次の2つのコマンドを別々のターミナルで実行すると、WebSocketで待ち受けるApp ServerへCodex CLIから接続できます。

bash

codex app-server --listen ws://127.0.0.1:4500
codex --remote ws://127.0.0.1:4500

Codex CLIと自作アプリは、どちらもApp Serverに接続するクライアントになれます。ただし、公式ドキュメントはApp ServerコマンドとWebSocket接続を実験的かつ本番非サポートとしています。stdioを選んだだけで本番利用が保証されるわけではありません。

Codex App Serverの仕組み:Thread・Turn・Item

通信には、JSONでメソッド呼び出しと応答をやり取りするJSON-RPC 2.0に沿った形式を使います。ただし、各メッセージのjsonrpcフィールドは省略します。リクエストは method・params・id を持ち、応答は同じ id を返し、通知は id を持ちません。

会話全体、1回の指示に対する処理、その中で生じる入出力を、次の3つに分けて扱います。

単位 意味 対応するメソッド・通知の例
Thread ユーザーとCodexの会話1本。複数のTurnを含む thread/start、thread/resume、thread/fork
Turn ユーザーの指示を受けてから、その指示への処理が完了するまでの単位 turn/start、turn/interrupt、turn/steer
Item Turnの中に積まれる入出力の最小単位 item/started、item/completed、item/agentMessage/delta

Itemには、エージェントの発言以外の作業も含まれます。ユーザーメッセージ、推論、シェルコマンドの実行、ファイル編集、MCPツール呼び出し、Web検索、レビューモードの開始と終了などが、それぞれItemの一種として届きます。0.154.0のスキーマでは19種類のItemが定義されていました。

ThreadがTurnを含み、TurnがItemを積み上げる階層

Threadの中にTurnが並び、Turnの中にコマンド実行やファイル編集を含むItemが積まれる

クライアント側では、コマンド実行やファイル編集などのイベントを受け取り、作業の進み具合を表示できます。最終回答を待つ間も、利用者が現在の処理を把握できる仕組みです。

利用バージョンのスキーマを確認する

メソッドやパラメーターの定義をまとめたスキーマは、次のコマンドで生成できます。出力内容は、インストールされているCodexのバージョンに対応します。

bash

codex app-server generate-json-schema --out ./schema
codex app-server generate-ts --out ./ts

0.154.0で生成したスキーマでは、クライアントから呼べるリクエストが99種類、サーバーからの通知が81種類、サーバーからクライアントへのリクエストが10種類ありました。サーバー発のリクエストには、コマンド実行やファイル変更の承認などが含まれます。アプリ側で承認画面を表示し、利用者の判断を返す際に使います。

Codex App ServerをPythonから使う手順

PythonからApp Serverを子プロセスとして起動し、標準入出力でJSONを送受信します。Codex用の追加SDKは不要です。最小クライアントはPythonの標準ライブラリだけで動き、後半のWebアプリではFlaskを使います。

1. 実行環境とファイルを準備する

実行環境は下記のようにしました。

準備するもの 条件
Codex CLI 本記事では0.154.0を使用。codex –versionで確認
Python 3.10以上
uv Python環境と依存関係の管理に使用
Codexの認証 同じ実行ユーザーでcodex loginが完了していること
対象リポジトリ 調査対象のローカルGitリポジトリ。レビュー例では未コミット変更も必要

2. 接続を初期化してスレッドを作る

通信は、次の順序で進みます。

  1. codex app-serverを子プロセスとして起動します。
  2. initializeリクエストを送り、応答を受けてinitialized通知を送ります。
  3. thread/startで会話を作成します。
  4. turn/startで指示を送り、イベントを受け取ります。
  5. turn/completedで処理結果の状態を確認します。

接続直後に initialize リクエストと initialized 通知を送るのが必須で、この手順を飛ばすと以降のリクエストは「Not initialized」で拒否されます。

codex_client.py

"""codex app-server を子プロセスとして起動し、JSON-RPCで対話する最小クライアント。"""

import json
import subprocess
import threading
import time
from queue import Queue


class CodexAppServer:
    def __init__(self, cwd: str):
        self.cwd = cwd
        self.proc = subprocess.Popen(
            ["codex", "app-server"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.DEVNULL,
            text=True,
            bufsize=1,
        )
        self.events: Queue = Queue()
        self._results: dict = {}
        self._next_id = 0
        threading.Thread(target=self._read_loop, daemon=True).start()

    def _read_loop(self) -> None:
        for line in self.proc.stdout:
            line = line.strip()
            if not line:
                continue
            message = json.loads(line)
            if "method" in message:
                self.events.put(message)  # サーバからの通知
            else:
                self._results[message["id"]] = message  # リクエストへの応答

    def _send(self, method: str, params: dict, request_id: int | None = None) -> None:
        message = {"method": method, "params": params}
        if request_id is not None:
            message["id"] = request_id
        self.proc.stdin.write(json.dumps(message) + "\n")
        self.proc.stdin.flush()

    def call(self, method: str, params: dict | None = None, timeout: float = 120.0) -> dict:
        self._next_id += 1
        request_id = self._next_id
        self._send(method, params or {}, request_id)
        deadline = time.time() + timeout
        while time.time() < deadline:
            if request_id in self._results:
                message = self._results.pop(request_id)
                if "error" in message:
                    raise RuntimeError(f"{method}: {message['error']['message']}")
                return message["result"]
            time.sleep(0.05)
        raise TimeoutError(method)

    def initialize(self) -> dict:
        client_info = {"name": "techfun_demo", "title": "Tech Fun Demo", "version": "0.1.0"}
        info = self.call("initialize", {"clientInfo": client_info})
        self._send("initialized", {})
        return info

    def start_thread(self) -> dict:
        params = {"cwd": self.cwd, "approvalPolicy": "never", "sandbox": "read-only"}
        return self.call("thread/start", params)["thread"]

    def close(self) -> None:
        self.proc.terminate()

このクライアントは、メソッド呼び出しへの応答と、サーバーから届くイベントを振り分けます。通知にはmethodとparamsが含まれ、idはありません。サーバー発のリクエストにはidも含まれますが、この例は承認を求めない設定のため、その応答処理は実装していません。

3. 指示を送り、実行イベントを表示する

hello_appserver.pyでは、リポジトリの構成を説明するよう指示します。受信したイベントから、実行コマンドや回答文を取り出して表示します。

hello_appserver.py

"""App Serverへ最初の1ターンを投げ、返ってくるイベントをそのまま表示する。"""

import sys

from codex_client import CodexAppServer

PROMPT = "このリポジトリの構成を1行で説明してください。"


def main() -> None:
    cwd = sys.argv[1] if len(sys.argv) > 1 else "."
    codex = CodexAppServer(cwd)

    info = codex.initialize()
    print(f"userAgent : {info['userAgent']}")
    print(f"codexHome : {info['codexHome']}")

    thread = codex.start_thread()
    print(f"threadId  : {thread['id']}")
    print(f"session   : {thread['path']}\n")

    codex.call("turn/start", {"threadId": thread["id"], "input": [{"type": "text", "text": PROMPT}]})

    while True:
        event = codex.events.get()
        method, params = event["method"], event.get("params", {})
        if method in ("item/started", "item/completed"):
            item = params["item"]
            print(f"[{method}] {item['type']}")
            if method == "item/completed" and item["type"] == "agentMessage":
                print(f"  -> {item['text']}")
            if method == "item/completed" and item["type"] == "commandExecution":
                print(f"  -> $ {item['command']} (exit={item['exitCode']})")
        elif method == "item/agentMessage/delta":
            print(params["delta"], end="", flush=True)
        elif method == "error":
            print(f"[error] {params['error']['message']}")
        elif method == "turn/completed":
            print(f"\n[turn/completed] status={params['turn']['status']}")
            break

    codex.close()


if __name__ == "__main__":
    main()

次のコマンドの /path/to/your/repo を、調査対象リポジトリのパスに置き換えて実行します。検証時はuserAgentとcodexHomeに続き、スレッドIDと作業イベントが表示されました。

bash

uv run python hello_appserver.py /path/to/your/repo

hello_appserver.pyの実行でThreadの払い出しとItemイベントが順に表示される様子

initializeの応答、threadId、セッションファイルのパスに続き、Codexの作業がItemイベントとして流れる

turn/start に渡す input はテキストだけではありません。image、localImage、audio、localAudio を同じ配列に混ぜられるため、スクリーンショットを添えて指示するクライアントも作れます。

自作アプリの会話をCodex CLIで再開する

自作アプリで開始した会話は、同じ保存先を使うCodex CLIから再開できます。検証では、thread/startの応答に含まれるpathに、次のセッションファイルが返りました。

text

/home/ubuntu/.codex/sessions/2026/09/12/rollout-2026-09-12T09-19-35-01a092fb-dfe8-7862-96ce-725df9e80dcd.jsonl

保存先とファイル形式は、Codex CLIの対話セッションと共通です。次のコマンドのIDを、hello_appserver.pyで表示されたthreadIdに置き換えると、ターミナルから会話を続けられます。

bash

codex resume 01a092fb-dfe8-7862-96ce-725df9e80dcd

自作アプリが作成したスレッドをcodex resumeでターミナルから再開した画面

自作アプリで開始したスレッドのIDを指定すると、Codex CLIが同じ会話を読み込んで続きから対話できる

実際に再開して「直前の回答を一言で繰り返してください」と入力すると、自作アプリ側で得ていた回答がそのまま返りました。

この結果から、検証環境では自作アプリからCLIへ会話を引き継げることを確認できました。別の環境でも利用する場合は、実行ユーザーとCodexの保存先(CODEX_HOME)が一致しているかを確認してください。

Python・Flaskでコードレビュー用Webアプリを作る

次は、ブラウザの「レビュー開始」ボタンからCodexを呼び出します。PythonのWebフレームワークであるFlaskを使い、未コミット変更のレビュー結果を画面へ表示します。

review/startでレビュー対象を指定する

Codexのレビュー機能は review/start で呼び出します。targetに指定できる対象は、次の4種類です。

target レビュー対象
{"type":"uncommittedChanges"} ステージ済み・未ステージ・未追跡のファイル
{"type":"baseBranch","branch":"main"} 指定ブランチとの差分
{"type":"commit","sha":"abc1234"} 特定のコミット
{"type":"custom","instructions":"…"} 自由記述の指示

delivery は inline を指定し、既存スレッド上のTurnとしてレビューを実行します。レビュー用スレッドを分岐させるdetachedは、0.154.0のスキーマでは非推奨です。検証環境では「paginated threads do not support detached review」というエラーが発生しました。

レビュー専用のスレッドを分けたい場合は、thread/startで新しく作成してからinlineで実行します。

Flaskからレビューを開始して結果を中継する

app.pyは、ボタンが押されるとApp Serverを起動し、レビューを開始します。サーバーからブラウザへ継続的にデータを送るServer-Sent Events(SSE)を使い、実行コマンドとレビュー結果を中継します。

app.py

"""「レビュー開始」ボタンを押すと review/start を呼び、結果をSSEで流すWebアプリ。"""

import json
import os

from flask import Flask, Response, render_template

from codex_client import CodexAppServer

app = Flask(__name__)
PROJECT_DIR = os.environ.get("REVIEW_PROJECT_DIR", os.getcwd())


def sse(label: str, text: str) -> str:
    return f"data: {json.dumps({'label': label, 'text': text}, ensure_ascii=False)}\n\n"


@app.get("/")
def index() -> str:
    return render_template("index.html", project_dir=PROJECT_DIR)


@app.get("/review")
def review() -> Response:
    def stream():
        codex = CodexAppServer(PROJECT_DIR)
        codex.initialize()
        thread = codex.start_thread()
        yield sse("thread", thread["id"])

        codex.call(
            "review/start",
            {
                "threadId": thread["id"],
                "delivery": "inline",
                "target": {"type": "uncommittedChanges"},
            },
        )

        while True:
            event = codex.events.get()
            method, params = event["method"], event.get("params", {})
            if method == "item/completed":
                item = params["item"]
                if item["type"] == "exitedReviewMode":
                    yield sse("review", item["review"])
                elif item["type"] == "commandExecution":
                    yield sse("command", f"$ {item['command']}")
            elif method == "error" and not params["willRetry"]:
                yield sse("error", params["error"]["message"])
            elif method == "turn/completed":
                yield sse("done", params["turn"]["status"])
                break

        codex.close()

    return Response(stream(), mimetype="text/event-stream")


if __name__ == "__main__":
    app.run(port=5000, threaded=True)

レビュー結果は exitedReviewMode というItemの review に、全体の説明と指摘の箇条書きがまとまったプレーンテキストとして入ります。指摘を1件ずつ構造化して受け取る必要がなければ、この文字列をそのまま表示すれば十分です。

ブラウザにボタンと結果の表示欄を用意する

templates/index.htmlには、開始ボタンとログの表示欄を置きます。ブラウザのEventSourceでSSEに接続し、受け取った内容を順に追加します。

templates/index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Codex Review Button</title>
  <style>
    body { font-family: sans-serif; margin: 2rem; max-width: 48rem; }
    button { font-size: 1rem; padding: 0.6rem 1.4rem; }
    pre { background: #f6f8fa; padding: 1rem; white-space: pre-wrap; }
    .label { color: #2d6be4; font-weight: bold; }
  </style>
</head>
<body>
  <h1>Codex レビューボタン</h1>
  <p>対象ディレクトリ: <code>{{ project_dir }}</code> の未コミット変更</p>
  <button id="start">レビュー開始</button>
  <pre id="log"></pre>
  <script>
    const log = document.getElementById("log");
    document.getElementById("start").addEventListener("click", () => {
      log.textContent = "";
      const source = new EventSource("/review");
      source.onmessage = (event) => {
        const { label, text } = JSON.parse(event.data);
        log.insertAdjacentHTML("beforeend",
          `<span class="label">[${label}]</span> ${text}\n`);
        if (label === "done") source.close();
      };
    });
  </script>
</body>
</html>

Webアプリを起動してレビュー結果を確認する

REVIEW_PROJECT_DIRに、未コミット変更があるGitリポジトリのパスを指定します。起動後、ブラウザで http://127.0.0.1:5000/ を開いてください。

bash

uv sync --locked
REVIEW_PROJECT_DIR=/path/to/your/repo uv run python app.py

レビュー開始ボタンと対象ディレクトリだけが表示されたWebアプリの初期画面

Webアプリの初期画面。対象ディレクトリの未コミット変更をレビューするボタンだけを置いている

ボタンを押すと、threadIdが表示され、Codexが git status や git diff を確認していく過程が流れ、最後にレビュー本文が届きます。

ボタン押下後にthreadId、実行コマンド、レビュー本文が順に表示される画面

ボタン押下後、Codexが実行したコマンドとレビュー結果がSSEで順に流れてくる

このデモは、codex loginで設定済みの認証を利用します。アプリのコードにOpenAIのAPIキーを記述する必要はありません。Codex側の認証は必要なので、ログイン済みのユーザーと同じ環境から起動してください。

Codex App Serverの実装で注意する点

パラメーターの表記は生成したスキーマで確認する

検証時には、公式ドキュメントの例と0.154.0の列挙値が一致しない箇所がありました。thread/startのsandboxに workspaceWrite を渡すと、次のエラーが発生しました。

text

Invalid request: unknown variant `workspaceWrite`, expected one of `read-only`, `workspace-write`, `danger-full-access`

指定先によって、同じ概念でも表記が異なります。

指定先 書き込みを許可する値
thread/startのsandbox "workspace-write"
turn/startのsandboxPolicy {"type": "workspaceWrite"}

値が受け付けられない場合は、利用バージョンで生成したスキーマを確認してください。また、一部の実験的なメソッドやフィールドには、initializeの capabilities.experimentalApi をtrueにする設定が必要です。今回のサンプルで使う範囲では不要でした。

読み取り専用と承認なしの設定を区別する

サンプルでは sandbox をread-only、approvalPolicyをneverにしています。read-onlyはファイルの書き込みを制限する設定です。neverは承認を求めない設定であり、あらゆる操作を許可する意味ではありません。

承認を求める設定に変更する場合は、サーバーから届く承認リクエストに応答する処理が必要です。代表例は item/commandExecution/requestApproval と item/fileChange/requestApproval です。サンプルはこの応答処理を省いているため、そのまま設定だけ変更すると承認待ちで停止する可能性があります。

エラー通知とレビュー結果の重複を扱う

error通知には、自動で再試行するかを示すwillRetryが含まれます。今回のWebアプリは、再試行しないエラーだけを画面に表示する構成です。

検証では、exitedReviewModeのreviewと、その後のagentMessageのtextに同じレビュー本文が届きました。両方を表示すると重複するため、app.pyでは前者だけを表示しています。

実験的機能と本番利用の条件を確認する

CLIのヘルプではApp Serverを実験的機能として扱っています。メソッドや列挙値は変わり得るため、バージョンを更新した際はスキーマと動作の再確認が必要です。本記事のデモだけでは、本番での安定性や複数ユーザーでの運用は判断できません。

企業向けに連携機能を開発する場合は、clientInfo.nameの扱いも確認してください。この値はOpenAIのコンプライアンスログでクライアントを識別するために使われます。公式ドキュメントは、企業向けの新しい連携について、既知のクライアント一覧へ追加するためOpenAIへ連絡するよう案内しています(初期化の説明)。

Codex App Serverの活用例:レビュー画面や会話履歴の表示

今回確認したのは、未コミット変更をレビューするローカルWebアプリです。同じ仕組みの応用例として、次のような機能が考えられます。いずれも、本記事では実装・動作検証していません。

活用例 利用するメソッド・追加する処理
リポジトリごとのレビュー画面 対象ディレクトリを選び、review/startを実行する
ブランチ差分のレビュー review/startのtargetをbaseBranchに変更する
過去の会話一覧と再開ボタン thread/listで一覧を取得し、thread/resumeで再開する
実行中の作業への追加指示 turn/steerで現在のTurnへ指示を送る

たとえば、アプリで定型レビューを開始し、詳しく確認したい指摘があればCLIで会話を続ける運用が考えられます。Codex側の履歴を利用することで、用途ごとに操作画面を用意できます。

まとめ

Codex App Serverを使うと、会話履歴や実行イベントを扱う独自のCodexクライアントを作れます。本記事では、Pythonからの接続、CLIでの会話再開、Flaskのレビューボタンまでを紹介しました。

  • 独自UIで会話履歴・承認・進捗を扱う場合はApp Server、ジョブの自動化にはCodex SDKが選択肢になります。
  • 会話はThread、1回の処理はTurn、その中の入出力はItemとして扱います。
  • 今回のデモは、標準入出力とJSON-RPCを使い、Codex側のログイン状態で動作します。

まずは読み取り専用の設定でhello_appserver.pyを実行し、イベントの受信を確認してください。その後、レビューボタンの実装へ進むと、Codexとの通信と画面表示を順に理解できます。設定や機能を追加する際は、利用バージョンのスキーマを確認し、承認への応答など必要な処理を補ってください。

参考リンク:

生成AI活用支援サービスのご紹介

Tech Funでは、お客様のフェーズに合わせ、生成AI活用に向けた様々な支援をご提供しています。

  1. 無料診断パック:業務・プロセスの現状を無料で診断し、生成AI活用の可能性をレポートします。
  2. 検証(PoC)パック:診断で有効性が確認された業務を対象に、プロトタイプ構築を支援します。
  3. コンサルティングサービス:生成AI導入戦略の策定から運用体制構築までを包括的に支援します。
  4. Plain RAG:社内文書をもとに回答し、社内問い合わせを効率化するAIチャットパッケージです。

生成AIに限らず、Web・業務システム開発やインフラ設計など、技術領域を問わずご相談を承っています。「何から始めれば良いか分からない」という段階でも構いませんので、ぜひお気軽にお問い合わせください。

執筆・編集

Tech Fun Magazine R&Dチーム
Tech Funの生成AI研究に携わるエンジニアが、最新のAIモデル動向やプロンプト設計、実業務への応用手法など、生成AIに特化した知見を執筆・編集しています。
モデル評価や業務シナリオに応じたAI活用設計など、日々のR&D活動で得られる実践的なノウハウをわかりやすく紹介します。

ARTICLE
生成AI関連記事一覧

生成AI関連

ChatGPT Workとは?Businessで週次報告を任…

生成AI関連

Amazon Bedrock Managed Knowled…

生成AI関連

Google Sheets canvasはどこまで業務アプリ…

生成AI関連

Amazon BedrockのRerank APIで検索順位…

生成AI関連

OpenAI Agents APIとは?本番運用に必要な5つ…

生成AI関連

Codex App Serverとは?Pythonでの使い方…

生成AI関連

Amazon Bedrockの基盤モデルパーサーを実測する―…

生成AI関連

ChatGPT WorkのScheduled TasksをW…

生成AI関連

Amazon Bedrock Knowledge Bases…

記事一覧を見る