diff --git a/docs/architecture/adr/README.md b/docs/architecture/adr/README.md index 59430c8..f4874c4 100644 --- a/docs/architecture/adr/README.md +++ b/docs/architecture/adr/README.md @@ -9,6 +9,7 @@ Per-work-item решения живут в `docs/work-items//06-adr/ADR-NNN- | adr-0002 | Очередь задач вместо in-process потоков | accepted | 2026-06-03 | ORCH-1 | | adr-0003 | Условный staging-гейт перед прод-деплоем | accepted | 2026-06-05 | ORCH-35 | | adr-0004 | Поллинг с ретраем в check_ci_green (фикс CI-race) | accepted | 2026-06-05 | ORCH-045 | +| adr-0005 | Контейнеры бегут под uid:gid хоста (1000:1000) | accepted | 2026-06-06 | ORCH-040 | ## Формат **Контекст → Решение → Альтернативы → Последствия → Связи.** Статус: proposed / accepted / superseded. diff --git a/docs/architecture/adr/adr-0005-container-runs-as-host-uid.md b/docs/architecture/adr/adr-0005-container-runs-as-host-uid.md new file mode 100644 index 0000000..339da3c --- /dev/null +++ b/docs/architecture/adr/adr-0005-container-runs-as-host-uid.md @@ -0,0 +1,42 @@ +# adr-0005: Контейнеры оркестратора бегут под uid:gid хоста (1000:1000) + +- **Статус:** accepted +- **Дата:** 2026-06-06 +- **Задача:** ORCH-040 + +## Контекст +Оба контейнера (`orchestrator`, `orchestrator-staging`) запускались под `uid=0 (root)` и +монтировали хостовый `/home/slin/repos` → `/repos` (rw). Claude-CLI агенты исполняются +`subprocess.Popen` внутри контейнера под тем же root, поэтому все артефакты конвейера +(git worktree, коммиты в `docs/`) появлялись на хосте как `root:root`. Деплой прода под +`slin` (uid 1000) ломался на правах git до ручного `chown`. Это сквозное свойство рантайма: +касается агентов **всех** проектов, а не отдельной фичи. + +## Решение +Оба сервиса в `docker-compose.yml` запускаются под `user: "1000:1000"` (uid:gid хоста `slin`). +- `group_add: ["999"]` сохраняется — доступ к docker.sock идёт через gid 999, не через root. +- target SSH-маунта приведён к `/home/slin/.ssh` (был `/root/.ssh`), синхронно с + `HOME=/home/slin`, который форсит launcher → единый HOME по осям uid/claude/ssh. +- Образ и launcher не меняются: numeric uid не требует записи в `/etc/passwd`, + `git config --system safe.directory '*'` уже есть. + +Обязательные host-prerequisites (Owner, вне кода): доступ uid 1000 к +`/home/slin/.claude/.credentials.json` (блокер), ssh-ключи в новом HOME, рестарт prod +только в окно тишины. Детали и команды — work-item ADR-001 и `docs/operations/INFRA.md`. + +## Альтернативы +- **drop-privileges только для subprocess агента** (`gosu`/`setuid`) — контейнер остаётся + root; новый код в горячем пути launcher, два uid в одном контейнере; отклонён. +- **chown-хук после каждой стадии** — лечит симптом, требует root внутри контейнера + (несовместимо), хрупкий пост-шаг; отклонён (fallback на крайний случай). + +## Последствия +- Артефакты создаются под `slin:slin`; деплой прода не требует ручного `chown`. +- HOME консистентен (uid = claude = ssh = `/home/slin`); устранён рассинхрон SSH-маунта. +- Появляется явная привязка рантайма к uid 1000 хоста (задокументирована в INFRA.md). +- Прод-рестарт self = групповой риск (общий инстанс с enduro-trails) → строго окно тишины; + страховка — staging-гейт (adr-0003). + +## Связи +adr-0003 (staging-гейт — обязательная проверка перед прод-рестартом self), +adr-0001 (`is_self_hosting_repo`), work-item `docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md`. diff --git a/docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md b/docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md new file mode 100644 index 0000000..b00c2b8 --- /dev/null +++ b/docs/work-items/ORCH-040/06-adr/ADR-001-run-agents-as-host-uid.md @@ -0,0 +1,109 @@ +# ADR-001: Контейнер и агенты бегут под uid:gid хоста (1000:1000), а не root + +- **Статус:** Accepted +- **Дата:** 2026-06-06 +- **Задача:** ORCH-040 +- **Связи:** глобальный [adr-0005](../../../architecture/adr/adr-0005-container-runs-as-host-uid.md), adr-0003 (staging-гейт — страховка перед прод-рестартом self), adr-0001 (`is_self_hosting_repo`). + +## Контекст + +Контейнер `orchestrator` (prod, 8500) работает под `uid=0 (root)` и монтирует хостовый +`/home/slin/repos` → `/repos` (rw). Claude-CLI агенты запускаются через +`subprocess.Popen` **внутри контейнера**, т.е. под тем же root. Все артефакты конвейера +(git worktree `/repos/_wt/...`, коммиты в `docs/work-items/...`) появляются на **хосте** +с владельцем `root:root`. + +Следствие: при каждом деплое прода `git pull` / `git reset` под пользователем `slin` +(uid 1000) падает с `insufficient permission for adding an object to repository database` +/ `Permission denied`. Каждый деплой ломается, пока вручную не сделать `chown`. + +Разведкой (05–06.06) подтверждено: +- `slin = uid 1000 gid 1000`, в группах `sudo`, `docker(999)`; на хосте `/repos` и + `/app/data` уже `1000:1000`. +- launcher **уже** форсит `HOME=/home/slin` в двух местах: env `Popen` (`launcher.py:326`) + и `git_env` (`launcher.py:513`). Креды читаются из `/home/slin/.claude`. +- `docker-compose.yml`: оба сервиса имеют `group_add: ["999"]` (доступ к docker.sock — + через gid 999, **не** через root); SSH-маунт обоих = `/home/slin/.orchestrator-ssh:/root/.ssh:ro`. +- `CLAUDE_BIN=/opt/claude-code/bin/claude.exe` (`launcher.py:187`), `+x` для всех. +- Dockerfile содержит `git config --system --add safe.directory '*'`. + +## Рассмотренные варианты + +1. **Вариант 1 (выбран): `user: "1000:1000"` в docker-compose для обоих сервисов.** + Контейнер целиком бежит под uid 1000. Все файлы сразу `slin:slin`, git на хосте без + chown. Лечит корень проблемы одной декларативной строкой на сервис, без нового кода. + +2. **Вариант 2: drop-privileges только для subprocess агента** (`gosu` / `preexec_fn setuid`). + Контейнер остаётся root, агент бежит под 1000. Точечно, но: новый код в горячем пути + launcher, два класса процессов с разными uid в одном контейнере (uvicorn root vs агент + 1000), сложнее отлаживать, выше риск регресса конвейера. Корень (root-владение из самого + uvicorn-процесса при операциях с `/repos`) лечится не полностью. + +3. **Вариант 3 (fallback): chown-хук нормализации прав после стадии** + (`chown -R 1000:1000` worktree/docs). Лечит симптом, не причину; требует root внутри + контейнера (т.е. несовместим с В1) и добавляет хрупкий пост-шаг в каждый переход стадии. + +## Решение + +Принимаем **Вариант 1**. Изменения (применяет Dev на стадии development): + +1. **`docker-compose.yml`** — для **обоих** сервисов (`orchestrator`, `orchestrator-staging`): + - добавить `user: "1000:1000"`; + - **сохранить** `group_add: ["999"]` (МИНА 1 — НЕ удалять); + - изменить target SSH-маунта `/root/.ssh` → `/home/slin/.ssh`, чтобы он совпал с + `HOME=/home/slin`, который форсит launcher. Итог: `/home/slin/.orchestrator-ssh:/home/slin/.ssh:ro`; + - claude-маунты (`/home/slin/.claude`, `/home/slin/.claude.json:ro`) — оставить как есть. + +2. **`src/agents/launcher.py`** — НЕ менять. `HOME=/home/slin` и + `CLAUDE_BIN=/opt/claude-code/bin/claude.exe` остаются валидными под uid 1000 + (`/home/slin` материализуется bind-маунтами; бинарь исполним для всех). Правка + допустима ТОЛЬКО при доказанной поломке запуска под 1000. + +3. **`Dockerfile`** — НЕ менять. Отдельный non-root user внутри образа не создаём: + numeric `user: "1000:1000"` работает без записи в `/etc/passwd`; `safe.directory '*'` + уже покрывает git над bind-маунтом. Правка допустима только если запуск под 1000 + выявит отсутствующий каталог/право. + +### Host-prerequisites (вне кода, выполняет Owner — обязательная процедура) + +Без них переход на uid 1000 ломает конвейер. Фиксируются здесь и в INFRA.md как +обязательная процедура; в git не коммитятся. + +| # | Предусловие | Команда / проверка | Зачем | +|---|-------------|--------------------|-------| +| P-1 (блокер) | uid 1000 читает claude creds | `chown -R 1000:1000 /home/slin/.claude`; проверка `sudo -u '#1000' test -r /home/slin/.claude/.credentials.json` | МИНА 2: иначе preflight (ORCH-044) завернёт весь конвейер | +| P-2 | ssh-ключи читаемы uid 1000 и в новом HOME | ключи в `/home/slin/.orchestrator-ssh` читаемы 1000; маунт ведёт в `/home/slin/.ssh` | деплой по ssh (`DEPLOY_SSH_*`) | +| P-3 | uid:gid рантайма подтверждён | `id slin` → `1000:1000`; `/repos`, `/app/data` уже `1000:1000` | целевые файлы под slin | +| P-4 | рестарт self только в окно тишины | `GET /status` без активных задач перед рестартом prod | self-hosting: общий инстанс с enduro-trails | + +**Выбор способа P-1:** `chown -R 1000:1000 /home/slin/.claude` (рекомендация разведки). +Обоснование: креды и так принадлежат slin по смыслу; chown проще и надёжнее ослабления +read-битов и не оставляет файл world-readable. Маунт `/home/slin/.claude` оставлен rw — +claude CLI может обновлять токен; под uid 1000 после chown это работает. + +## Порядок безопасного внедрения (обязателен) + +1. Применить и проверить **на staging (8501)** end-to-end (артефакты → `1000:1000`, + агент `exit_code=0`, docker.sock и ssh-деплой живы) — `15-staging-log.md`, + гейт `check_staging_status`. +2. Прод-рестарт под новым uid — **только в окно тишины** (P-4). +3. Регресс на хосте: новые tracked-артефакты `1000:1000`, `git pull` под slin без ошибок. + +## Последствия + +**Плюсы:** +- Корень устранён: артефакты создаются под `slin:slin`, ручной `chown` после деплоя не нужен. +- `HOME` теперь консистентен по всем осям (uid = claude = ssh = `/home/slin`); устранён + скрытый рассинхрон SSH-маунта (`/root/.ssh`) с форсимым HOME. +- Минимальная поверхность изменения: декларативный compose, без нового кода в launcher. + +**Минусы / ограничения:** +- Появляется жёсткая привязка к `uid 1000` хоста — задокументирована в INFRA.md; + при переносе на другой хост uid пересматривается. +- Требуются host-prerequisites (P-1…P-4) — часть фикса не закрывается кодом; P-1 — блокер. +- Прод-рестарт self = групповой риск (enduro-trails) → строго окно тишины (P-4), + страховка — staging-гейт (adr-0003). + +**Вне объёма:** массовый `chown` уже существующих `root:root` файлов в истории (разовая +операция Owner, команда описана в INFRA.md); логика конвейера/QG/схема БД — без изменений. +``` diff --git a/docs/work-items/ORCH-040/07-infra-requirements.md b/docs/work-items/ORCH-040/07-infra-requirements.md new file mode 100644 index 0000000..b0716ee --- /dev/null +++ b/docs/work-items/ORCH-040/07-infra-requirements.md @@ -0,0 +1,47 @@ +# 07 — Инфра-требования: ORCH-040 + +Work Item: **ORCH-040** · Решение: [ADR-001](06-adr/ADR-001-run-agents-as-host-uid.md) (Вариант 1) + +> Требования к рантайму/инфре, которые Dev обязан реализовать, а Reviewer — проверить. +> Топология стадий и БД **не меняются**. Меняется только runtime-uid контейнера и target SSH-маунта. + +## R-1 — runtime uid контейнера +- Оба сервиса в `docker-compose.yml` запускаются под `user: "1000:1000"`. +- `group_add: ["999"]` **сохраняется** на обоих (docker.sock через gid 999, МИНА 1). + +## R-2 — SSH-маунт согласован с HOME +- target SSH-маунта = `/home/slin/.ssh` (не `/root/.ssh`) на обоих сервисах. +- Совпадает с `HOME=/home/slin`, форсимым в `src/agents/launcher.py` (L326, L513). +- Источник (`/home/slin/.orchestrator-ssh`) и режим `:ro` — без изменений. + +## R-3 — claude-маунты без изменений +- `/home/slin/.claude` (rw) и `/home/slin/.claude.json:ro` остаются. +- Доступ под uid 1000 обеспечивается host-prerequisite P-1 (chown creds), см. ADR. + +## R-4 — образ и launcher без изменений (по умолчанию) +- `Dockerfile` не меняется (numeric uid не требует записи в `/etc/passwd`; + `safe.directory '*'` уже есть). Изменение допустимо только при доказанной поломке под 1000. +- `src/agents/launcher.py` не меняется (`HOME`, `CLAUDE_BIN` валидны под 1000). + +## R-5 — host-prerequisites (Owner, вне кода) +P-1…P-4 из ADR §«Host-prerequisites» — обязательная процедура. P-1 (доступ uid 1000 к +claude creds) — блокер: без него preflight (ORCH-044) заворачивает конвейер. + +## R-6 — порядок внедрения +1. staging (8501) end-to-end → `15-staging-log.md` / `check_staging_status` зелёный; +2. прод-рестарт self — только в окно тишины (`GET /status` без активных задач, P-4); +3. регресс на хосте: новые tracked-артефакты `1000:1000`, `git pull` под slin без ошибок. + +## R-7 — обновление документации (golden source) +Dev в том же PR обновляет: +- `docs/operations/INFRA.md` — блок «Тома (volumes)» (SSH target `/home/slin/.ssh`) и + явное указание runtime-uid (`user: 1000:1000`) контейнеров; команда разового хост-`chown` + legacy `root:root` файлов. +- `CHANGELOG.md` — запись `fix:`/`refactor:`. +- глобальный [adr-0005](../../architecture/adr/adr-0005-container-runs-as-host-uid.md) уже + заведён архитектором; индекс `docs/architecture/adr/README.md` обновлён. + +## Что НЕ требуется +- Новых томов, портов, env-переменных — нет. +- Изменения API, схемы БД, реестра QG/стадий — нет. +- Multi-node / облачные сервисы — нет (принципы архитектуры). diff --git a/docs/work-items/ORCH-040/10-tech-risks.md b/docs/work-items/ORCH-040/10-tech-risks.md new file mode 100644 index 0000000..d0849ab --- /dev/null +++ b/docs/work-items/ORCH-040/10-tech-risks.md @@ -0,0 +1,19 @@ +# 10 — Технические риски: ORCH-040 + +Work Item: **ORCH-040** · Решение: [ADR-001](06-adr/ADR-001-run-agents-as-host-uid.md) + +| # | Риск | Вероятн. | Влияние | Митигация | +|---|------|----------|---------|-----------| +| TR-1 | **МИНА 2 — claude creds недоступны uid 1000** → preflight (ORCH-044) валит весь конвейер | Средн. | Крит. (блокер) | P-1: `chown -R 1000:1000 /home/slin/.claude` ДО рестарта; проверка `sudo -u '#1000' test -r .../.credentials.json`; staging-прогон ловит до прода (AC-3) | +| TR-2 | **МИНА 1 — потеря доступа к docker.sock** при смене uid → деплой-операции падают | Низк. | Высок. | `group_add: ["999"]` сохраняется на обоих сервисах (НЕ удалять); проверка `docker ps` из контейнера (AC-4) | +| TR-3 | **SSH-маунт ведёт в чужой HOME** (`/root/.ssh`) → ssh-деплой не находит ключи | Средн. | Высок. | R-2: target → `/home/slin/.ssh`, синхронно с форсимым `HOME`; проверка деплой-хука (AC-5) | +| TR-4 | **Рестарт prod self вне окна тишины** роняет конвейер всех проектов (enduro-trails) | Средн. | Крит. | P-4: рестарт только при `GET /status` без активных задач; страховка — staging-гейт adr-0003 (AC-7, AC-9) | +| TR-5 | **Регресс launcher** при невалидном HOME/uid (`/home/slin` отсутствует, claude.exe не исполним) | Низк. | Высок. | `/home/slin` материализуется bind-маунтами; `claude.exe` `+x` для всех; staging end-to-end + `pytest tests/ -q` (AC-6) | +| TR-6 | **Legacy `root:root` файлы в истории** мешают git под slin даже после фикса | Высок. | Средн. | Вне объёма задачи: разовый хост-`chown` делает Owner; команда описана в INFRA.md | +| TR-7 | **Привязка к uid 1000 конкретного хоста** усложняет перенос на другой хост | Низк. | Низк. | Задокументировано в INFRA.md как явное допущение рантайма; пересмотр при миграции хоста | +| TR-8 | **Запись в bind-маунты под 1000** (`/app/data`, `/repos`) при неверных правах хоста | Низк. | Средн. | P-3: `/repos` и `/app/data` уже `1000:1000` (подтверждено разведкой) | + +## Сводный вывод +Основной блокер — TR-1 (creds). Все критичные риски снимаются обязательным staging-прогоном +(adr-0003) ПЕРЕД прод-рестартом и выполнением host-prerequisites P-1…P-4. Изменение +декларативное (compose), без правок горячего кода launcher → низкая поверхность регресса.