architect(ET): auto-commit from architect run_id=176
All checks were successful
CI / test (push) Successful in 14s
All checks were successful
CI / test (push) Successful in 14s
This commit is contained in:
@@ -9,6 +9,7 @@ Per-work-item решения живут в `docs/work-items/<id>/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.
|
||||
|
||||
42
docs/architecture/adr/adr-0005-container-runs-as-host-uid.md
Normal file
42
docs/architecture/adr/adr-0005-container-runs-as-host-uid.md
Normal file
@@ -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`.
|
||||
@@ -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/схема БД — без изменений.
|
||||
```
|
||||
47
docs/work-items/ORCH-040/07-infra-requirements.md
Normal file
47
docs/work-items/ORCH-040/07-infra-requirements.md
Normal file
@@ -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 / облачные сервисы — нет (принципы архитектуры).
|
||||
19
docs/work-items/ORCH-040/10-tech-risks.md
Normal file
19
docs/work-items/ORCH-040/10-tech-risks.md
Normal file
@@ -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 → низкая поверхность регресса.
|
||||
Reference in New Issue
Block a user