Runbook — Temporality
Операционное руководство для администратора Temporality-стека.
#1. Архитектура развёртывания
┌─────────────────────────────────────────────────────┐
│ docker compose │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ postgres │ │ journal │ │ agent-kernel │ │
│ │ :5432 │ │ :8080 │ │ :8090 │ │
│ └────────────┘ └────────────┘ └──────────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ debugger │ │ temporal │ │
│ │ :3000 │ │ :7233/:8233 │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────┘
Сервисы: postgres (данные), journal (события), agent-kernel (workflow), debugger (UI), temporal (durable execution).
#2. Backup / Restore PostgreSQL
#Backup
# Пока контейнер работает
docker compose exec postgres pg_dump -U temporality temporality \
| gzip > backup-$(date +%Y%m%d-%H%M%S).sql.gz
# Проверить целостность
gunzip -t backup-*.sql.gz && echo "backup OK"
#Restore
# Остановить всё кроме postgres
docker compose stop journal agent-kernel debugger
# Восстановить
gunzip -c backup-YYYYMMDD-HHMMSS.sql.gz \
| docker compose exec -T postgres psql -U temporality temporality
# Перезапустить
docker compose up -d
#Автоматизация
Добавить в crontab хоста:
0 3 * * * cd /path/to/temporality && docker compose exec -T postgres pg_dump -U temporality temporality | gzip > /backups/temporality-$(date +\%Y\%m\%d).sql.gz && find /backups -name '*.sql.gz' -mtime +30 -delete
#Проверка backup
Раз в месяц восстановить backup в тестовую БД и проверить:
gunzip -c backup.sql.gz | docker compose exec -T postgres psql -U temporality test_restore
# Проверить наличие данных
docker compose exec -T postgres psql -U temporality test_restore -c \
"SELECT count(*) FROM observation_events; SELECT count(*) FROM kernel_event_outbox;"
docker compose exec -T postgres psql -U temporality -c "DROP DATABASE test_restore;"
#3. Migration / Upgrade
#Порядок обновления
- Backup (см. §2).
git pull— получить новую версию.docker compose build— собрать образы.docker compose up -d— перезапустить. Миграции применяются автоматически при старте journal и kernel.- Проверить health (см. §5).
#Миграции
Схема живёт в migrations/, каталог = владелец:
migrations/workspace/— kernel применяет всё при каждом старте (MigrateAll); baseline0001_baseline_v2026.2.21держит финальную схему, файлы0002+— инкременты после него;migrations/outbox/,migrations/quota/— по одному файлу на стор;migrations/runtime/не существует: историческая цепочка удалена, схему журнала journal несёт встроенной константой.
Каждая миграция идемпотентна — набор перечитывается при каждом старте, новая миграция это просто новый файл (подключать её вручную никуда не надо). Baseline самодостаточен для апгрейда с любой прошлой версии: секция 2 несёт ремонт колонок и data-фиксы всей цепочки, промежуточные версии не нужны.
Ручное применение:
docker compose exec postgres psql -U temporality temporality \
-f /path/to/migration.up.sql
#4. Token Rotation
#Текущие токены
JOURNAL_AUTH_TOKENS— токены journal API (reader/writer/operator)KERNEL_AUTH_TOKENS— токены kernel API (reader/writer/operator)TEMPORALITY_API_TOKEN— токен kernel → journal (writer); может быть не задан: тогда ядро на старте автоматически провиженит сервисный принципалtemporality-kernelв общей БД (writer, все проекты) и использует его токен. Деактивация этого пользователя в UI — осознанное действие администратора: ядро упадёт на старте с понятной ошибкой, пока его не реактивируют или не зададутTEMPORALITY_API_TOKENвручную.
#Добавление нового токена (без простоя)
# 1. Сгенерировать новый токен
NEW_TOKEN=$(openssl rand -hex 32)
# 2. Добавить в .env (не удаляя старый!)
echo "NEW_USER_TOKEN=${NEW_TOKEN}:newuser:reader:project-a" >> .env
# 3. Обновить JOURNAL_AUTH_TOKENS / KERNEL_AUTH_TOKENS в .env
# Формат: token:subject:role:projects;token2:...
# Добавить новый токен через ; к существующим
# 4. Перезапустить (старые токены продолжают работать)
docker compose restart journal agent-kernel
# 5. Проверить новый токен
curl -H "Authorization: Bearer $NEW_TOKEN" http://localhost:8080/healthz
# 6. Раздать новый токен пользователю
#Удаление токена (с пропаданием доступа)
# 1. Удалить токен из JOURNAL_AUTH_TOKENS / KERNEL_AUTH_TOKENS в .env
# 2. Перезапустить
docker compose restart journal agent-kernel
#Ротация TEMPORALITY_API_TOKEN
# 1. Сгенерировать новый
NEW_API_TOKEN=$(openssl rand -hex 32)
# 2. Добавить как writer в JOURNAL_AUTH_TOKENS
# 3. Обновить TEMPORALITY_API_TOKEN в .env
# 4. Перезапустить оба сервиса
docker compose restart journal agent-kernel
#5. Health Monitoring
#Эндпоинты
| Сервис | URL | Проверяет |
|---|---|---|
| Journal | GET http://localhost:8080/healthz |
DB connection |
| Kernel | GET http://localhost:8090/healthz |
Liveness |
| Temporal UI | http://localhost:8233 |
Web UI |
#Проверка состояния
# Все сервисы запущены?
docker compose ps
# Journal health
curl -s http://localhost:8080/healthz
# Kernel health
curl -s http://localhost:8090/healthz
# Temporal UI доступен?
curl -s -o /dev/null -w '%{http_code}' http://localhost:8233
#Автоматический мониторинг
Docker compose healthcheck уже настроен для postgres, journal, kernel. Можно добавить внешний мониторинг через cron:
*/5 * * * * curl -sf http://localhost:8080/healthz || systemctl restart temporality-journal
*/5 * * * * curl -sf http://localhost:8090/healthz || systemctl restart temporality-agent-kernel
#6. Outbox Monitoring
Outbox — очередь событий kernel → journal. Если outbox растёт, значит journal недоступен или не принимает события.
#Проверка состояния
# Требует operator-токен
curl -s 'http://localhost:8090/v1/agent/outbox' \
-H "Authorization: Bearer $OPERATOR_TOKEN"
Ответ:
{
"pending": 0,
"oldest_pending_age_seconds": 0,
"delivered": 1542,
"last_error": ""
}
#Тревога
pending > 0дольше 5 минут — journal недоступен.last_errorне пустой — проблема с доставкой.
#Действия
# Проверить journal
curl -s http://localhost:8080/healthz
# Проверить логи
docker compose logs journal --tail=20
# Если journal упал — перезапустить
docker compose restart journal
#7. Quota Monitoring
#Проверка использования
curl -s 'http://localhost:8090/v1/agent/quotas?project=PROJECT' \
-H "Authorization: Bearer $READER_TOKEN"
Ответ:
{
"project": "forge",
"used": 12,
"limit": 50,
"reset_at": "2026-09-28T00:00:00Z"
}
#Настройка квот
В .env:
KERNEL_RUN_QUOTAS=*:50,forge:200,lighthouse:100
*:50— дефолт 50 запусков/день на проектforge:200— конкретный проект- Пустая строка = без ограничений
После изменения — docker compose restart agent-kernel.
#Превышение квоты
Агент получает HTTP 429 с Retry-After. Оператор видит:
{
"error": "daily run quota exceeded for project forge",
"quota": {
"allowed": false,
"limit": 50,
"used": 50,
"reset_at": "2026-09-28T00:00:00Z"
}
}
#8. Типовые аварии
#MCP-сервер не подключается (EOF на initialize)
Симптом: connect MCP server ... connection closed: calling "initialize": client is closing: EOF при сохранении/проверке удалённого MCP-сервера.
Причина: сервер работает по Streamable HTTP, а в настройках выбран тип
sse (или наоборот). SSE-клиент ждёт от GET-стрима события протокола SSE,
которых streamable-сервер не присылает, и соединение умирает по EOF/таймауту.
Сетевая доступность (ping) тут ни при чём — проверять нужно HTTP-ответ.
Решение: сменить тип сервера на http (Streamable HTTP) в настройках
MCP-сервера. Ошибка также содержит подсказку о смене типа. Проверить сервер
вручную из контейнера:
docker compose exec -T agent-kernel wget -qO- --server-response \
--header='Content-Type: application/json' \
--header='Accept: application/json, text/event-stream' \
--header='Authorization: Bearer <ТОКЕН>' \
--post-data='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0.0.1"}}}' \
http://<host>:<port>/mcp
#Kernel не стартует (MCP connection error)
Симптом: connect MCP server: ... EOF в логах.
Причина: KERNEL_MCP_COMMAND указывает на бинарник, который не запускается в контейнере.
Решение: Отключить MCP (KERNEL_MCP_COMMAND= в .env) или
проверить бинарник:
docker compose exec -T agent-kernel /usr/local/bin/test-mcp
# Должен hang (ждёт stdin), не паниковать
#Outbox растёт
Симптом: pending увеличивается, oldest_pending_age_seconds растёт.
Причина: Journal недоступен.
Решение:
docker compose logs journal --tail=20
docker compose restart journal
#Temporal workflow завис
Симптом: Run показывает Running но не завершается.
Проверка:
# Temporal UI
open http://localhost:8233
# Или через kernel
curl -s "http://localhost:8090/v1/agent/runs/RUN_ID?project=PROJECT" \
-H "Authorization: Bearer $READER_TOKEN"
Действия:
- Проверить логи kernel:
docker compose logs agent-kernel --tail=50 - Если workflow завис в approval — отправить approval через UI
- Если workflow завис в tool — проверить sandbox (docker ps, логи)
#Нехватка памяти (PostgreSQL)
Симптомы: OOM killer, postgres restarts.
Решение:
# Проверить использование
docker stats postgres
# Очистить старые outbox-записи (доставленные >30 дней назад)
docker compose exec postgres psql -U temporality temporality -c \
"DELETE FROM kernel_event_outbox WHERE delivered_at < now() - interval '30 days';"
#Конфликт workflow ID
Симптом: 409 Conflict при запуске run.
Причина: Повторный run_id для того же проекта.
Решение: Использовать уникальный run_id (timestamp, UUID).
#9. Secrets
#Через файлы (docker secrets / 12-factor _FILE)
Kernel и journal поддерживают конвенцию <VAR>_FILE:
| Переменная | _FILE аналог |
|---|---|
DATABASE_URL |
DATABASE_URL_FILE |
TEMPORALITY_MODEL_API_KEY |
TEMPORALITY_MODEL_API_KEY_FILE |
TEMPORALITY_API_TOKEN |
TEMPORALITY_API_TOKEN_FILE |
KERNEL_AUTH_TOKENS |
KERNEL_AUTH_TOKENS_FILE |
JOURNAL_AUTH_TOKENS |
JOURNAL_AUTH_TOKENS_FILE |
Если VAR не задан, а VAR_FILE указывает на файл — значение берётся
из файла (trimmed). Прямое значение всегда побеждает.
#Пример docker-compose secrets
secrets:
model_api_key:
file: ./secrets/model_api_key.txt
services:
agent-kernel:
secrets:
- source: model_api_key
target: temporality_model_api_key
environment:
TEMPORALITY_MODEL_API_KEY_FILE: /run/secrets/temporality_model_api_key
#10. Полезные команды
# Логи в реальном времени
docker compose logs -f agent-kernel
# Пересобрать и перезапустить один сервис
docker compose up -d --build agent-kernel
# Запустить shell в контейнере
docker compose exec agent-kernel sh
# Проверить версию Go в контейнере
docker compose exec agent-kernel go version # только build stage
# Полный перезапуск
docker compose down && docker compose up -d
# Очистить неиспользуемые образы
docker system prune
#11. Модель доступа (оргструктура)
Спека: docs/org-structure.md; план волн: docs/plan-org-structure.md. Эта секция — практическая выжимка для администратора.
#Как вычисляется видимость
ресурс (agent/skill/mcp-server/provider/trigger): org_unit_id
NULL → глобальный, виден всем
узел → виден на узле и во всём его поддереве (materialized path `org_unit.path`)
проект: workspace_project_org_unit (m2m)
нет строк → org-нейтральный, виден всем
иначе → виден пользователям, чья цепочка предков пересекается с узлами проекта
∪ явные участники (workspace_project_member, доступ вне оргобласти)
пользователь: org_unit_id (основной узел)
NULL → видит всё (унаследованная супер-видимость, переходный режим)
роль admin на установке → обходит все проверки видимости
Видимость ≠ полномочия: право использования/изменения определяется эффективной ролью (роль установки, membership-роль, гранты org_unit_role на узел и его предков — максимум). Живые Run'ы не зависят от последующих изменений оргструктуры: контекст фиксируется в workspace_run.exec_context при старте.
#Как перепривязать ресурс
Привязка меняется только admin-эндпоинтом (обычный PUT ресурса привязку не трогает):
# привязать / перепривязать (пустой id = сделать глобальным)
PUT /v1/org/resources/{kind}/{id}/binding {"org_unit_id": "rd"}
# kind: agent | skill | mcp-server | provider | trigger
# убрать привязку (ресурс станет глобальным)
DELETE /v1/org/resources/{kind}/{id}/binding
# проекты: replace-семантика через обычный PUT проекта с полем org_units
PUT /v1/workspace/projects/{id} {"name": "…", "org_units": ["rd", "core"]}
После мутаций области ядро само пересчитывает скоупы токенов (refreshUserTokens); аудит: resource.bound/unbound, org_unit.* — смотри GET /v1/org/audit?limit=.
#Отчёт о расхождении моделей (волна G)
scripts/access_divergence.sh (SQL: scripts/access-divergence.sql) — read-only отчёт, сравнивающий унаследованные JSONB-списки (workspace_user.projects, workspace_project.allowed_users) с эффективной видимостью новой модели:
- A — пользователи, у которых старый список проектов ≠ новой эффективной видимости (
only_in_old/only_in_new— что пропало/добавилось); - B — проекты, у которых
allowed_users≠ новой аудитории; - C — сводка (сколько расхождений, сколько пользователей без узла и т.п.).
Отчёт имеет смысл только ДО применения миграции, удалившей легаси-колонки (сейчас это секция squashed baseline 0001) — на стенде она уже применена, скрипт оставлен как шаблон/документацию этапа 4 спеки §48. Семантика NULL: ресурс NULL = глобальный; пользователь NULL = видит всё.
#Что удалила волна G (легаси-колонки доступа)
- колонка
workspace_project.allowed_users— чтение для решений доступа отключено ещё в волне B (доступ = org ∪ membership); - колонка
workspace_user.projects— скоупы проектов пользователя считаются черезVisibleProjectIDs(волна C); - поля убраны из API:
allowed_usersв ответах/запросах проектов иprojectsв пользователях больше не приходят и не принимаются (клиентские SDK/скрипты, отправлявшие эти поля, продолжат работать — неизвестные поля игнорируются).
Обратно колонки не восстановить (down-миграция создаёт их заново со значениями по умолчанию "*" / []).