Модуль 3 · Встраивание · урок 11 из 14
App Server: свой клиент для Codex
Все поверхности Codex — CLI, расширение для IDE, десктоп, веб — работают на одном и том же агентном цикле. Связующее звено между циклом и интерфейсом называется App Server. Если вы строите свой клиент — панель для команды, интеграцию во внутренний портал, оркестратор, который ведёт много агентов, — это ваш уровень. В уроке разберём протокол, напишем минимальный клиент и поймём, когда App Server действительно нужен, а когда хватит SDK.
Зачем отдельный протокол
В статье OpenAI о создании App Server (февраль 2026) рассказана история. Расширению VS Code нужен был тот же цикл, что в терминальном интерфейсе. Сначала пробовали выставить Codex как MCP-сервер, но удержать семантику MCP так, чтобы она подходила IDE, оказалось трудно. В итоге сделали JSON-RPC-протокол, повторяющий цикл TUI. Потом пришли партнёры — JetBrains и Xcode, — и десктопному приложению понадобилось вести много агентов параллельно. Отсюда требования стабильности и обратной совместимости.
Что даёт App Server по сравнению с SDK: авторизацию, историю разговоров, одобрения и потоковые события агента в одном протоколе. Документация формулирует прямо: если вы автоматизируете задачи или запускаете Codex в CI, берите SDK. App Server — для глубокой интеграции в свой продукт. Учтите риск: на октябрь 2026 документация называет саму команду codex app-server экспериментальной и не поддерживаемой для продакшен-нагрузок, а не только транспорт WebSocket.
Если вы читали старые инструкции. Команда
codex mcp-serverи отдельный бинарникcodex-mcp-serverудалены. Существующие интеграции документация предлагает переводить на App Server.
Транспорты
codex app-server # stdio, по умолчанию
codex app-server --listen ws://127.0.0.1:4500 # WebSocket (экспериментально)
codex app-server --listen unix:// # Unix-сокет по умолчанию- stdio — JSON Lines: одно сообщение на строку. Самый простой и надёжный вариант для клиента, который сам запускает процесс.
- WebSocket — одно сообщение JSON-RPC на текстовый кадр. Транспорт экспериментальный и официально не поддерживается для продакшена. Тот же слушатель отдаёт проверки
GET /readyzиGET /healthz. - Unix-сокет — WebSocket поверх Unix-сокета.
Важное предупреждение про WebSocket. Слушатель не на loopback-адресе сейчас по умолчанию принимает соединения без аутентификации. Прежде чем открывать его наружу, настройте авторизацию (--ws-auth capability-token --ws-token-file /абсолютный/путь или подписанные bearer-токены) и TLS. Простой ws:// годится только для localhost и SSH-туннеля.
Той же механикой пользуется удалённый терминальный режим: codex app-server --listen ws://127.0.0.1:4500 на одной машине и codex --remote ws://127.0.0.1:4500 на другой.
Протокол
App Server говорит на JSON-RPC 2.0, но без поля "jsonrpc": "2.0" в сообщениях. Три вида сообщений:
{ "method": "thread/start", "id": 10, "params": { "model": "gpt-6.1-sol" } }
{ "id": 10, "result": { "thread": { "id": "thr_123" } } }
{ "method": "turn/started", "params": { "turn": { "id": "turn_456" } } }Запрос несёт method, params и id. Ответ повторяет id и содержит result или error. Уведомление идёт без id.
Протокол двунаправленный: сервер тоже отправляет запросы клиенту. Так приходят одобрения — сервер ставит ход на паузу и ждёт решения.
Примитивы
- Thread — разговор пользователя с агентом, долговечный контейнер. Потоки создают, продолжают, ветвят, архивируют.
- Turn — один запрос пользователя и вся работа агента после него.
- Item — единица ввода или вывода: сообщение пользователя, сообщение агента, выполнение команды, изменение файла, вызов инструмента. Жизненный цикл:
item/started→ необязательные дельты →item/completed. Финальным состоянием считаетсяitem/completed.
Жизненный цикл соединения
- Клиент отправляет
initializeсclientInfoи затем уведомлениеinitialized. До этого сервер отвергает любые запросы, повторныйinitializeвозвращает ошибку. thread/startсоздаёт поток,thread/resumeпродолжает,thread/forkветвит.turn/startсthreadIdи вводом пользователя начинает ход.turn/steerдобавляет указание в идущий ход,turn/interruptпрерывает.- Клиент читает уведомления:
item/started,item/agentMessage/delta,item/completed,turn/diff/updated,turn/plan/updated. turn/completedсообщает статус:completed,interruptedилиfailed.
Минимальный клиент
Клиент на Node.js: запускает сервер по stdio, создаёт поток, отправляет задачу, печатает ответ агента по мере генерации и отклоняет все запросы на одобрение.
import { spawn } from "node:child_process";
import readline from "node:readline";
const proc = spawn("codex", ["app-server"], { stdio: ["pipe", "pipe", "inherit"] });
const rl = readline.createInterface({ input: proc.stdout });
const send = (msg: unknown) => proc.stdin.write(JSON.stringify(msg) + "\n");
rl.on("line", (line) => {
const msg = JSON.parse(line);
// ответ на thread/start — запускаем ход
if (msg.id === 1 && msg.result?.thread?.id) {
send({
method: "turn/start",
id: 2,
params: {
threadId: msg.result.thread.id,
input: [{ type: "text", text: "Кратко опиши структуру этого репозитория." }],
},
});
}
// запрос от сервера: одобрение команды или правки файла
const approvals = ["item/commandExecution/requestApproval", "item/fileChange/requestApproval"];
if (msg.id !== undefined && approvals.includes(msg.method)) {
console.error(`\n[отклонено] ${msg.params?.command ?? msg.method}`);
send({ id: msg.id, result: { decision: "decline" } });
}
if (msg.method === "item/agentMessage/delta") process.stdout.write(msg.params.delta);
if (msg.method === "turn/completed") {
console.log(`\n[ход завершён: ${msg.params.turn.status}]`);
proc.kill();
}
});
send({ method: "initialize", id: 0, params: { clientInfo: { name: "amiga_panel", title: "Amiga Panel", version: "0.1.0" } } });
send({ method: "initialized", params: {} });
send({ method: "thread/start", id: 1, params: { cwd: process.cwd(), approvalPolicy: "on-request", sandbox: "read-only" } });Запуск: npx tsx client.mts внутри любого репозитория.
Что здесь стоит заметить:
- Одобрения. Запросы
item/commandExecution/requestApprovalиitem/fileChange/requestApprovalприходят сid, и на них нужно ответить решением. Для команд доступныaccept,acceptForSession,decline,cancelи вариант с поправкой правил выполнения. После ответа сервер присылаетserverRequest/resolved, а затемitem/completedсо статусомcompleted,failedилиdeclined. Настоящий клиент показывает пользователю команду, рабочий каталог и причину запроса. Наш просто отказывает. Другие запросы от сервера —item/permissions/requestApproval,item/tool/requestUserInput,mcpServer/elicitation/request— отвечаются в своём формате, их клиент тоже должен обрабатывать, иначе ход будет ждать ответа. clientInfo.name. Это не формальность: по нему клиент идентифицируется в журналах соответствия OpenAI (Compliance Logs). Если вы делаете интеграцию для корпоративного использования, документация просит связаться с OpenAI, чтобы её добавили в список известных клиентов.- Значения полей. В примерах документации и в сгенерированной схеме встречаются разные написания значений, например режима песочницы. Не угадывайте — генерируйте схему под свою версию.
Схема под вашу версию
Протокол развивается, поэтому типы не надо писать руками:
codex app-server generate-ts --out ./schemas
codex app-server generate-json-schema --out ./schemasВывод соответствует ровно той версии Codex, которой вы его сгенерировали. Закрепите версию CLI в своём продукте и перегенерируйте схему при обновлении. Так поступают и официальные клиенты: локальные приложения поставляют бинарник App Server, закреплённый на проверенной версии.
Часть методов и полей скрыта за флагом capabilities.experimentalApi в initialize. Без него сервер отвергает экспериментальные вызовы — и это хорошо: стабильная поверхность не сломается при обновлении. Для быстрой проверки без клиента есть отладочная команда codex debug app-server send-message-v2 "...".
Что ещё умеет протокол
Кроме потоков и ходов, в App Server есть ревью (review/start с целями uncommittedChanges, baseBranch, commit, custom), список моделей (model/list), навыки, приложения-коннекторы, авторизация через API-ключ или ChatGPT, лимиты и расход токенов. Сервер присылает и ошибки с типами — например, ContextWindowExceeded или UsageLimitExceeded, — чтобы клиент мог показать понятное сообщение. Полный список — в документации App Server. Исходники лежат в репозитории openai/codex.
Когда это оправдано
App Server стоит своих затрат, если вам нужно хотя бы одно из трёх:
- Свой интерфейс с одобрениями. Человек должен видеть, что агент собирается сделать, и отвечать из вашего UI.
- Много агентов под управлением программы. Оркестратор, который ведёт задачи из трекера, перезапускает зависших агентов и собирает результаты. Открытая спецификация OpenAI Symphony описывает именно такую схему поверх
codex app-server— подробнее в уроке «Оркестрация параллельных агентов». - Удалённый агент. Агент работает рядом с кодом и вычислениями, а клиент подключается к нему по сети.
Во всех остальных случаях начните с SDK. Python SDK уже работает поверх app-server, и вы получите большую часть возможностей без собственной обвязки.
Попробуйте сами
10–15 мин на рабочем месте- Запустите
codex app-server generate-ts --out ./schemasи найдите в схеме типыThreadStartParamsиCommandExecutionRequestApprovalResponse. - Сохраните клиент из урока и запустите его. Затем попросите агента «создай файл hello.txt» и посмотрите, как приходит запрос на одобрение и как сервер реагирует на отказ.
- Замените автоматический отказ на вопрос в консоли: показать команду и спросить y/n.
Коротко
- App Server — двунаправленный JSON-RPC (без поля
jsonrpc), на нём работают официальные клиенты Codex. - Транспорт по умолчанию — stdio с JSONL. WebSocket экспериментальный, без авторизации наружу не открывать.
- Рукопожатие
initialize→initialized, дальше потоки, ходы и элементы с потоковыми событиями. - Одобрения — запросы от сервера, клиент отвечает решением.
- Схему генерируйте под свою версию CLI. Для CI и простой автоматизации берите SDK.