Перейти к содержанию

Первая задача

Статья проводит одну задачу через полный цикл исполнения — создание, claim, run, checkpoint, артефакт, завершение — тремя способами: напрямую через HTTP API (curl), через CLI control-plane и через MCP-сервер Control Plane в Claude Code. Предполагается, что стек поднят и bootstrap выполнен (Установка и первый запуск).

Что произойдёт

sequenceDiagram
    autonumber
    participant O as Оператор
    participant IAM as IAM
    participant CP as Control Plane
    O->>IAM: обмен PAT → токен audience control-plane
    O->>CP: POST /tasks → TASK-000001 (status todo)
    O->>CP: POST /sessions → sessionId
    O->>CP: POST /tasks/TASK-000001:claim → claimId, fencingToken<br/>(status in_progress)
    O->>CP: POST /tasks/TASK-000001:start-run → runId
    O->>CP: POST /runs/{runId}/checkpoints
    O->>CP: POST /artifacts
    O->>CP: POST /runs/{runId}:succeed → run succeeded,<br/>task done
    O->>CP: GET /events → журнал всего цикла

В примерах используется тип задачи devops из пакета каталога core (его ставит bootstrap). Можно не указывать тип вовсе — тогда применится системный тип task с теми же ключевыми статусами.

Способ 1. HTTP API

Подготовка переменных

CP=http://127.0.0.1:18000
IAM=http://127.0.0.1:18010
STATE=deploy/state/taimen.json
WS=$(jq -r .workspaceId "$STATE")

TOKEN=$(curl -s -X POST "$IAM/api/v1/platform-access-tokens:exchange" \
  -H 'Content-Type: application/json' \
  -d "{\"token\": \"$(cat secrets/harness-pat)\", \"audience\": \"control-plane\"}" \
  | jq -r .accessToken)
AUTH="Authorization: Bearer $TOKEN"

Токен живёт 300 секунд. Если на каком-то шаге пришёл 401, повторите обмен — PAT остаётся действительным.

1. Создать задачу

curl -s -X POST "$CP/api/v1/tasks" -H "$AUTH" -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @- <<JSON | tee /tmp/task.json | jq '{publicId, status, systemStatusCategory, typeKey, origin}'
{
  "title": "Проверить smoke локального стенда",
  "description": "Выполнить make smoke и приложить вывод.",
  "priority": "high",
  "typeKey": "devops",
  "workspaceId": "$WS",
  "customFields": {"environment": "local", "components": ["control-plane", "iam-service"]},
  "acceptance": [
    {"key": "smoke-ok", "kind": "deterministic",
     "description": "make smoke возвращает код 0"}
  ]
}
JSON
{
  "publicId": "TASK-000001",
  "status": "todo",
  "systemStatusCategory": "active",
  "typeKey": "devops",
  "origin": { "kind": "human", "evidence": [] }
}

Что стоит отметить:

  • status — начальный статус типа (initialStatus), категория — active;
  • origin не передан — ядро вывело его по виду пишущего principal (human);
  • customFields проверены по fieldSchema типа devops (например, environment принимает только local, staging, production, runner-host);
  • Idempotency-Key защищает от дубля при повторе запроса.

Куда задача может перейти дальше:

curl -s "$CP/api/v1/tasks/TASK-000001/transitions" -H "$AUTH" | jq

2. Открыть сессию

Claim всегда берётся в рамках сессии клиента:

SESSION=$(curl -s -X POST "$CP/api/v1/sessions" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"clientName": "curl-quickstart",
       "harness": {"type": "curl", "protocolVersion": "2",
                   "capabilities": ["tasks.interactive", "checkpoints"]}}' | jq -r .id)

Сессия живёт CP_SESSION_TTL_SECONDS (по умолчанию 300 с) без heartbeat (POST /api/v1/sessions/{id}:heartbeat). Для этого примера пяти минут хватит.

3. Взять задачу (claim)

curl -s -X POST "$CP/api/v1/tasks/TASK-000001:claim" -H "$AUTH" -H 'Content-Type: application/json' \
  -d "{\"sessionId\": \"$SESSION\", \"intent\": \"проверю smoke\"}" | tee /tmp/claim.json \
  | jq '{id, status, fencingToken, expiresAt}'
CLAIM=$(jq -r .id /tmp/claim.json)
FENCE=$(jq -r .fencingToken /tmp/claim.json)
{ "id": "<claim-id>", "status": "active", "fencingToken": 1, "expiresAt": "<время>" }

Задача перешла в claimStatus своего типа — in_progress. Аренда живёт 300 секунд; для долгой работы продлевайте её POST /api/v1/claims/{id}:heartbeat.

Почему нельзя взять

