Bootstrap¶
deploy/bootstrap.py — единый идемпотентный скрипт первичной инициализации
платформы: он заводит tenant в IAM и Control Plane, первого человека-
администратора, его Platform Access Token, проект и workspace, каталог типов
задач, service accounts ядра и опциональных сервисов. Статья разбирает каждый
шаг по коду скрипта: какие вызовы API он делает, что сохраняет и что делать при
сбое.
Запуск¶
make bootstrap запускает скрипт через
uv run --no-project --quiet --with pyyaml --with jsonschema python3, если uv
установлен, и системным python3 — если нет. Там, где make нет (например,
на сервере стенда), скрипт вызывают напрямую:
uv run --no-project --with pyyaml --with jsonschema python3 deploy/bootstrap.py --env .env
# или системным python3, если в нём есть PyYAML и jsonschema:
python3 deploy/bootstrap.py --env .env
Скрипт работает с хоста, а не из контейнера: к Control Plane и IAM он ходит
по портам 127.0.0.1 (CP_HOST_PORT, IAM_HOST_PORT), значения читает из
.env.
Параметры¶
| Флаг | По умолчанию | Смысл |
|---|---|---|
--env |
.env |
файл окружения (путь относительно корня суперпроекта) |
--name |
COMPOSE_PROJECT_NAME |
имя окружения: файл состояния deploy/state/<name>.json |
--operator |
служебное имя в скрипте | отображаемое имя первого человека-администратора; задайте своё |
--tenant-slug |
COMPOSE_PROJECT_NAME |
slug tenant в IAM и Control Plane (он же ключ шаблона проекта и slug workspace) |
--pat-ttl |
15552000 (180 дней) |
срок жизни выпускаемых PAT, секунды (не больше IAM_PAT_MAX_TTL_SECONDS, 365 дней) |
--secrets-dir |
secrets |
куда писать PAT и env-файлы service accounts |
--packages |
deploy/packages.yaml |
файл установки каталога (kind: Installation) — шаг 5b |
--no-packages |
выкл. | пропустить шаг 5b: пакеты ставит человек планом (plan --out → apply --plan) |
--harness-people |
выкл. | реестр людей с персональным харнессом; без него шаг 8 пропускается |
Зависимости¶
Скрипт использует только стандартную библиотеку Python для HTTP, но шаг 5b
импортирует package-sdk, которому нужны PyYAML и jsonschema.
make bootstrap при установленном uv подключает их сам; без uv они нужны в
системном Python.
Доменные валидаторы Control Plane package-sdk берёт из сабмодуля
services/control-plane/src; если они не импортируются, выдаётся предупреждение и
проверяется только схема формата.
Идемпотентность и состояние¶
Все созданные идентификаторы (не секреты) пишутся в
deploy/state/<name>.json после каждого шага. Повторный запуск читает этот
файл и пропускает сделанное: tenant, principals, шаблон и проект не создаются
заново. Некоторые действия выполняются при каждом запуске намеренно — они
приводят стенд к описанию:
PATCHпотолков scopes всех audiences по реестру скрипта;- установка пакетов каталога (новая версия типа — только при расхождении);
- публикация описаний сервиса уведомлений и fleet-controller и привязка их личностей в ядре (шаги 5c, 5d).
Секреты в файл состояния не попадают: PAT и client secrets пишутся только в
secrets/ с правами 0600, в stdout печатаются лишь префиксы и id.
Состояние и данные должны совпадать
Файл состояния описывает конкретные базы. Если удалить volumes (полный
сброс), выполните make reset-state: цель переносит
deploy/state/<name>.json и выданные bootstrap credentials
(secrets/harness-pat, secrets/*-iam.env) в secrets/stale-<время>/;
ключи подписи и .env не трогает. Если этого не сделать, bootstrap
остановится сразу после
ожидания сервисов: IAM tenant из файла состояния не найден (404
tenant_not_found), и скрипт подскажет make reset-state. Обратная ситуация — базы
живы, а файл состояния потерян — даёт 409 already_bootstrapped на шаге 3:
восстановите файл из резервной копии.
Пример deploy/state/taimen.json после первого прогона:
{
"iamTenantId": "<tenant-id>",
"iamAudiences": ["control-plane", "iam", "memory-service", "notification-service"],
"iamOperatorPrincipalId": "<iam-principal-id>",
"cpServiceAccountClientId": "<client-id>",
"cpServiceAccountPrincipalId": "<iam-principal-id>",
"cpServiceAccountCeiling": ["memory:on-behalf", "memory:read", "…"],
"cpTenantId": "<tenant-id>",
"cpOperatorPrincipalId": "<cp-principal-id>",
"cpLegacyAdminKeyId": "<api-key-id>",
"cpOperatorBindingId": "<binding-id>",
"operatorPatPrefix": "<prefix>",
"operatorPatExpiresAt": "<дата>",
"templateId": "<template-id>",
"projectId": "<project-id>",
"workspaceId": "<workspace-id>",
"catalog": { "…": {} },
"cpLegacyAdminKeyRevoked": true
}
Шаги¶
flowchart TB
S1[1. Ожидание CP и IAM] --> S2[2. IAM: tenant, audiences,<br/>human principal оператора]
S2 --> S2a[2a. Service account ядра<br/>→ secrets/control-plane-iam.env]
S2a --> S3[3. CP bootstrap: tenant с тем же id,<br/>admin principal, binding оператора]
S3 --> S4[4. PAT оператора<br/>→ secrets/harness-pat, обмен]
S4 --> S5[5. Шаблон проекта, проект, workspace]
S5 --> S5b[5b. Пакеты каталога]
S5b --> S5c[5c. Сервис уведомлений: SA по описанию,<br/>личность в ядре → secrets/notification-iam.env]
S5c --> R[Отзыв legacy api-key]
R --> END[готово]
Нумерация в выводе скрипта историческая: пропуски в ней — выведенные шаги, номера остальных сохранены.
1. Ожидание сервисов¶
Опрос GET /health/ready Control Plane и GET /healthz IAM раз в 2 секунды,
до 180 секунд. Не дождался — SystemExit("не дождался …").
Если в файле состояния уже есть iamTenantId, скрипт проверяет, что tenant
существует в IAM (GET /api/v1/tenants/{t}/audiences с bootstrap-токеном).
Ответ 404 означает, что volumes сброшены, а state остался; скрипт
останавливается с сообщением … ссылается на IAM tenant …, которого нет в IAM
(volumes сброшены?). Запуститеmake reset-stateи повторите bootstrap.
2. IAM: tenant, audiences, оператор¶
Все вызовы — с заголовком X-IAM-Bootstrap-Token: ${IAM_BOOTSTRAP_TOKEN}.
POST /api/v1/tenants{"slug": <slug>, "name": <slug>}→iamTenantId.-
Для каждого audience реестра —
POST /api/v1/tenants/{t}/audiences{"key", "allowedScopes"}(ответ409— уже есть, не ошибка), затем всегдаPATCH /api/v1/tenants/{t}/audiences/{key}{"allowedScopes"}— потолок audience растёт вместе с сервисом.Audience allowedScopescontrol-planecontrol-plane:read,control-plane:write,control-plane:admin,control-plane:decidememory-servicememory:read,memory:write,memory:pii,memory:tenants,memory:on-behalf,memory:servicenotification-servicenotifications:send,notifications:read,notifications:adminiamiam:channel-links,iam:agentshuman-harnessharness:use,harness:inboundfleetfleet:read,fleet:admin -
POST /api/v1/tenants/{t}/principals{"kind": "human", "displayName": <--operator>}→iamOperatorPrincipalId.
Если IAM_TENANT_ID в .env не совпадает с созданным tenant, скрипт печатает
!! впишите в .env: IAM_TENANT_ID=… — сделайте это (переменная нужна launcher'у
харнесса, fleet-controller и коннекторам).
2a. Service account Control Plane¶
POST /api/v1/tenants/{t}/service-accounts:
{
"displayName": "Taimen Control Plane",
"audiences": ["memory-service"],
"scopeCeiling": ["memory:read", "memory:write", "memory:tenants", "memory:on-behalf",
"memory:service"]
}
Ответ (clientId, clientSecret) записывается в
secrets/control-plane-iam.env как CP_IAM_CLIENT_ID / CP_IAM_CLIENT_SECRET.
Этот файл подключён env_file к трём процессам ядра; с ним Control Plane ходит
в память токеном IAM (CP_CONTEXT_AUTH=auto).
Если потолок в коде скрипта изменился по сравнению с сохранённым
(cpServiceAccountCeiling), service account перевыпускается, а прежний
отзывается (POST …/service-accounts/{clientId}:revoke): изменения потолка у
service account нет.
Перезапуск ядра
После выпуска или перевыпуска файла выполните
tools/compose up -d control-plane-api control-plane-worker context-adapter —
env_file читается при создании контейнера.
3. Control Plane: tenant, администратор, binding¶
POST /api/v1/bootstrap с Authorization: Bearer ${CP_BOOTSTRAP_TOKEN}:
{
"tenantSlug": "taimen",
"tenantName": "Taimen",
"adminDisplayName": "Alice Operator",
"tenantId": "<iam-tenant-id>",
"iamBinding": {
"issuer": "http://taimen.localhost/iam",
"iamTenantId": "<iam-tenant-id>",
"iamPrincipalId": "<iam-principal-id>"
}
}
Одной транзакцией Control Plane создаёт:
- tenant с тем же UUID, что у tenant IAM, — единый идентификатор организации;
- principal
humanс правомadmin; - legacy API-ключ администратора (возвращается в ответе, скрипт его позже отзывает);
- binding оператора
(issuer, iamPrincipalId)со всеми правами по имени — не толькоadmin, чтобы токен с потолком read+write не сузил binding до нуля.
Эндпоинт работает один раз: если в базе уже есть tenant, ответ
409 already_bootstrapped. Issuer берётся из TAIMEN_PUBLIC_URL — поэтому
публичный адрес нужно выбрать до bootstrap.
4. PAT оператора¶
POST /api/v1/tenants/{t}/principals/{p}/authentication-contexts{"issuer", "acr": "bootstrap", "amr": ["bootstrap-script"]}— свежий контекст аутентификации (IAM выпускает PAT человеку, только если контекст не старше 300 секунд).-
POST /api/v1/tenants/{t}/principals/{p}/platform-access-tokensсIdempotency-Key: -
Токен пишется в
secrets/harness-pat(0600); префикс и срок — в состояние. - Первый обмен
POST /api/v1/platform-access-tokens:exchangeна токен audiencecontrol-planeсо всеми тремя scopes — сразу, чтобы неверный PAT был виден на этом шаге. Все последующие шаги идут от имени оператора, но токен берётся перед каждым запросом: access token живёт минуты, а шаг 5b — дольше. Bootstrap обменивает PAT заново до истечения токена, а после401 invalid_credentials— ещё раз и повторяет запрос, поэтому долгая установка не падает на 401.
Если secrets/harness-pat уже существует, выпуск пропускается и используется
файл.
5. Шаблон проекта, проект и workspace¶
От имени оператора (Bearer-токен из шага 4):
POST /api/v1/project-templates{"key": <slug>, "displayName": …}→templateId.-
POST /api/v1/projects:{ "workspaceSlug": "taimen", "workspaceName": "Taimen", "workspaceTypeKey": "generic", "templateId": "<template-id>", "ownerPrincipalId": "<cp-principal-id>" }Control Plane создаёт workspace системного типа
genericи прикрепляет к нему Project Profile →projectId,workspaceId.
5b. Каталог из пакетов¶
Файл установки по умолчанию — deploy/packages.yaml:
Ядро доменно-нейтрально: само по себе оно знает только системный тип task.
Типы предметной области ставятся своими пакетами. Установка по умолчанию ставит
образец packages/example — тип задачи request; замените его своими пакетами.
Шаг ставит пакеты тем же единым планом, что и package-sdk plan → apply
--plan: install.plan строит план и пишет его в
deploy/state/<окружение>.packages-plan.json, install.apply применяет ровно
его. Подтверждение человека здесь заменяет сам запуск bootstrap, и это помечено
в журнале. Пустой план не применяется: повторный запуск ничего не пишет.
План сначала проверяет пакеты (JSON Schema формата, доменные валидаторы Control
Plane, замкнутость ссылок, engines, переменные установки), затем сравнивает их
со стендом по секциям: каталог, план ядра для пакетов с процессами или
календарями, онтологии и их включение, правила уведомлений, вывод из оборота.
Каталог приводится в порядке WorkspaceType, Capability, ConnectionType, Role, Skill,
ArtifactType, TaskType, Agent, ProjectTemplate, WorkRule; у пакета с
процессами или календарями типы задач, агентов, календари, процессы и правила
ставит ядро своим планом (подробно — в Пакетах
каталога):
| Вид объекта | Как применяется |
|---|---|
TaskType, ProjectTemplate |
версии неизменяемы: если активная версия расходится с пакетом — публикуется новая, прежние активные → deprecated; совпадает — «без изменений» |
WorkspaceType, Role |
создаются или приводятся PATCH |
Capability |
только создание |
Skill |
контракт неизменяем, меняется поднятием версии; описание и config — PATCH |
retire в файле установки |
указанные объекты выводятся из оборота |
Строки ${NAME} в spec подставляются из .env и окружения процесса. Если в
установке есть правила уведомлений (NotificationRule) или их вывод из
оборота, нужен сервис уведомлений: адрес — NOTIFICATION_SERVICE_URL, токен —
NOTIFY_TOKEN или обмен PAT оператора на audience notification-service
(для такой установки bootstrap выпускает PAT оператора с этим audience;
обменянный токен, как и токен ядра, обновляется перед истечением,
NOTIFY_TOKEN — нет). Без
адреса план отказывает, и bootstrap останавливается на шаге 5b. Итог шага —
запись packages в состоянии: файл установки, путь к плану, planHash, число
изменений и признак applied.
Флаг --no-packages пропускает шаг: на работающем стенде пакеты ставит
человек — package-sdk lock → plan --out → просмотр плана → apply --plan
(см. Установку и выпуск).
5c, 5d. Сервисы платформы¶
Сервисы, которые ходят в ядро по client credentials IAM, описаны агентами без
размещения (placement: none, identity.kind: service); описания задаёт
deploy/bootstrap.py. Для каждого — notification-service (5c) и
fleet-controller (5d) — шаг выполняется всегда, даже если профиль сервиса не
поднят (затрагивает только IAM и Control Plane):
- Service account IAM с audiences и потолком из
identity.iamописания →secrets/notification-iam.env(NS_SERVICE_CLIENT_ID,NS_SERVICE_CLIENT_SECRET) илиsecrets/fleet-iam.env(FLEET_CLIENT_ID,FLEET_CLIENT_SECRET). Если потолок в описании изменился, учётка перевыпускается, прежняя отзывается. POST /api/v1/agents— публикация описания в ядре.PUT /api/v1/agents/{key}/identity— привязка IAM principal учётки; principal ядра и связку с правами из описания выводит ядро. Повтор с той же учёткой ничего не меняет.- Если учётка перевыпущена, а ядро помнит прежнюю (
409 agent_identity_conflictна шаге 3), —POST /api/v1/agents/{key}/identity:replace: principal ядра остаётся прежним, прежняя связка отзывается, новая получает права текущей ревизии описания.
После выпуска файла скрипт напоминает пересоздать сервис
(tools/compose --profile notify up -d notification-service,
tools/compose --profile fleet up -d fleet-controller).
Затем скрипт отзывает legacy API-ключ администратора из шага 3
(POST /api/v1/api-keys/{id}:revoke) — стенд работает только через IAM.
Итог¶
В конце скрипт печатает путь к файлу состояния и подсказку для клиента:
готово: deploy/state/taimen.json
credential для MCP-плагина/CLI: ~/.config/iam/credentials.json, ключ http://taimen.localhost/iam|<tenant-id>|<iam-principal-id> → содержимое secrets/harness-pat
Как этим воспользоваться — в Первой задаче.
Файлы, которые создаёт bootstrap¶
| Файл | Шаг | Содержимое | Кто читает |
|---|---|---|---|
deploy/state/<name>.json |
все | идентификаторы, не секреты | сам bootstrap |
secrets/control-plane-iam.env |
2a | client credentials ядра | процессы Control Plane |
secrets/harness-pat |
4 | PAT оператора | человек: CLI, MCP-плагин, curl |
secrets/notification-iam.env |
5c | client credentials сервиса уведомлений | notification-service |
secrets/fleet-iam.env |
5d | client credentials контроллера fleet | fleet-controller |
Повторный запуск и перевыпуск¶
| Задача | Действие |
|---|---|
Обновить каталог после изменения packages/ |
make bootstrap — изменится только то, что расходится |
| Поставить другой набор пакетов | make bootstrap ARGS="--packages deploy/<env>/packages.yaml" |
| Начать заново после сброса volumes | make reset-state, затем make bootstrap |
| Перевыпустить PAT оператора (истекает) | удалить secrets/harness-pat и запустить bootstrap: новый выпуск со свежим контекстом аутентификации; старый PAT отзовите отдельно |
| Перевыпустить service account ядра | удалить secrets/control-plane-iam.env и запустить bootstrap, затем перезапустить ядро |
| Добавить исполнителей | описать агента в пакете и разместить через fleet — Декларативные агенты |
Типичные ошибки¶
| Сообщение | Причина | Решение |
|---|---|---|
не дождался http://127.0.0.1:18000/health/ready |
ядро не поднялось или порт другой | make ps, make logs svc=control-plane-api; проверить CP_HOST_PORT |
POST /api/v1/tenants: HTTP 401 |
IAM_BOOTSTRAP_TOKEN в .env не совпадает с тем, с которым запущен iam-service |
после правки .env пересоздать контейнер: tools/compose up -d iam-service |
POST /api/v1/bootstrap: HTTP 409 … already_bootstrapped |
Control Plane уже инициализирован, файла состояния нет | восстановить deploy/state/<name>.json |
POST /api/v1/bootstrap: HTTP 403 … bootstrap_disabled |
пустой CP_BOOTSTRAP_TOKEN |
задать значение, пересоздать control-plane-api |
нужен PyYAML / нужен jsonschema |
uv не установлен, у системного Python нет зависимостей | установить uv или pip install pyyaml jsonschema; при прямом вызове — uv run --no-project --with pyyaml --with jsonschema python3 deploy/bootstrap.py … |
… ссылается на IAM tenant …, которого нет в IAM (volumes сброшены?) |
volumes сброшены, файл состояния остался | make reset-state и повторить bootstrap |
пакеты не прошли проверку: … |
ошибка в YAML пакета | make packages-check, исправить пакет |
platform-access-tokens:exchange: HTTP 500 |
IAM не может прочитать ключ подписи | на Linux chown 10001:10001 secrets/iam-signing.pem, перезапустить iam-service |
токен audience notification-service не получен |
в установке есть правила уведомлений, а PAT оператора выпущен без audience notification-service (например, до того, как они появились) |
задать NOTIFY_TOKEN или удалить secrets/harness-pat и запустить bootstrap снова: шаг 4 выпустит PAT с этим audience |
5b. установка пакетов остановлена: стенд ответил HTTP 401 |
PAT оператора отозван или истёк: просроченный access token bootstrap обновляет сам | перевыпустить PAT (см. выше) |