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

ADR 0146 — База, согласованная в bootstrap, и mounted как локальный режим по умолчанию

Контекст​

ADR 0141 создал BRABO_PROJECTS_BASE: одну базу на хосте, монтируемую по идентичности ($X:$X) в api и engine, где живут папки каждого проекта в режиме Pasta montada. Он решил задачу, которую ставил перед собой, — исчезла строка bind-mount на проект в compose, а вместе с ней и перезапуск, который убивал каждый находящийся в полёте ход агента, чтобы подключить ОДИН проект.

Но он оставил переменную отсутствующей по умолчанию, и следствие этого — причина существования этой фазы. Без переменной api сообщает projectsBase: null, и мастер создания не предлагает этот режим (docs/adr/0141-base-unica-dos-projetos-montados.md:68-72). Нормальное состояние только что клонированной установки, следовательно, таково: единственный режим, который кладёт код в папку, открываемую пользователем в редакторе, не появляется на экране. У container нет папки, которую он видит; runner требует бинарника, chmod, сопряжения и — начиная с ADR 0145 — запущенного Docker на его машине. У продукта три режима выполнения, и ни один из них не даёт, без работы по настройке, которую никто не просил пользователя делать, самого простого, чего он ожидает: увидеть файлы.

Не хватает жеста согласия. Выбор места, где будут жить проекты, — решение владельца машины, происходит один раз и происходит на хосте — не внутри контейнера, не через api, не через веб-интерфейс. У продукта уже есть ровно одно место с этими тремя свойствами: pnpm bootstrap (scripts/dev/bootstrap.sh), меню, которое уже создаёт .env, исправляет владельцев папок и перенастраивает Ollama.

Есть второй факт мира, изменившийся со времён ADR 0130, который эта фаза не может игнорировать. Broker поднимается под profiles: ["container-broker"] и не поднимается по умолчанию, и зафиксированное обоснование дословно (docs/adr/0130-broker-de-container.md:131-135):

Nada o chama para escrever ainda; dar acesso ao Docker do host a toda máquina de desenvolvimento em troca de nada seria uma mudança de postura sem contrapartida.

Предпосылка этой фразы — «ничто его не вызывает» — перестала быть верной. ADR 0133 дал container_start реального вызывающего; ADR 0136 добавил stop и remove; ADR 0134 добавил exec. А ADR 0144 заставил mounted поднимать контейнер через broker. Проект mounted в локальном compose без включённого profile заканчивается не ошибкой конфигурации: он заканчивается BrokerIndisponivelError, после того как пользователь выбрал режим, создал проект и поговорил с Arquiteto.

Решение​

1. База по умолчанию — $HOME/projetos-brabo, и записывает её Node, а не bash​

Предлагаемый путь — $HOME/projetos-brabo, всегда раскрываемый в абсолютный перед записью. ~ по-прежнему запрещён, и не из вкуса: Compose не раскрывает его на нужной стороне, и его запись создаёт папку с именем ~ (.env.example:225-229).

Под $HOME, потому что именно это Docker Desktop расшаривает по умолчанию — /Users на macOS, профиль пользователя на Windows. Значение по умолчанию, требующее, чтобы пользователь открыл настройки Docker, прежде чем оно заработает, — не значение по умолчанию; это замаскированный вопрос. Не $HOME/brabo, потому что база не может быть (и не может содержать) checkout самого Brabo — scripts/dev/preflight.mjs:316 уже отказывается подниматься в этом случае, — и тот, кто клонирует продукт туда, попал бы ровно в этот отказ.

И не $HOME/brabo-projetos, несмотря на то что это самое естественное имя, потому что .env.example:202 уже использует этот путь как закомментированный пример PROJECT_WORKSPACES_HOST_DIR — корня workspaces, которыми управляет ПРОДУКТ. Это разные переменные с противоположными владельцами, и ADR 0141 отказался их смешивать именно потому, что коллизия «ломается так, что это трудно увидеть»: <base>/loja столкнулся бы с проектом container, чей workspace_dir_name тоже loja, и bootstrap сделал бы git init внутри чужого проекта (.env.example:231-237). Предложить по умолчанию путь, который документация уже предлагает для другой переменной, значило бы войти в эту ловушку через парадную дверь.