Если claim отвергнут, спросите у Control Plane причину: GET /api/v1/tasks/TASK-000001/claimability — ответ объясняет, что мешает (активный claim другого principal, незавершённые блокирующие задачи, неудовлетворённые requirements, терминальный статус).

4. Начать run

RUN=$(curl -s -X POST "$CP/api/v1/tasks/TASK-000001:start-run" -H "$AUTH" \
  -H 'Content-Type: application/json' \
  -d "{\"claimId\": \"$CLAIM\", \"fencingToken\": $FENCE}" | jq -r .id)

claimId и fencingToken обязательны: run стартует только под живым claim с актуальным токеном.

5. Записать checkpoint и артефакт

curl -s -X POST "$CP/api/v1/runs/$RUN/checkpoints" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"kind": "progress", "data": {"step": "smoke", "note": "запускаю make smoke"}}' | jq '{seq, kind}'

curl -s -X POST "$CP/api/v1/artifacts" -H "$AUTH" -H 'Content-Type: application/json' \
  -d "{\"type\": \"report\", \"name\": \"smoke-output\", \"task\": \"TASK-000001\",
       \"runId\": \"$RUN\",
       \"content\": {\"text\": \"iam-service OK 200; control-plane-api OK 200; memory-service OK 200\"}}" \
  | jq '{id, type, name}'

Checkpoint нужен для возобновления: если run прервётся, следующий исполнитель прочитает checkpoints в GET /api/v1/runs/{id}/context. Артефакт — результат работы, на который потом можно сослаться как на evidence.

6. Завершить

curl -s -X POST "$CP/api/v1/runs/$RUN:succeed" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"output": {"summary": "smoke зелёный"}}' \
  | jq '{run: .run.status, task: .task.status, category: .task.systemStatusCategory}'
{ "run": "succeeded", "task": "done", "category": "terminal_success" }

По умолчанию :succeed атомарно завершает и задачу (completeTask: true), переводя её в completionStatus типа, и освобождает claim. Передайте "completeTask": false, если задача должна остаться открытой — например, для следующей попытки или ревью.

Провал и приостановка

Честный провал — POST /api/v1/runs/{id}:fail с failureReason: claim сохраняется, можно начать следующий run. Приостановка до решения человека — :suspend с waitingForApprovalId. См. Исполнение — claims и runs.

7. Посмотреть журнал

TASK_ID=$(jq -r .id /tmp/task.json)
curl -s "$CP/api/v1/events?entityType=task&entityId=$TASK_ID" -H "$AUTH" \
  | jq -r '.items[] | "\(.occurredAt)  \(.type)"'
…  task.created
…  task.claimed
…  task.completed

Полный журнал всех сущностей — GET /api/v1/events?tail=20 (там же session.opened, run.started, run.checkpointed, artifact.created, run.succeeded). Эти же события context-adapter доставит в память tenant.

Способ 2. CLI control-plane

CLI входит в пакет control-plane и ставится как uv-инструмент из сабмодуля (рядом должен лежать platform-auth-sdk — path-зависимость):

uv tool install ./control-plane
control-plane --version

Подключение

CLI (как и MCP-сервер) ищет credential в таком порядке: IAM-identity, если задан CONTROL_PLANE_IAM_URL, затем legacy-ключ. Самый простой способ для локального стенда — передать PAT переменной окружения:

export CONTROL_PLANE_SERVER=http://127.0.0.1:18000
export CONTROL_PLANE_IAM_URL=http://127.0.0.1:18010
export CONTROL_PLANE_IAM_TENANT=$(jq -r .iamTenantId deploy/state/taimen.json)
export IAM_CREDENTIAL_MODE=environment
export IAM_PLATFORM_ACCESS_TOKEN=$(cat secrets/harness-pat)

control-plane whoami

IAM_PLATFORM_ACCESS_TOKEN требует IAM_CREDENTIAL_MODE

PAT из переменной окружения принимается только вместе с IAM_CREDENTIAL_MODE=environment (или ci); иначе клиент отвечает iam_environment_mode_required. Так унаследованная переменная не может молча подменить credential разработчика.

Постоянный вариант — файл ~/.config/iam/credentials.json с правами 600. Ключ записи — <CONTROL_PLANE_IAM_URL>|<tenant-id>|<iam-principal-id>:

{
  "http://127.0.0.1:18010|<tenant-id>|<iam-principal-id>": {
    "token": "<содержимое secrets/harness-pat>",
    "principalId": "<iam-principal-id>"
  }
}

Если в файле несколько credential одного tenant, процесс должен объявить себя переменной IAM_PRINCIPAL=<iam-principal-id>, иначе получит iam_credential_ambiguous. На macOS клиент сначала смотрит в Keychain (сервис iam.platform-access-token); отключить — IAM_NO_KEYCHAIN=1.

Ключ записи совпадает с адресом IAM, а не с issuer

