Архитектура¶
Статья описывает, из каких процессов состоит развёрнутая платформа Taimen, как они связаны, по каким путям идут запросы и события и где живёт авторитетное состояние. Она нужна, чтобы правильно выбрать компонент для новой функции, понимать поведение при сбоях и читать логи.
Архитектурные принципы¶
- Один факт — один авторитетный дом. Одно и то же состояние не редактируется одновременно в Git, Control Plane и памяти.
- Компоненты независимы. Между сервисами — версионируемые HTTP- и
event-контракты, а не общие базы данных и не импорты кода. Общая только
библиотека проверки токенов
platform-auth-sdk. - Память не управляет работой. Извлечённый контекст никогда не заменяет проверку в Control Plane.
- Identity ≠ лицензия ≠ доменное право. IAM подтверждает субъекта, Entitlement (если включён) — право на продукт, сервис-владелец ресурса — конкретное действие.
- Сбои деградируют локально. Память недоступна — операции Control Plane продолжаются, события копятся и доставляются позже.
Компоненты развёрнутого стека¶
flowchart TB
subgraph edge["профиль edge"]
CADDY[caddy<br/>:80 / :443]
end
subgraph core["профиль core"]
IAM[iam-service<br/>:8010]
IAMDB[(iam-db<br/>PostgreSQL 16)]
CPAPI[control-plane-api<br/>:8000]
CPW[control-plane-worker]
CTX[context-adapter]
CPDB[(control-plane-db<br/>PostgreSQL 16)]
MEM[memory-service<br/>:8077]
MEMDB[(memory-db<br/>PostgreSQL 16 + AGE + pgvector)]
end
subgraph platform["профиль platform (заморожен)"]
KC[keycloak]
PAPI[platform-api]
PWEB[platform-web]
MINIO[(minio)]
end
subgraph ext["вне compose"]
RUNNER[runner<br/>control-plane-agent]
HARNESS[MCP-клиент / CLI /<br/>Human Harness]
LLM[OpenAI-совместимый<br/>LLM endpoint]
end
CADDY -->|/iam/*| IAM
CADDY -->|/api/v1/*, /health/*, /docs| CPAPI
CADDY -->|/auth/*| KC
CADDY -->|/platform/*| PAPI
CADDY -->|/| PWEB
IAM --- IAMDB
CPAPI --- CPDB
CPW --- CPDB
CTX --- CPDB
MEM --- MEMDB
CPAPI -->|JWKS| IAM
CPAPI -->|сборка контекста| MEM
CTX -->|observations:batch| MEM
MEM -->|JWKS| IAM
MEM -.->|эмбеддинги, реранк| LLM
PAPI -->|federation:exchange| IAM
PAPI -->|шлюз /api/v1/services/*| CPAPI
RUNNER -->|PAT → токен| IAM
RUNNER -->|claims, runs| CPAPI
HARNESS -->|PAT → токен| IAM
HARNESS -->|MCP-команды| CPAPI
| Процесс | Команда запуска | Роль |
|---|---|---|
iam-service |
alembic upgrade head && uvicorn iam_service.app:app |
Tenants, principals, audiences, PAT, service accounts, федерация, выпуск токенов, JWKS |
control-plane-api |
alembic upgrade head && uvicorn control_plane.main:app |
HTTP API /api/v1/*, WebSocket /api/v1/events/ws, health, метрики |
control-plane-worker |
python -m control_plane.worker |
Доставка outbox, истечение lease claims и сессий, GC ключей идемпотентности, исполнение исходов approval |
context-adapter |
python -m control_plane.worker.context_adapter |
Переигрывает журнал событий в память: at-least-once, по tenant, с durable-курсорами |
memory-service |
образ memory-service |
Граф знаний, документы, гибридный поиск, Context Compiler |
caddy |
caddy:2-alpine |
Единственная точка входа снаружи, раскладка путей по сервисам |
Все три процесса Control Plane используют один образ control-plane с
разными командами. Схема БД применяется миграциями Alembic при старте
control-plane-api и iam-service.
Runner вне compose
Демон автономного исполнителя control-plane-agent не входит в
compose.yml: он ставится на отдельный хост (или машину разработчика) и
ходит в Control Plane и IAM по сети, как любой другой клиент. См.
Установка runner.
Раскладка путей на периметре¶
Снаружи виден только Caddy. Локальный deploy/caddy/Caddyfile.local и
промышленный Caddyfile используют одинаковую раскладку путей, различаются TLS и
именем хоста.
| Путь | Сервис | Примечание |
|---|---|---|
/iam/* |
iam-service:8010 |
префикс срезается; issuer IAM = ${TAIMEN_PUBLIC_URL}/iam |
/api/v1/*, /health/*, /metrics, /docs, /redoc, /openapi.json |
control-plane-api:8000 |
|
/auth/* |
keycloak:8080 |
профиль platform; Keycloak сам живёт под /auth |
/platform/* |
platform-api:8000 |
профиль platform; префикс срезается |
/support-demo/* |
support-bot:8000 |
профиль demo |
/memory/* |
memory-service:8077 |
только в локальном Caddyfile; в промышленной раскладке память наружу не публикуется |
/ (всё остальное) |
platform-web:3000 |
профиль platform |
Кроме того, каждый сервис публикует порт на 127.0.0.1 хоста (например,
Control Plane — 18000, IAM — 18010, память — 18001) для локальной работы,
bootstrap и make smoke. Полная таблица — в
Сервисах и портах.
Источники истины¶
| Данные | Авторитетный дом |
|---|---|
| Tasks (Work Items), их типы, статусы, связи, поля, комментарии | Control Plane |
| Claims, fencing tokens, sessions, runs, checkpoints, run actions | Control Plane |
| Approvals и их исходы, artifacts, goals, observations journal | Control Plane |
| Workspaces, Project Profiles, роли, capabilities, skills, делегации | Control Plane |
| Локальные principals и их права (bindings к IAM) | Control Plane |
| Tenants, principals identity, credentials (PAT, service accounts), audiences | IAM Service |
| Наблюдения, факты, документы, provenance, граф знаний | Memory Service |
| Лицензии, планы, квоты (если профиль включён) | Entitlement Service |
| Код, конфигурация стенда, пакеты каталога, ADR | Git |
| Секреты | .env, secrets/ или внешний Secret Manager |
Производные представления — очередь оператора, лента событий в консоли, Context Pack для агента — не являются источниками истины и пересобираются.
Поток: аутентифицированный запрос¶
Любой клиент — человек через MCP, runner, сервис — идёт в Control Plane по одной схеме: долгоживущий секрет предъявляется только IAM, Control Plane видит лишь короткоживущий токен своего audience.
sequenceDiagram
autonumber
participant C as Клиент (MCP / runner / сервис)
participant IAM as iam-service
participant CP as control-plane-api
participant DB as control-plane-db
C->>IAM: POST /api/v1/platform-access-tokens:exchange<br/>{token: PAT, audience: "control-plane", scopes}
IAM-->>C: {accessToken, expiresIn: 300, scope, sessionId}
C->>CP: POST /api/v1/tasks<br/>Authorization: Bearer <accessToken>
CP->>CP: проверка подписи по JWKS IAM,<br/>iss, aud = control-plane, exp
CP->>DB: iam_principal_bindings по (issuer, sub)
DB-->>CP: локальный Principal и его permissions
CP->>CP: permissions ∩ scope токена,<br/>доменная авторизация (CP_AUTHZ_MODE)
CP->>DB: команда + событие в журнал + outbox<br/>(одна транзакция)
CP-->>C: 201 Task
Service account (без человека и без PAT) вместо шага 1 вызывает
POST /api/v1/tokens/exchange с clientId/clientSecret. Детали — в
Модели безопасности.
Поток: события и память¶
Control Plane пишет каждое изменение в append-only журнал событий в той же транзакции, что и саму команду. Дальше события расходятся по независимым потребителям:
flowchart LR
CMD[Команда API] -->|одна транзакция| J[(events<br/>+ outbox)]
J -->|FOR UPDATE SKIP LOCKED| W[control-plane-worker<br/>доставка outbox]
J -->|курсор на tenant| CA[context-adapter]
CA -->|POST /api/memory/observations:batch<br/>namespace tenant:<id>| MEM[memory-service]
J -->|GET /api/v1/events<br/>курсор| CL[Клиенты:<br/>консоль, коннекторы,<br/>policy worker]
J -->|WS /api/v1/events/ws| RT[Realtime-клиенты]
Свойства доставки в память:
- at-least-once, без потерь, по tenant. Курсор tenant сдвигается только после подтверждения памятью; повтор после сбоя дедуплицируется по идентичности наблюдения.
- Изоляция сбоев. Постоянный отказ памяти на одном tenant паркует только его
курсор с нарастающим backoff; остальные tenant продолжают течь. Снятие с паузы —
явное действие оператора (
POST /api/v1/operations/context-adapter/{tenant_id}:redrive). - Singleton. Один потребитель на кластер (advisory lock PostgreSQL); вторая реплика ждёт, а не читает дважды.
- Namespace памяти tenant —
tenant:<tenant_id>, поддерево workspace —tenant:<tenant_id>:ws:<workspace_id>.
Поток: сборка контекста¶
Контекст для человека или агента собирается через Control Plane, а не прямым походом в память: Control Plane сначала авторизует и разрешает scope внутри tenant вызывающего, потом спрашивает память.
sequenceDiagram
participant H as Харнесс / агент
participant CP as control-plane-api
participant M as memory-service
H->>CP: POST /api/v1/context<br/>{task, query, maxTokens, strategy}
CP->>CP: авторизация, разрешение workspace/project,<br/>namespaces tenant
CP->>M: POST /api/memory/context<br/>(service account или ключ памяти)
M-->>CP: ContextPack с provenance
CP-->>H: операционный снимок + recalled-часть<br/>(маркированы раздельно)
Если память недоступна, операционная часть ответа всё равно формируется — деградация видна вызывающему. Подробнее — Контекст задачи и память.
Поток: исполнение задачи агентом¶
sequenceDiagram
autonumber
participant R as runner (control-plane-agent)
participant CP as Control Plane
participant X as Адаптер (Claude Code / Codex)
R->>CP: GET /api/v1/work/available
R->>CP: POST /api/v1/sessions
R->>CP: POST /api/v1/tasks/{ref}:claim → claimId, fencingToken
R->>CP: POST /api/v1/tasks/{ref}:start-run
R->>X: рабочая копия + prompt + контекст задачи
loop пока идёт работа
X-->>R: вызовы инструментов
R->>CP: run actions, checkpoints, heartbeat claim
end
R->>CP: POST /api/v1/artifacts (commit, transcript)
R->>CP: POST /api/v1/runs/{id}:succeed
Claim — эксклюзивная аренда задачи с TTL и монотонным fencing token: если
исполнитель потерял lease, его последующие записи отвергаются как
stale_claim. См. Исполнение — claims и runs.
Поток: вход человека через панель (профиль platform)¶
sequenceDiagram
participant B as Браузер
participant KC as Keycloak (/auth)
participant W as platform-web
participant PA as platform-api (/platform)
participant IAM as iam-service
participant CP as control-plane-api
B->>KC: Authorization Code + PKCE
KC-->>B: токен realm platform
B->>W: страница консоли
W->>PA: /api/v1/services/control-plane/...<br/>Bearer токен Keycloak
PA->>IAM: POST /api/v1/tenants/{t}/federation:exchange
IAM-->>PA: токен audience control-plane
PA->>CP: проксированный запрос
platform-api не выпускает токенов и не хранит authority: он меняет upstream-
токен Keycloak на токен IAM нужного audience и проксирует запрос. См.
Шлюз platform-api.
Инварианты интеграции¶
- Control Plane не вызывает память внутри доменной транзакции.
- Ни один сервис не читает базы IAM или других сервисов напрямую.
- Каждый audience получает отдельный короткоживущий токен (по умолчанию TTL
300 с,
IAM_TOKEN_TTL_SECONDS). - Токен IAM не несёт доменных прав: права Control Plane — в его собственной
таблице
iam_principal_bindings. - События доставляются в память как минимум один раз и дедуплицируются.
- В ответе контекста операционная и recalled-части не смешиваются без маркировки.
- Секреты, полные payload инструментов и рассуждения модели не попадают в события и память: транскрипт прогона публикуется артефактом с редакцией, без thinking.