Имя, однако, лишь уклонение. Защита — это механизм: скрипт отклоняет любую базу, которая равна, содержит или содержится в PROJECT_WORKSPACES_HOST_DIR, переиспользуя dentroDe из scripts/dev/base-de-projetos.mjs — ту же функцию, на которой уже держится baseSobrepoeOCheckout. Проверка действует для введённого пути, а не только для значения по умолчанию; хорошо выбранное значение по умолчанию лишь избавляет от того, чтобы отказ был первым, что увидит пользователь.

Записывает scripts/dev/consentir-base.mjs, новый, родственник scripts/dev/base-de-projetos.mjs (который уже содержит чистую логику нормализации и перекрытия), вызываемый пунктом меню bootstrap.sh.

Это не bash, и обе причины измерены, а не эстетические. Первая: у bootstrap.sh нет помощника для записи переменной в .env — garantir_env (scripts/dev/bootstrap.sh:91) лишь копирует .env.example, когда файла нет. Единственный помощник, обновляющий ключ на месте, сохраняя комментарии и дописывая в конец, когда ключа нет, — это escreverEnv (scripts/dev/preflight.mjs:128). Вторая, и решающая: пункт меню не может спрашивать. Каждая команда меню выполняется в фоне со stdin из /dev/null (scripts/dev/bootstrap.sh:770), и комментарий над строкой объясняет почему — без этого всё, что читает stdin, крадёт стрелки у пользователя, а команда, ждущая ответа, зависает, так и не показав вопроса. Шаг согласия, написанный на bash внутри меню, был бы по построению шагом, который не может согласиться.

Уже присутствующее значение никогда не перезаписывается без подтверждения. И без TTY скрипт сообщает и ничего не делает — дословный прецедент perguntarUsoDoOllama (scripts/dev/preflight.mjs:208), который без терминала применяет значение по умолчанию и предупреждает, вместо того чтобы зависнуть. Скрипт согласия, который «соглашается» сам в CI-конвейере, — отрицание самого слова.

Папка создаётся через mkdir -p после того, как пользователь принял значение по умолчанию или ввёл другой путь, никогда раньше.

2. Общий доступ Docker Desktop доказывается МОНТИРОВАНИЕМ​

На macOS и Windows папка вне списка общего доступа Docker Desktop монтируется пустой или заставляет docker run падать — а весь режим Pasta montada зависит от этого монтирования. Проверка обязательна, и её форма — это вердикт:

Доказывается монтированием, никогда чтением конфигурации Docker Desktop. Тест — docker run --rm -v <base>:/sonda <imagem já presente> ls /sonda. Чтение settings.json было бы чтением намерения, объявленного в недокументированной структуре, которая меняется между версиями и между macOS и Windows и которая описывает то, что пользователь настроил, — а не то, что сделает daemon. Монтирование измеряет свойство. Это та же мерка, которую продукт уже применяет к providers: capability объявляется, только когда доказана выполнением, никогда не считывается из официальной документации (ADR 0041/0042).

Только на macOS и Windows, по process.platform. На Linux нет списка общего доступа: проверка — существование и право на запись, и ничего больше. Это делает проверку второй платформенной охраной продукта — первая — validarDirDentroDoHomeNoLinux (apps/runner/src/guard.ts:240), которая ограничивает --dir пределами $HOME только на Linux. Обе симметричны по принципу: каждая ограничивает лишь там, где ограничение что-то значит. Стоит зафиксировать, что сегодня ни один dev-скрипт не определяет платформу — ни bootstrap.sh, ни preflight.mjs не вызывают uname и не читают process.platform, — поэтому этот ADR вводит шаблон, и именно поэтому он записан здесь, а не становится if без владельца.

База вне списка: отказ с действенным сообщением, называющим проверенный путь и где его добавить (Docker Desktop → Settings → Resources → File sharing). Никогда не записывает переменную, никогда не монтирует молча. Принятая и не расшаренная база создаёт проект, который api принимает, создаёт и показывает, — и чей контейнер поднимается с пустой папкой, далеко от экрана, где был сделан выбор. Это тот же урок RN-170 и причина, по которой отказ идёт до записи.

