# 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/схема БД — без изменений.
```
