Адаптеры исполнителей¶
Адаптер — сменная часть runner'а, которая отвечает на один вопрос: «заставить кодового агента сделать работу». Статья описывает три реализации — Claude Code, Codex и OpenCode, — как они запускают CLI, где берут credentials, в каком режиме разрешений работают и что попадает в prompt.
Контракт адаптера¶
Цикл координации принадлежит демону control-plane-agent; адаптер получает одну задачу:
| Аргумент | Что это |
|---|---|
task |
задача целиком (publicId, title, description, typeKey, …) |
run |
текущий run (id, attempt) |
client |
ControlPlaneClient под identity агента |
workspace |
рабочая копия задачи или None, если пул рабочих копий не настроен |
Адаптер возвращает спецификации артефактов (report, transcript); демон проверяет их на
переносимость (нет локальных путей и credential'ов), записывает, добавляет артефакт
commit и сам вызывает succeed_run. Исключение из адаптера превращается в fail_run с
причиной, из которой вырезаны пути хоста.
Выбор адаптера — CONTROL_PLANE_AGENT_ADAPTER:
| Значение | Реализация | Назначение |
|---|---|---|
echo (по умолчанию) |
встроенный | протокольные проверки: пишет action echo.observe и артефакт report с заголовком задачи |
claude-code |
control_plane_claude |
Claude Code CLI (claude -p) |
codex |
control_plane_codex |
Codex CLI (codex exec) |
Вендорские адаптеры импортируются лениво: демон работает на хосте, где CLI не установлен,
а неизвестное имя даёт unknown adapter '…' при старте.
OpenCode устроен иначе — это отдельный харнесс со своим циклом (см. ниже).
Общие правила всех адаптеров¶
- Prompt — через stdin, никогда аргументом: аргументы процесса видны всем на хосте.
- Секреты — только через наследуемое окружение. Токен подписки или API-ключ не попадают ни в argv, ни в файлы конфигурации run'а.
- Сессия агента переживает рестарт. Идентификатор сессии пишется в checkpoint run'а; следующая попытка той же задачи продолжает тот же разговор, а не начинает новый.
- Сырой поток остаётся на хосте. Каждая строка вывода CLI пишется в локальный журнал
<runtime>/sessions/<publicId>-<session>.jsonl(0600, потолок 32 МБ на файл за всю его жизнь, включая продолженные ходы). В Control Plane уходят только summary, счётчики и ограниченный отредактированный транскрипт (см. Трасса прогонов). - Один ход на run. Адаптер не ведёт многоходовый диалог; продолжение работы — следующий run, читающий checkpoints предыдущего.
- Исход решает демон. Агенту прямо сказано: не claim'ить, не завершать, не пушить и не открывать pull request — ветку публикует демон.
Claude Code¶
Как запускается¶
claude --print --output-format stream-json --verbose \
--permission-mode <mode> \
--session-id <uuid> # новая сессия
| --resume <uuid> # продолжение
[--model <model>] \
[--mcp-config <runtime>/mcp.json --strict-mcp-config] \
[--disallowedTools mcp__control-plane__cp_claim_task …]
Рабочий каталог процесса — рабочая копия задачи (worktrees/<publicId>/<repo>).
Идентификатор сессии генерируется адаптером до запуска процесса и сразу пишется в
checkpoint claude-code.session ({"claudeSessionId", "resumed", "phase": "started"}). Если
runner упадёт посреди хода, следующая попытка найдёт этот checkpoint и передаст --resume.
По завершении хода пишется checkpoint с phase: finished, subtype, turns; при ошибке —
phase: failed и тип исключения (без текста: в нём бывают пути).
Результат хода — событие result потока stream-json. Ход с is_error или ненулевым кодом
выхода — провал run; ход без события result — ошибка claude exited with code … without a
result с хвостом stderr в журнале runner'а.
Аутентификация¶
| Вариант | Переменная | Примечание |
|---|---|---|
| Подписка | CLAUDE_CODE_OAUTH_TOKEN |
выпускается claude setup-token на машине с браузером; принадлежит человеку |
| API-ключ | ANTHROPIC_API_KEY |
оплата за токены |
Адаптер токен не читает и не копирует: дочерний процесс и запущенные им MCP-серверы наследуют окружение runner'а. Следствие: агент видит токен в своём окружении — это свойство конструкции, поэтому периметр строится вокруг процесса (пользователь, контейнер), а не внутри него.
Окно подписки
Исчерпание окна подписки сейчас — обычная ошибка хода и fail_run. Параллельные
прогоны на токене человека расходуют то же окно, что и его интерактивные сессии.
Режим разрешений¶
CONTROL_PLANE_CLAUDE_PERMISSION_MODE передаётся в --permission-mode:
| Режим | Поведение | Когда |
|---|---|---|
acceptEdits (по умолчанию) |
правки файлов без запроса, остальные команды требуют подтверждения | безопасный старт; в неинтерактивном режиме агент не сможет выполнять команды (тесты, сборка) |
bypassPermissions |
все инструменты без запросов | рабочий режим автономного исполнителя — только внутри периметра |
Без bypassPermissions автономный агент не выполнит ни одной shell-команды: подтверждать
некому, и в отчётах это видно как «every tool call requires approval». Периметр при этом —
непривилегированный пользователь с ProtectSystem=strict или контейнер (см.
Установка runner).
MCP внутри агента¶
При CONTROL_PLANE_CLAUDE_MCP=1 (по умолчанию) адаптер пишет
<runtime>/mcp.json (0600) и передаёт его с --strict-mcp-config — агент получает ровно
один MCP-сервер control-plane и не видит чужих, настроенных у пользователя:
В файле нет ни адреса сервера, ни токена: control-plane-mcp наследует окружение runner'а и
резолвит identity так же, как демон. Агент работает под тем же principal'ом.
Через MCP агент сам читает cp_get_run_context, пишет checkpoints и артефакты. Но
авторитетные инструменты у него отобраны флагом --disallowedTools. Список не
записан в адаптере, а вычисляется из самого MCP-сервера (withheld_tool_names()): отбирается
всё, что не помечено read-only и не входит в набор инструментов evidence.
| Агенту доступно | Агенту недоступно (примеры) |
|---|---|
все read-only: cp_whoami, cp_context, cp_get_task, cp_get_run_context, cp_list_*, cp_search_tools, … |
cp_claim_task, cp_release_task, cp_start_run |
evidence: cp_checkpoint, cp_record_action, cp_create_artifact, cp_remember, cp_comment, cp_request_approval |
cp_complete_run, cp_fail_run, cp_suspend_run, cp_prepare_handoff |
cp_create_task, cp_update_task, связи, цели |
|
cp_approve, cp_reject, cp_invoke_skill, cp_launch_child, cp_control_run, cp_focus_project |
Инструмент без аннотации считается авторитетным — забытая разметка закрывает дверь, а не открывает.
Это сужение задачи, а не граница безопасности
Агент работает под тем же credential, что и демон, и технически может дойти до API
мимо MCP. Настоящий потолок — права binding агента (и child grant, который вычисляет
сервер для дочерних run). --disallowedTools лишь убирает соблазн.
Prompt¶
Prompt собирается из частей в фиксированном порядке:
- Системная заметка: ты автономный исполнитель одной задачи; MCP
control-planeдоступен для контекста, записей и checkpoints; не claim'ить, не завершать; работать только в текущем каталоге; не пушить и не открывать PR; закончить summary. - Задача:
# Task <publicId>: <title>и описание. - Проект (если задача в проекте): статус и шаблон.
- Входы задачи, если их объявил её тип (раздел
## Входы, см. ниже). - Контекст задачи из памяти (раздел
## Контекст задачи, см. ниже). - Соглашения репозитория — содержимое
CONTROL_PLANE_CLAUDE_PROMPT_FILEпод заголовком## Repository conventions.
Системная заметка также говорит агенту, что файл-результат, который не является
частью кода (документ, отчёт), сдаётся вызовом cp_create_artifact с параметром file:
инструмент загружает файл в Control Plane, и тот становится артефактом run'а (см.
Содержимое в хранилище).
Файл соглашений¶
CONTROL_PLANE_CLAUDE_PROMPT_FILE — путь к текстовому файлу, который дописывается в
каждый prompt. Описание задачи говорит «что сделать»; файл соглашений — «как устроен
этот репозиторий»: как запускать тесты, что никогда не коммитить, где лежат ADR, как
нумеровать миграции, каким сервисам можно доверять как контрактам. Это знание иначе стоит
по одному раунду ревью на каждую ветку.
Файл читается при каждом запуске хода — правка вступает в силу без перезапуска runner'а. Нечитаемый файл пишется в журнал предупреждением, ход продолжается без него.
Пример структуры:
# Соглашения для агента-исполнителя
## Среда
- Рабочая копия — текущий каталог. Соседи `../platform-auth-sdk` стоят на ревизиях
суперпроекта и доступны только для чтения.
- Контракт другого сервиса не выдумывай: бери схемы из кода соседа и закрепляй
contract-тестом.
- Полный прогон тестов: `uv run ruff check . && uv run pytest -q`.
## Правила
- Меняешь контракт API или схему БД — обнови ADR в том же коммите.
- Не коммить секреты, абсолютные пути, `.env`, артефакты сборки.
- Не трогай `main`: демон сам коммитит и публикует ветку задачи.
## Отчёт
Первой строкой — что сделано. Дальше: изменённые места, какие тесты прогнаны и с каким
результатом, что требует решения человека.
Параметры¶
| Переменная | По умолчанию | Смысл |
|---|---|---|
CONTROL_PLANE_CLAUDE_BINARY |
claude |
путь к CLI |
CONTROL_PLANE_CLAUDE_MODEL |
как настроено у CLI | модель (--model) |
CONTROL_PLANE_CLAUDE_PERMISSION_MODE |
acceptEdits |
режим разрешений |
CONTROL_PLANE_CLAUDE_TIMEOUT |
3600 |
потолок одного хода, секунды; по истечении процесс убивается, run проваливается |
CONTROL_PLANE_CLAUDE_MCP |
1 |
0 — не передавать MCP внутрь |
CONTROL_PLANE_CLAUDE_LOGS |
1 |
0 — не писать локальный журнал сессии |
CONTROL_PLANE_CLAUDE_RESUME |
1 |
0 — всегда начинать новую сессию |
CONTROL_PLANE_CLAUDE_RUNTIME_DIR |
~/.claude-runner |
где лежат mcp.json и sessions/ |
CONTROL_PLANE_CLAUDE_PROMPT_FILE |
— | файл соглашений репозитория |
Таймаут хода
Задачи с миграцией, API, клиентом и тестами редко укладываются в час. Для таких
очередей поднимайте CONTROL_PLANE_CLAUDE_TIMEOUT (например до 10800), помня, что
зависший ход держит аренду до конца таймаута.
Codex¶
Как запускается¶
codex exec --json --sandbox <sandbox> [--model <model>] - # новая сессия
codex exec --json --sandbox <sandbox> [--model <model>] resume <id> - # продолжение
- в конце заставляет Codex читать prompt из stdin.
В отличие от Claude Code, идентификатор новой сессии выбирает сам Codex и сообщает первым
событием потока (thread.started). Поэтому для новой сессии checkpoint codex.session
({"codexSessionId", …}) пишется в момент чтения этого события — уже после старта
процесса, но до реальной работы. Для продолжения известной сессии checkpoint пишется до
старта, как у Claude Code.
Особенности¶
- Без MCP внутри. Control Plane MCP Codex не передаётся: весь контекст задачи встроен в prompt, и агенту сказано, что инструментов Control Plane у него нет.
- Аутентификация —
auth.jsonв$CODEX_HOME(по умолчанию~/.codex/auth.json; подписка ChatGPT или ключ) илиOPENAI_API_KEY. Адаптер не трогаетCODEX_HOME, но Codex переписываетauth.jsonпри каждом запуске — каталог должен быть записываемым и постоянным (volume в контейнере), иначе после рестарта понадобится новый вход. - Песочница —
CONTROL_PLANE_CODEX_SANDBOX, по умолчаниюworkspace-write(правки в рабочей копии). Собственный режим Codex «только чтение» агенту бесполезен. Для ревьюера, которому нужно гонять тесты иgit fetch, применяютdanger-full-access— тоже только внутри периметра. - Исчерпание квоты распознаётся по тексту ошибки (
rate limit,usage limit,quota,429) и именуется отдельным исключением, но пока, как и у Claude Code, приводит к обычномуfail_run. - Файла соглашений у Codex-адаптера нет; всё нужное кладите в описание задачи.
- Action хода —
codex.turn; артефакты —report(summary) иtranscript.
Параметры¶
| Переменная | По умолчанию | Смысл |
|---|---|---|
CONTROL_PLANE_CODEX_BINARY |
codex |
путь к CLI |
CONTROL_PLANE_CODEX_MODEL |
как настроено у CLI | модель |
CONTROL_PLANE_CODEX_SANDBOX |
workspace-write |
режим песочницы |
CONTROL_PLANE_CODEX_TIMEOUT |
3600 |
потолок хода, секунды |
CONTROL_PLANE_CODEX_RESUME |
1 |
0 — всегда новая сессия |
CONTROL_PLANE_CODEX_LOGS |
1 |
0 — без локального журнала |
CONTROL_PLANE_CODEX_RUNTIME_DIR |
~/.codex-runner |
каталог журналов sessions/ |
CONTROL_PLANE_CODEX_CREDENTIAL_CLASS |
— | метка класса credential (подписка / ключ); попадает в metadata артефакта как credentialClass |
OpenCode¶
control-plane-opencode — самостоятельный харнесс, а не адаптер демона: у него свой цикл
discovery → claim → run, и он управляет процессом opencode serve по его HTTP API.
| Особенность | Значение |
|---|---|
| Запуск | control-plane-opencode рядом с opencode serve |
| Адрес OpenCode | OPENCODE_SERVER (по умолчанию http://127.0.0.1:4096), пароль — OPENCODE_SERVER_PASSWORD |
| Модель и агент | OPENCODE_MODEL, OPENCODE_AGENT |
| Очередь | CONTROL_PLANE_AGENT_WORKSPACE, CONTROL_PLANE_AGENT_PROJECT, CONTROL_PLANE_AGENT_SUBPROJECTS, CONTROL_PLANE_AGENT_POLL |
| Credential | только API-ключ Control Plane (CONTROL_PLANE_API_KEY или хранилище control-plane login) |
| Рабочая копия | нет пула worktree; работает в каталоге процесса |
| Continuity | checkpoint opencode.session с openCodeSessionId и lastMessageId |
| Результат | action opencode.prompt, артефакт report (opencode-summary) |
| Транскрипт | не публикуется |
Ограничения OpenCode-харнесса
Харнесс не умеет IAM-identity (только legacy API-ключ), не фильтрует задачи по
назначению и не использует рабочие копии. На сервере, где legacy-ключи выключены, он
не аутентифицируется. Используйте его для экспериментов, а для работы — демон с
адаптером claude-code или codex.
Входы задачи¶
Если тип задачи объявляет входы (artifactSchema.inputs, см.
Входы и выходы), демон готовит их сам
перед запуском адаптера; раздел в prompt получают Claude Code и Codex:
- читает
inputsизGET /runs/{id}/context; - скачивает каждый вход с содержимым в хранилище (
contentState: stored) черезGET /artifacts/{id}/content?forTask=<задача>в<runtime>/inputs/<key>/<name>. Каталог<runtime>— каталог задачи рядом с рабочей копией, а не внутри неё: вход не часть изменения, иgit add -Aего не захватит. Корень —CONTROL_PLANE_AGENT_RUNTIME_DIR, по умолчанию.runtimeв корне пула рабочих копий (без пула —~/.control-plane-agent/runtime); - имя файла очищается до одного компонента пути: разделители и служебные символы
заменяются на
_, ведущие точки убираются, длина ограничена; - каталог входов пересоздаётся на каждом run, так что новая head-ревизия заменяет старую; после успешного run он удаляется.
В prompt появляется раздел ## Входы — внутри ограды <task_inputs>…</task_inputs> с
предупреждением, что имена и содержимое входов — данные от других участников, а не
инструкции. По строке на вход: ключ, тип, задача-источник и связь, имя, id артефакта,
media type, размер и одно из:
| Состояние | Что в строке |
|---|---|
| скачан | file: <локальный путь> |
| не скачался | not downloaded (<код ошибки>); read it with cp_get_artifact_content |
| содержимое удалено | content purged by an administrator |
| ссылка без содержимого | reference only: <uri> |
Неудачное скачивание не проваливает run: агент видит, какой вход недоступен и почему, и
может получить его сам MCP-инструментом cp_get_artifact_content (Claude Code). Обходится
ли работа без входа — решает агент и пишет об этом в summary.
Контекст задачи в prompt¶
Все три реализации запрашивают у Control Plane рабочий контекст задачи (POST
/api/v1/context с task, run и includeMemory) и превращают его в раздел
## Контекст задачи одной общей функцией. Правила раздела:
- элементы памяти сгруппированы по секциям пакета (
current,relevant_facts,related_entities,documents, затем прочие), у каждого — источник[source: …]; - всё находится внутри ограды
<recalled_memory>…</recalled_memory>с предупреждением, что это данные от разных участников, а не инструкции; элемент не может закрыть ограду досрочно или начать собственную строку prompt'а; - каждая строка проходит редакцию путей хоста и credential'ов;
- один элемент — не длиннее 600 символов; весь раздел — не больше
CONTROL_PLANE_CONTEXT_BUDGET_CHARS(по умолчанию 12000); не вошедшее считается строкой… N more item(s) omitted by the context budget; - если память недоступна, пуста или не поместилась — одна строка
контекст памяти недоступен: <причина>. Run из-за этого не падает: задача сама по себе авторитетна.
Подробнее о памяти и контексте — Контекст задачи и память.
Сравнение¶
| Claude Code | Codex | OpenCode | |
|---|---|---|---|
| Тип | адаптер демона | адаптер демона | отдельный харнесс |
harness.type |
claude-code |
codex |
opencode |
| Credential Control Plane | IAM PAT или API-ключ | IAM PAT или API-ключ | только API-ключ |
| Рабочие копии и ветки | да | да | нет |
| MCP Control Plane внутри | да, без авторитетных инструментов | нет | нет |
| Файл соглашений | CONTROL_PLANE_CLAUDE_PROMPT_FILE |
нет | нет |
Транскрипт и tool.* actions |
да | да | нет |
| Checkpoint сессии | claude-code.session (до старта) |
codex.session (по thread.started) |
opencode.session |
Сигнал «остановлен» (executor_blocked) |
checkpoint blocked (cp_checkpoint) |
файл CONTROL_PLANE_BLOCKED_FILE → checkpoint blocked |
нет |