ADR 0180 — Контейнер проекта работает от владельца папки, измеренного api
Статус
Accepted. 2026-09-29 (AT-247, история HS-071, эпик EP-030, раунд 27).
Поверх ADR 0130 (broker, сдерживание в
пять слоёв и ВЫЧИСЛЯЕМАЯ спецификация) и
ADR 0144 (broker обслуживает mounted). Не
редактирует их: добавляет поле в спецификацию и говорит, почему оно не открывает
никакой двери.
Контекст
Измерено в реальном использовании 2026-09-29: docker inspect контейнера проекта
показывал пустой User= (root) и CapDrop=[ALL]. Папка проекта, смонтированная в
/work, — 755 с uid оператора (1000). Root без CAP_DAC_OVERRIDE не
пишет в папку другого uid, а --cap-drop ALL — ровно то, что отнимает эту
возможность. Итог: npm install давал EACCES у всех dev agents и у
QA-automação; работал лишь write_file, потому что пишет процессом engine
на ХОСТЕ, вне контейнера.
Воспроизведено в реальном Docker (node:22-bookworm-slim, --cap-drop ALL, папка
755 uid 1000): от root touch даёт Permission denied; с
--user 1000:1000 пишет. С uid без записи в /etc/passwd HOME
становится /, и npm умирает в //.npm.
У DockerPort пять операций, bind — ОДНА папка помеченного типа, а у типа
нет privileged/cap_add/свободного -v (ADR 0130). Любое исправление должно
в это поместиться.
Решение
EspecificacaoDeContainerполучаетusuario?: { uid, gid }. CLI-адаптер переводит его в--user <uid>:<gid>и в--env HOME=/tmp(константа: uid без passwd получаетHOME=/). Безusuariorunвыходит байт в байт как раньше.- Измеряет api, которая достигает папки (базы, смонтированной по идентичности,
или управляемого корня):
GET /internal/projects/:id/container-specначинает возвращатьusuarioDaPasta(statпапки;null, если не измеряет, если проектrunnerили если владелец root). Broker составляетusuarioИЗ ЭТОГО контекста — никогда из тела запроса: уstartнет тела, аpedidoDeExecValidadoигнорирует любое лишнее поле. - Broker перепроверяет (
usuarioValidado): целые в 1..2^31-1,0отклоняется с указанием поля. Root — это ОТСУТСТВИЕ; явный--user 0был бы ослаблением, которого никто не решал. execне получает поля:docker execнаследует пользователя контейнера.- Действует для
containerиmounted(api измеряет папку, которую каждый монтирует); режимrunnerне проходит через broker и не меняется.
Последствия
- Не открывает двери сдерживания. У процесса становится МЕНЬШЕ власти над
хостом, чем раньше (не-root вместо root),
--cap-drop ALLсохраняется, и нет поля, в которое вызывающий записал бы пользователя, capability, mount или сеть. - Второй рассмотренный вариант, вернуть
DAC_OVERRIDE/CHOWN/FOWNER, отвергнут: ослабляет сдерживание, закреплённое ADR 0130, и сохраняет root. - Третий, чтобы broker сам делал
stat, потребовал бы монтировать папки проектов в broker (сегодня у него лишь сокет) — больше поверхности в процессе, эквивалентном root. api уже их достигает. - Это не BREAKING для оператора: нечитаемая папка или папка root попадает в "как
всегда". УЖЕ созданный контейнер не меняется, пока его не пересоздадут (
--user— частьrun); следующийcontainer_startуже поднимается правильно. - Владелец папки ≠ тот, кем должен быть dev agent, — это не различие, которое делает продукт: цель — писать там, где оператор уже пишет.
- Не измерено: Docker Desktop (macOS/Windows), где отображение uid
bind-mount иное; Docker rootless; папка на NFS с
root_squash.