ADR 0065 — Контейнер на проект: граница перестаёт быть политикой
Статус
Принят, с объявленным срезом. Половина, которую поставляет этот ADR, — артефакт Arquiteto, барьер и граница внешнего эффекта — реализована и доказана тестом. Половина, которую он не поставляет, — подготовка, остановка, перезапуск и очистка контейнера — объявлена в «Чего этот ADR НЕ делает», с причиной. Срез касается объёма, а не аргумента: архитектурное решение действует целиком, и именно оно диктует, что строит следующая фаза.
Этот ADR пересматривает ADR 0055, который остаётся принятым и не редактируется. 0055 говорит о себе в разделе последствий, что он — политика, а не изоляция, и фиксирует контейнер на проект как явный незакрытый вопрос. Именно этим вопросом занимается этот документ.
Контекст
Долг словами документа, который его создал
O que este ADR NÃO resolve. Escopo é política, não isolamento. Enquanto o monorepo do Brabo estiver montado em
/workspacedentro do container que executa os comandos, a fronteira depende da política acertar. — ADR 0055
И заметка о принятии того же документа, измеряющая оставшуюся дыру:
нормализация пути лексическая, .. проваливается, а symlink изнутри
проекта, указывающий наружу, не обнаруживается. Закрыть это — не написать
правило получше, а иметь стену.
Сегодня агент выполняется в том же контейнере, что и monorepo Brabo. То, что
отделяет его от кода платформы, которая его выполняет, — сравнение строк в
decide.ts.
Находки, которые не сходятся
FASE 13b оставила две открытые находки, и их аргумент ценнее
списка прогонов, которые их породили (docs/explanation/achados-execucao-real.md):
- Z и AD — allowlist глаголов не сходится. Глагол, форма и вызов —
разные пространства:
curl,wget,python -c "urllib..."и скрипт.sh, делающий что-либо из трёх, — это один и тот же исходящий трафик, записанный четырьмя способами. Прогоны 6, 7 и 8 застряли по одному на каждом. - AE — агент QA пытается исправить код, который оценивает, вопреки собственному prompt, сдерживаемый двумя независимыми барьерами (allowlist и область).
Вывод FASE 13, написанный до появления этой фазы, — тот же, к которому приходит этот ADR: путь к автономии не проходит через ослабление политики.
Что решил пользователь
"Cada projeto tenha sua própria infra apartada, ou seja, subirá via container por cada projeto, isolando assim o terminal e dando permissão total a ele; o code somente é liberado após definição do arquiteto, pois ele que definirá qual tipo de container subirá aquele código, pois será o decisor de melhor oportunidade para qual imagem ser a melhor."
И об охвате полного разрешения:
"Agente livre para o que quiser desde que não seja comandos de git ligado ao deploy e ao PR — estas ações ainda devem ser humanas."
Две фразы, три решения: кто выбирает образ, когда открывается Code и где заканчивается свобода.
Решение
1. Образ проекта — АРТЕФАКТ Arquiteto
artifact.project_image в event log, с image, rationale, network и
resources. Версионируемый — пересмотр означает выдачу новой версии, а действующий —
с наибольшим version.
Артефакт, а не конфигурация, потому что кто выбирает образ, тот выбирает, что
агент может делать: какой runtime существует, какой менеджер пакетов, какой
компилятор. Это архитектурное решение того же калибра, что module_map, а у
архитектурного решения есть автор, дата и обоснование. У переменной окружения нет
ни одного из трёх.
Без таблицы, и не из экономии. Event log уже даёт три свойства, которые
нужны решению, — неизменяемое, версионируемое, с автором — и именно там уже живут
artifact.module_map и artifact.business_rule. Таблица дала бы то же
с возможным UPDATE, а UPDATE в архитектурном решении — то, как оно перестаёт
быть проверяемым.
Явный тег, latest отклонён. Артефакт, говорящий node:latest, ничего не
описывает: контейнер марта и сегодняшний — разные образы с одним
именем, и аудит начинает лгать.
Потолок ресурсов, который отклоняет, а не урезает. Запрос выше максимума — это 400 с причиной, никогда не молчаливая обрезка — артефакт, обещающий больше, чем получает контейнер, лжёт тому, кто его проверяет.
2. Пока Arquiteto не решил, Code не открывается
Барьер — буквальный приказ пользователя, и его причина продуктовая: контейнер —
то, что придаёт смысл чтению кода там, — читать, чтобы потом запускать, собирать, исправлять.
Поверхность чтения из ADR 0060
отвечает 409, пока состояние sem_decisao, с сообщением, говорящим,
чего не хватает.
409, а не 403: с запросившим и его разрешением всё в порядке — ресурса
в этом состоянии ещё не существует. А проверка живёт в той же воронке, что и
сдерживание пути (alvo), а не в четырёх маршрутах, по причине
RN-092: проверка, продублированная в четырёх вызывающих, — это
проверка, которая однажды разойдётся в одном из них.
3. Граница: внутри свободно, снаружи — человек
Внутри контейнера агент свободен — читать, писать, устанавливать, собирать, тестировать, запускать. Именно это закрывает Z и AD, и это единственный известный способ их закрыть: стена заменяет перечисление.
Снаружи по-прежнему человек. Три эффекта пересекают стену и выходят в мир —
git push, открытие PR и деплой, — и команда terminal, которая их вызывает,
запрещается, с сообщением, говорящим, какое типизированное действие использовать.
deny, а не require_approval, и это часть, требующая аргумента. У каждого
из этих эффектов уже есть типизированный путь (git_push, pr_open, git_merge), который
рождается proposed_action, имеет собственную минимальную роль, выполняется платформой и
оставляет в event log, что было отправлено и куда. Terminal был бы
второй дверью для того же эффекта, без каких-либо из этих гарантий: лог сказал бы
«выполнилась команда». А require_approval не хватило бы, потому что есть «всегда
разрешать» — один клик записал бы шаблон в allow, и вторая дверь осталась бы
открытой навсегда. deny побеждает allow на любом этапе, и поэтому
это правильная форма этого правила: это не настраиваемое предпочтение, а место, где
заканчивается контейнер.
Запрет не отнимает силу у агента — он перенаправляет. Так dev agent
всегда и делал (agent_io.ex предлагает git_push); меняется то, что теперь это
гарантировано, а не просто договорено. А merge в защищённую ветку остаётся ручным по
RN-014, нетронутой.
4. Сеть — позиция КОНТЕЙНЕРА, решаемая один раз, — не команда за командой
Это решение, которое производит запрошенный отдельный вердикт о сети, и оно противоположно тому, что подсказывает интуиция.
Соблазном было бы добавить исходящий трафик в allowlist: запретить curl, wget,
npm install. Это ровно та попытка, о которой Z и AD доказали, что она не сходится.
Allowlist исходящего трафика имел бы ту же форму, тот же размер и ту же судьбу,
что allowlist глаголов.
Поэтому сеть решается один раз, в артефакте, на границе, которую понимает ядро:
network: none — значение по умолчанию, и именно оно делает «внутри агент свободен»
безопасной фразой — свободен в месте без выхода. egress — законный запрос (стек,
скачивающий зависимости, без него не работает), Arquiteto объявляет его с
обоснованием, а авторизует пользователь при подготовке — по той же
причине, по которой он авторизует потолок параллелизма (ADR 0053):
выход в интернет — это расходы и поверхность.
Расходы трактуются так же: cpus, memoryMb и pidsLimit в артефакте,
с жёстким потолком. pidsLimit заслуживает заметки — именно он сдерживает fork bomb, не завися
ни от какого allowlist глаголов, и это самый чистый пример того, что меняется, когда
граница перестаёт быть лексической.
Потолки токенов не меняются: по-прежнему проект, сессия и task.
Чего этот ADR НЕ делает
Жизненный цикл контейнера (25b). Подготовка, остановка, перезапуск, очистка; что происходит при смене образа; что переживает перезапуск; worktree агента, начинающий жить внутри контейнера.
Причина конкретна и не в отсутствии проекта: состоянию контейнера нужна таблица. Id контейнера, статус, используемый образ, когда поднят, какой версии артефакта соответствует — ничто из этого не является событием, это изменяемое состояние, и втиснуть его в event log значило бы использовать неправильный инструмент, потому что правильный был занят. Единственный слот миграции этой волны принадлежит другой фазе.
Поставить половину подготовки было бы хуже, чем не поставить: контейнер, который поднимается и не перезапускается, хуже, чем никакого — он накапливается, никто не знает, чей он, и первый выбранный образ на практике становится постоянным.
Честное следствие среза: половина «внутри агент свободен» пока
не вступила в силу. Политика terminal из ADR 0055 остаётся ровно какой была —
область пути, узкий allowlist, cd, ослабленный внутри области. Ослабить
до появления стены значило бы повторить ошибку, которую этот документ пришёл исправить,
а FASE 13 уже записала вывод: путь к автономии не проходит через
ослабление политики. Эта фаза поставляет ВНЕШНЮЮ половину границы — ту,
которая должна действовать раньше, а не позже.
Последствия
Что улучшается сейчас. Решение об образе существует, у него есть владелец, версия и обоснование; барьер Code реален и протестирован в четырёх маршрутах; а вторая дверь для push/PR/deploy закрылась — до того, как она реально появилась, а это правильный момент, чтобы закрыть дверь.
Что меняется для оператора. У существующего проекта нет решения об образе,
поэтому вкладка Code начинает отвечать 409, пока не отработает Arquiteto. Это
наблюдаемое изменение поведения поверхности, которая только что появилась (ADR 0060), и
поэтому это изменение рождается в breaking/: кто уже пользовался вкладкой, должен
знать, почему она закрылась.
Что теряется. Ничего из выполнения: ни одна работавшая команда не перестаёт
работать, потому что git push через terminal никогда не был тем, как dev agent отправляет.
Теряется возможность настроить ярлык через terminal для
push/PR/deploy — и потеря этого и есть суть.
Что остаётся в долгу, измеренное. Symlink изнутри, указывающий наружу, по-прежнему не обнаруживается. Этот ADR не закрывает этот вектор; он решает, как его закрыть (стена), и поставляет решение об образе, которое нужно стене. Пока контейнер не поднимется, слабость 0055 остаётся в силе, и это записано здесь, а не выдано за решённое.
Рассмотренные альтернативы
Ослабить политику сейчас и поднять контейнер потом. Отклонено с самым сильным аргументом, какой есть у проекта: это буквально вывод FASE 13 наоборот. Свобода без стены — это дыра, а не решение.
Allowlist исходящего сетевого трафика. Отклонено из-за Z и AD: у него была бы та же форма и та же судьба, что у allowlist глаголов. Сеть — свойство контейнера.
Образ как конфигурация проекта (колонка, .env, экран Configurações).
Отклонено, потому что отнимает у Arquiteto решение, которое ему дал пользователь, и потому что
у конфигурации нет обоснования. Обязательный rationale — то, что делает решение
пересматриваемым, а не археологическим.
Таблица project_containers сейчас. Это правильная конструкция для состояния
контейнера, и именно поэтому она остаётся для волны со слотом
миграции. Импровизировать состояние в event log, чтобы не ждать, породило бы
исправляющую миграцию сразу следом.
require_approval в terminal вместо deny. Отклонено из-за «всегда
разрешать»: он записывает шаблон в allow, и одного клика хватило бы, чтобы вторая
дверь навсегда осталась открытой.
Ссылки
- ADR 0055 — политика, которую пересматривает этот документ и которая сама объявила этот незакрытый вопрос.
- ADR 0060 — вкладка Code, чей барьер закрывает это решение.
- ADR 0053 — расходы авторизует тот, кто отвечает за проект; сеть следует тому же критерию.
- ADR 0045 — worktree принадлежит АГЕНТУ, а не task; именно он начинает жить внутри контейнера.
- RN-014, RN-092, RN-105, RN-106.
docs/explanation/achados-execucao-real.md— находки Z, AD и AE.apps/api/src/domain/containers/project-container.ts,apps/api/src/domain/actions/external-effect.ts,apps/api/src/application/use-cases/containers/,apps/engine/lib/engine/harness/tools/choose_project_image.ex.