Bootstrap печатает подсказку с ключом на основе issuer (http://taimen.localhost/iam|…). Такой ключ подходит, если CONTROL_PLANE_IAM_URL=http://taimen.localhost/iam (через Caddy). При обращении к IAM напрямую по порту ключ начинается с http://127.0.0.1:18010.

Цикл через CLI

CLI не создаёт задачи — создайте её через API (способ 1) или MCP (способ 3). Дальше:

control-plane work list                          # доступная principal работа
control-plane task get TASK-000002
control-plane task claimability TASK-000002
control-plane task claim TASK-000002 --intent "беру"
# → {"sessionId": "...", "claim": {"id": "<claim-id>", "fencingToken": 1, ...}}
control-plane run start TASK-000002 --claim <claim-id> --fencing-token 1
control-plane artifact add --type report --name smoke-output --task TASK-000002 --run <run-id>
control-plane run status <run-id>
control-plane events tail --replay 10            # Ctrl+C для выхода

task claim открывает собственную сессию CLI и не шлёт heartbeat: у вас есть TTL сессии и claim (по умолчанию 5 минут), чтобы начать и завершить run. Завершение run выполняется через API (:succeed) или MCP (cp_complete_run). Полный список команд — CLI и MCP-сервер.

Способ 3. MCP-сервер в Claude Code

MCP-сервер control-plane-mcp ставится тем же uv tool install ./control-plane и работает по stdio. Он держит сессию и heartbeat claim сам, а права — те же, что у PAT оператора.

Подключение

claude mcp add control-plane \
  -e CONTROL_PLANE_SERVER=http://127.0.0.1:18000 \
  -e CONTROL_PLANE_IAM_URL=http://127.0.0.1:18010 \
  -e CONTROL_PLANE_IAM_TENANT=<tenant-id> \
  -e CONTROL_PLANE_HARNESS_TYPE=claude-code \
  -- control-plane-mcp

Credential берётся из ~/.config/iam/credentials.json (см. выше) или из пары IAM_CREDENTIAL_MODE=environment + IAM_PLATFORM_ACCESS_TOKEN, переданной через -e. Проверьте в сессии Claude Code: попросите вызвать cp_whoami.

Вместо CONTROL_PLANE_SERVER можно положить в репозиторий файл .control-plane/config.json (control-plane init --server … --workspace … --project …) — тогда MCP-сервер знает проект этого репозитория и создаёт задачи в его workspace.

Цикл в диалоге

Инструменты MCP-сервера рассчитаны на работу с подтверждением человека: описания cp_create_task, cp_claim_task и cp_complete_run требуют явного решения пользователя. Типичный диалог:

Реплика оператора Инструмент Что происходит
«Покажи, кто я и что мне доступно» cp_whoami, cp_list_work identity, права, доступные задачи
«Создай задачу типа devops: проверить smoke локального стенда, environment=local» cp_list_task_types, cp_create_task задача TASK-000003 в статусе todo
«Беру TASK-000003» cp_claim_task сессия + claim; heartbeat держит MCP-сервер
«Начинай» cp_start_run run под текущим claim
— cp_checkpoint, cp_record_action прогресс и аудит действий
«Приложи вывод smoke» cp_create_artifact артефакт к задаче и run
«Готово, закрывай» cp_complete_run run succeeded, задача done, claim освобождён

Если инструмент вернул stale_claim, task_already_claimed или run_not_active, владение задачей потеряно: MCP-сервер подсказывает перечитать cp_context и не повторять запись. Подробно о плагине и повседневной работе — MCP-плагин для Claude Code.

Что дальше

Хочу Куда
Настроить свои типы задач и статусы Типы задач и статусы, Пакеты каталога
Связать задачи с целями и evidence Цели, приёмка и evidence
Добавить approval перед завершением Approvals
Отдать задачу автономному агенту Агенты и runner
Дать агенту контекст из памяти Контекст задачи и память
Работать через веб-консоль профиль platform, Консоль платформы

Типичные проблемы

Симптом Причина Решение
401 на любом запросе к Control Plane токен истёк (300 с) или обмен делался не тем PAT повторить обмен
403 scope_not_allowed при обмене запрошен scope вне потолка PAT или без префикса (read вместо control-plane:read) запросить корректные scopes или не передавать scopes
stale_claim при start-run или :succeed claim истёк или перехвачен, fencing token устарел взять задачу заново; для долгой работы — heartbeat
422 при создании задачи с customFields поля не проходят fieldSchema типа GET /api/v1/task-types/{id} — посмотреть схему
CLI: no credentials for … не задан CONTROL_PLANE_IAM_URL или не найден PAT переменные из раздела «Подключение»
CLI/MCP: iam_credentials_file_permissions у credentials.json права шире 600 chmod 600 ~/.config/iam/credentials.json

См. также