3. Broker выходит из profiles в ЛОКАЛЬНОМ compose, и только в нём​

Вердикт: выходит в docker/docker-compose.yml; остаётся под profiles: ["container-broker"] в docker/docker-compose.prod.yml.

Исходное обоснование (docs/adr/0130-broker-de-container.md:131-135, процитированное в Контексте) имеет две половины, и выживает только вторая. «Ничто его не вызывает для записи» ложно со времён ADR 0133/0134/0136 — у broker четыре реальных вызывающих. А «в обмен на ничто» перестало описывать обмен: с этим ADR, делающим mounted локальным режимом по умолчанию (пункт 4), и ADR 0144, поднимающим mounted через broker, выключенный profile становится тем, что отделяет режим по умолчанию от завершения в BrokerIndisponivelError. Встречная выгода существует, и это обычный путь.

Асимметрию локальное↔production поддерживает то, что сокет означает на каждой стороне. У того, кто запускает pnpm dev, уже есть сокет Docker — он запускает docker compose с daemon собственного хоста. Передача его broker не даёт ничего, чем оператор ещё не обладает; предельная поверхность — HTTP самого broker, который не публикует порт (docker/docker-compose.yml:486-487), живёт в сети internal: true, до которой дотягивается только api, и сравнивает BRABO_SERVICE_TOKEN за постоянное время. В production расчёт противоположный: сокет — настоящая граница привилегий, оператор — не разработчик, и ничто в этой фазе не меняет режим по умолчанию production-установки. Поэтому profile остаётся там.

Пять слоёв сдерживания не затрагиваются (docs/adr/0130-broker-de-container.md:171-176). Этот ADR меняет то, когда работает broker, но никогда то, что он принимает: он по-прежнему получает projectId и одну из пяти операций, по-прежнему обращается к api за решением Arquiteto и по-прежнему не имеет ни одного параметра, куда можно было бы написать privileged, cap_add, network: host или свободный -v. Сдерживанием никогда не был profile.

DOCKER_GID/group_add остаётся хрупкой точкой окружения, и теперь она действует для каждой машины разработчика, а не только для тех, кто включал profile, — см. Последствия.

4. При настроенной базе mounted — режим мастера по умолчанию​

База присутствует: mounted предвыбран, с путём, предложенным как <base>/<slug> и редактируемым. База отсутствует (projectsBase: null): mounted не предлагается, и container остаётся режимом по умолчанию.

Ни одна из двух половин не является новым правилом. «Не предлагать без базы» — это RN-500 и docs/adr/0141-base-unica-dos-projetos-montados.md:68-72; «предлагать <base>/<slug>» — это RN-501 и ADR 0142, который отложил проверку диска именно для того, чтобы мастер мог предложить путь, который ещё не существует. Этот ADR фиксирует, что обе так и не были реализованы на клиенте: у GET /workspaces/:workspaceId/projects-base нет ни одного вызывающего в web, а карточка режима предлагается безусловно. Фаза делает их истинными; она их не изобретает.

Новое — это предвыбор. Он пересматривает значение по умолчанию ADR 0072, где container — «нормальный», а локальный режим — escape hatch, — и пересматривает его только для локальной установки, где база согласована человеком на хосте. Enum projectExecutionModeEnum не меняется, ни один новый режим не рождается, и конвертация между режимами (ADR 0111) остаётся идентичной.

5. Скрипт согласия версионируется в репозитории​

scripts/dev/consentir-base.mjs живёт в репозитории, входит в diff, проходит ревью и является одним и тем же файлом для всех. Никогда не генерируется динамически api, никогда не скачивается во время выполнения.

Это та же цепочка поставок, которая уже закрепляет каждый сторонний action по commit SHA и проверяет каждый скачанный бинарник через sha256sum -c: скрипт, который настраивает хост пользователя, создаёт папку и пишет в .env, — это именно тот артефакт, чьё происхождение должно быть проверяемым. Скрипт, сгенерированный api, был бы кодом, который выполняется на машине пользователя и который никто не проверял, доставленным по каналу, целостность которого продукт не проверяет.

