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 — брат этой фазы и решает
другую половину: локальный агент и его возможности.