Перейти к основному содержимому

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 /workspace dentro 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.