Явное следствие: api не получает маршрута, выдающего скрипт для хоста. Существующий прокси GET /runner-releases/binary не является прецедентом этого — он отдаёт уже опубликованный в GitHub Releases артефакт, а не генерирует его; а отсутствие подписи на этом пути — это ровно BRB-005, открытый пункт backlog, а не шаблон для подражания.

Последствия​

Пользователь, который клонирует Brabo и запускает pnpm bootstrap, получает согласованную базу, проверенную против того Docker, который у него реально есть, и мастер, по умолчанию предлагающий режим, кладущий код в папку, которую он открывает в редакторе. Сессии 2, 3 и 4 этой фазы строят, в этом порядке, шаг bootstrap, поведение мастера и изменение profile.

Чего этот ADR НЕ касается. decide.ts и абсолютные потолки остаются идентичными — ничто здесь не является действием агента. proposed_action по-прежнему источник каждого внешнего эффекта. Неизменяемость event log не затрагивается. Барьер образа действует для трёх режимов, как его оставил ADR 0135, и проект mounted без принятого artifact.project_image по-прежнему отвечает 409 во вкладке Code. У broker по-прежнему пять операций и вычисляемая спецификация, никогда не принимаемая (ADR 0128/0130), — шестая операция остаётся продуктовым решением с собственным ADR.

Объявленная стоимость 1: DOCKER_GID становится важен для всех. Пока broker поднимался только под profile, неверный group_add затрагивал тех, кто включил profile намеренно. Теперь он поднимается по умолчанию в локальном compose, и неверный GID становится ошибкой прав на сокете для любого разработчика. Значение по умолчанию ${DOCKER_GID:-999} верно во многих дистрибутивах Linux и неверно в других; сессия 4 должна сообщать об этом в preflight.mjs вместе с состоянием базы, а не позволять сбою проявиться только тогда, когда кто-то предложит container_start.

Объявленная стоимость 2: база одна, и она навигируема. ADR 0141 уже объявил обе вещи, и этот ADR наследует их, не улучшая: symlink под базой, указывающий наружу, разрешается по-разному с двух сторон, а код вне базы требует перемещения папки. .env.example:239-241 уже указывает выбирать выделенную папку — всё под базой доступно изнутри контейнеров продукта, — и сообщение шага согласия повторяет это указание, а не предполагает, что его прочли.

Объявленная стоимость 3: зонд общего доступа стоит одного docker run. Он запускается один раз, при жесте согласия, и переиспользует уже присутствующий образ, чтобы не тянуть из сети. Если локального образа нет ни одного, шаг говорит об этом и просит пользователя один раз запустить pnpm dev заранее — никогда не скачивает образ сам, чтобы иметь возможность протестировать.

Принятые пробелы, с источником. Учётные данные git по-прежнему отбрасываются на пути docker exec в режиме runner (apps/runner/src/index.ts:318-320; у порта Docker нет поля env, по замыслу ADR 0130) — эта фаза его не закрывает. Symlink изнутри проекта, указывающий наружу, по-прежнему не обнаруживается decide(), который чист и не делает I/O (docs/adr/0055-escopo-de-caminho-na-politica-de-terminal.md:9-15); apps/runner/src/guard.ts:9-31 остаётся best-effort по объявленному инварианту. BRB-005 (подпись опубликованных артефактов) и BRB-031 (ручной chmod +x) остаются открытыми: эта фаза их затрагивает и не решает.

Windows остаётся вне проверенного пути. Зонд общего доступа запускается в Windows, потому что process.platform его включает, но продукт по-прежнему проверяется только на Linux и macOS (.env.example:198-201), и ничто в этой фазе этого не меняет.

Ссылается на 0130 (profile и пять слоёв), 0141 (база, которую этот ADR начинает согласовывать), 0142 (отложенная проверка, делающая предложение возможным), 0144 (mounted поднимается через broker) и 0072 (значение по умолчанию, которое пункт 4 пересматривает для локальной установки). Ни один из них не редактируется. 0147 — брат этой фазы и решает другую половину: локальный агент и его возможности.