Identity агента¶
Как завести автономному исполнителю собственную identity: principal вида agent в IAM и
Control Plane, binding с ограниченным набором прав и Platform Access Token (PAT), и как
runner этот PAT хранит и предъявляет. Статья для администратора tenant'а и инженера,
разворачивающего runner.
Зачем отдельный principal¶
Агент никогда не работает под credential человека. Причина — аудит: под общим credential работа оператора и работа агента в журнале событий неотличимы, а различить их и есть смысл разделения. Отдельный principal даёт:
- атрибуцию — каждый claim, run, артефакт, комментарий и событие несут principal агента;
- потолок прав — у агента нет
adminиapprovals.decide, поэтому он не может ни переписать собственные права, ни одобрить gate, поставленный, чтобы его остановить; - независимый отзыв — PAT агента отзывается, не задевая доступ людей;
- адресацию работы — задачи назначаются агенту по его CP principal id
(
assigneeId), а runner сCONTROL_PLANE_AGENT_ONLY_ASSIGNED=1берёт только их.
Если исполнителей несколько (например, кодер и ревьюер или исполнители разных
репозиториев), у каждого свой principal: иначе с ONLY_ASSIGNED они брали бы задачи
друг друга.
Две половины identity¶
flowchart LR
subgraph IAM
IP["IAM Principal<br/>kind = agent"]
PAT["PAT<br/>audience control-plane<br/>scopeCeiling read+write"]
IP --- PAT
end
subgraph CP["Control Plane"]
CPP["Principal<br/>kind = agent"]
B["iam_principal_binding<br/>(issuer, iamPrincipalId) → права"]
CPP --- B
end
PAT -- "exchange → access token<br/>principal_type = agent" --> B
| Сущность | Где | Что задаёт |
|---|---|---|
IAM Principal (kind: agent) |
IAM | кто предъявляет PAT |
| PAT | IAM | audiences и потолок scopes (control-plane:read, control-plane:write) |
CP Principal (kind: agent) |
Control Plane | чьим именем записана работа, кому назначаются задачи |
| Binding | Control Plane | пара (issuer, iamPrincipalId) → CP Principal + список прав |
Access token, который runner получает обменом PAT, живёт минуты (по умолчанию 300 с) и
несёт principal_type = agent. У агента нет человеческого входа, поэтому в токене нет
auth_time и acr — по этому признаку ядро и отличает сессии агента от операторских.
Права агента¶
Права задаются списком в binding. Сервер отклоняет binding не-человеческого principal'а с
admin или approvals.decide ошибкой permissions_not_allowed_for_kind, а создатель
binding не может выдать права, которых нет у него самого (permission_escalation).
Базовый набор, который использует deploy/bootstrap.py для агентов:
| Право | Зачем runner'у |
|---|---|
sessions.open |
открыть сессию харнесса |
tasks.read |
видеть задачи и /work/available |
tasks.write |
комментарии, отношения, правка полей (вердикт ревьюера), создание задачи ревью |
tasks.claim |
claim задачи |
events.read |
журнал событий |
artifacts.read, artifacts.write |
артефакты report, transcript, commit |
projects.read |
контекст проекта в prompt |
task_types.read |
понять, не исполняется ли тип задачи скиллом (без него такие задачи не берутся) |
Дополнительно по ситуации:
| Право | Когда нужно |
|---|---|
approvals.manage |
кодер в режиме ревью human: демон сам запрашивает gate-approval на задачу ревью |
skills.execute |
демон исполняет вызовы скиллов (CONTROL_PLANE_SKILLS_*) |
observations.write |
агент пишет в память через cp_remember |
Никогда не выдавайте агенту
admin и approvals.decide. Сервер откажет и сам, но при ручной правке прав через
роли это правило стоит держать в голове: агент, способный решать approvals, обходит
любой gate ревью.
Заведение identity по шагам¶
Агентам с описанием шаги не нужны
Для агента, описанного видом Agent, identity заводит платформа: fleet-controller
создаёт principal в IAM (scope iam:agents) и выпускает PAT на каждое размещение, а
principal Control Plane и связку с правами из identity.permissions выводит ядро
(PUT /api/v1/agents/{key}/identity). Отзыв — вывод агента из оборота. См. Агенты
описанием и Узлы и fleet.
Ручные шаги ниже нужны для principal'ов без описания.
Ниже — последовательность вызовов API. Вручную её повторяют, когда добавляют исполнителя без описания в уже развёрнутую систему.
Обозначения: $IAM — базовый URL IAM (например https://platform.example.com/iam), $CP —
Control Plane, $ISSUER — issuer IAM (совпадает с публичным URL IAM), $BOOT — bootstrap-токен
IAM, $ADMIN — access token администратора Control Plane.
1. Principal в IAM¶
curl -sS -X POST "$IAM/api/v1/tenants/<iam-tenant-id>/principals" \
-H "X-IAM-Bootstrap-Token: $BOOT" -H 'Content-Type: application/json' \
-d '{"kind": "agent", "displayName": "Autonomous Runner"}'
Почему agent, а не service_account
IAM выпускает PAT только principal'ам вида human и agent; для service_account
выпуск отвечает 422 principal_kind_not_allowed. Service account получает доступ через
client credentials — это другой механизм (см. Service accounts).
2. Principal в Control Plane¶
curl -sS -X POST "$CP/api/v1/principals" \
-H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
-d '{"kind": "agent", "displayName": "Autonomous Runner", "metadata": {"slug": "runner"}}'
Запомните id ответа — это <cp-principal-id>: на него назначают задачи и его указывают
как ревьюера у другого исполнителя.
3. Binding с правами¶
curl -sS -X POST "$CP/api/v1/principals/<cp-principal-id>/iam-bindings" \
-H "Authorization: Bearer $ADMIN" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"issuer": "'"$ISSUER"'",
"iamTenantId": "<iam-tenant-id>",
"iamPrincipalId": "<iam-principal-id>",
"permissions": ["sessions.open","tasks.read","tasks.write","tasks.claim",
"events.read","artifacts.read","artifacts.write",
"projects.read","task_types.read"]
}'
Сначала binding, потом первый запрос
Control Plane кэширует отрицательный ответ проверки binding до конца жизни процесса
control-plane-api. Если runner предъявит токен до создания binding, ответ
binding_not_found «залипнет», и появившийся позже binding не будет подхвачен до
перезапуска control-plane-api. Порядок — строго: binding, затем запуск runner'а.
Binding ищется по паре (issuer, iamPrincipalId). Смена issuer IAM (например, публичного
URL) требует перенести binding тем же движением, иначе вход закроется.
4. PAT¶
curl -sS -X POST "$IAM/api/v1/tenants/<iam-tenant-id>/principals/<iam-principal-id>/platform-access-tokens" \
-H "X-IAM-Bootstrap-Token: $BOOT" -H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"name": "runner-2026-09",
"audiences": ["control-plane"],
"scopeCeiling": ["control-plane:read", "control-plane:write"],
"expiresInSeconds": 15552000
}'
Ответ содержит token — полный PAT, он показывается один раз, и credential.publicPrefix
— безопасный префикс для журналов.
| Параметр | Правило |
|---|---|
Idempotency-Key |
обязателен, без него 400 idempotency_key_required |
scopeCeiling |
с префиксом audience: control-plane:read, не read; должен входить в allowedScopes audience, иначе 422 invalid_scope_ceiling |
expiresInSeconds |
по умолчанию 30 дней (pat_default_ttl_seconds), максимум 365 дней (pat_max_ttl_seconds), больше — 422 expiry_too_long |
control-plane:admin |
агенту не выдавать |
Человеку для выпуска PAT IAM требует свежий authentication context (не старше 300 с); у
агента такого входа нет, и в записи PAT честно фиксируется снимок agent_bootstrap —
кто и когда выпустил credential bootstrap-операцией.
Подробнее о PAT — Credentials и PAT.
Где runner держит PAT¶
Runner (через control_plane_client) ищет credential в таком порядке: IAM-identity, если
задан CONTROL_PLANE_IAM_URL; иначе legacy API-ключ (CONTROL_PLANE_API_KEY или хранилище
control-plane login). На IAM-only сервере работает только первый путь.
PAT для IAM-identity берётся из одного из источников:
~/.config/iam/credentials.json пользователя, под которым работает runner
($XDG_CONFIG_HOME/iam/credentials.json, если задан). Права файла — строго 0600:
при более широких клиент отказывается его читать (iam_credentials_file_permissions).
Запись адресуется ключом <CONTROL_PLANE_IAM_URL>|<iam-tenant-id>|<iam-principal-id>,
поэтому несколько исполнителей одного tenant'а живут в одном файле. Каждый процесс
объявляет, кто он, переменной IAM_PRINCIPAL (IAM principal id).
Записать токен удобнее CLI iam из пакета iam-service — он проверит tenant и audience
токена интроспекцией:
sudo -u runner env HOME=/home/runner IAM_PRINCIPAL=<iam-principal-id> \
IAM_BINDING_FILE=/opt/runner/iam-binding.json \
iam auth login --stdin < agent.pat
где iam-binding.json — несекретный файл
{"iamUrl": "https://platform.example.com/iam", "tenantId": "<iam-tenant-id>"}.
Переменная без IAM_CREDENTIAL_MODE=environment (или ci) — ошибка
iam_environment_mode_required: унаследованная переменная не должна молча подменять
учётную запись. Так устроен контейнерный вариант: entrypoint читает PAT из
/run/secrets/agent-pat и экспортирует его только в окружение процесса.
На macOS клиент сначала смотрит в Keychain (сервис iam.platform-access-token).
На хостах runner'а это обычно не нужно; IAM_NO_KEYCHAIN=1 отключает поиск.
Несколько исполнителей на одной машине¶
Если в файле несколько записей одного IAM URL | tenant, а процесс не объявил
IAM_PRINCIPAL, клиент откажет с iam_credential_ambiguous. Это намеренно: выбрать запись
наугад значило бы работать под чужой identity, а заметно это стало бы только в аудите.
Задавайте IAM_PRINCIPAL в каждом юните (drop-in identity.conf, см.
Установка runner).
Ротация и отзыв¶
| Действие | Как |
|---|---|
| Выпустить новый PAT | шаг 4 с новым name; записать токен; перезапустить runner; отозвать старый |
| Ротация существующего | POST $IAM/api/v1/tenants/<t>/platform-access-tokens/<credential-id>:rotate — не продлевает окно жизни, срок задаётся при выпуске |
| Отозвать | POST $IAM/api/v1/tenants/<t>/platform-access-tokens/<credential-id>:revoke?reason=... |
| Список PAT principal'а | GET $IAM/api/v1/tenants/<t>/platform-access-tokens?principalId=<iam-principal-id> |
| Отключить binding | POST $CP/api/v1/iam-bindings/<binding-id>:revoke |
После отзыва PAT runner не получит новый access token и перестанет claim'ить и писать; уже выданный access token доживает свой срок (минуты). CP Principal при этом остаётся — история работы не теряется.
Следите за сроком PAT
PAT выпускаются со сроком; истёкший PAT runner увидит как ошибку обмена, и все задачи перестанут браться. Заведите напоминание о перевыпуске заранее.
Токен подписки кодового агента (Claude Code, Codex) — отдельный credential, он принадлежит человеку, а не агенту, и отзывается у поставщика. Подробнее — Адаптеры исполнителей.