FASE 29 — установка одной строкой
Этот документ — срез фазы: к чему она стремится, от чего отказывается и в каком порядке. Решения находятся в ADR 0149, 0150, 0151 и 0152 — то, что есть здесь и нет там, — это намерение, а не решение.
Проблема
Сегодня Brabo устанавливается тремя разрозненными жестами, и ни один из них не знает о других:
| жест | где | что требуется от устанавливающего |
|---|---|---|
| поднять сервисы | docker compose -f docker/docker-compose.prod.yml | вручную экспортировать пять секретов (RN-114, ADR 0059) и иметь checkout |
| поставить runner на машину | браузер (ADR 0118) | скачать бинарник, ручной chmod +x (BRB-031), повторять для каждого проекта |
| выбрать, где живёт код | pnpm bootstrap (ADR 0146) | иметь checkout разработки — меню вне его не существует |
FASE 28 письменно отказалась от установщика одной строкой, и причина остаётся
в силе: «установщик одной строкой без подписи хуже текущей
загрузки» (fase-28-pasta-do-usuario.md, BRB-005).
curl | sh, скачивающий неподписанный артефакт, превращает любую
компрометацию канала в выполнение кода на машине устанавливающего — а
продукт сегодня ничего не подписывает из того, что публикует.
Поэтому эта фаза начинается с подписи, а не с установщика. Порядок — не предпочтение: без ADR 0149 ADR 0150 был бы тем, от чего FASE 28 отказалась.
Устройство, в четырёх слоях
У каждого слоя один механизм, и ни один не изобретает нового полномочия:
| слой | где | механизм |
|---|---|---|
| происхождение | release.yml, build-runner-binaries.yml | cosign keyless (OIDC Actions) для образов и бинарников; подписанный checksums.txt |
| установка | версионируемый install.sh, получаемый из GitHub Releases | проверяет собственное происхождение, обнаруживает уже существующее, спрашивает, записывает маркер |
| данные | docker/backup/ | резервное копирование и восстановление, работающие против compose, без кластера |
| живая папка | guard.ts runner | одна согласованная база; каждый проект — подпапка внутри неё |
Четвёртый слой существует по физической причине, которую FASE 28 уже назвала:
bind mount не пересекает сеть и не достаёт до пути вне смонтированного.
brabo-runner — процесс, который уже живёт на машине пользователя; не хватает
не агента, а того, чтобы он рождался с базой, а не с отдельным путём.
Согласие на конфигурацию ≠ одобрение действия
Пункт, который должны объявить все четыре ADR и который определяет устройство установщика:
Выбор базы, запись .env, создание папки и установка сервиса —
это конфигурация, на которую пользователь согласился в своём терминале, а не агент,
просящий разрешения действовать. Ничто из этого не становится proposed_action, а пропуск этого через неё
попал бы в область пути ADR 0055,
превратился бы в очередь рутинных одобрений и разъел бы как раз тот потолок, который придаёт
смысл клику.
Следствие действует в обе стороны, и именно оно отделяет эту фазу от обычного установщика: согласие не отменяет проверки. Пользователь соглашается с базой и с установкой; он не может согласиться с происхождением бинарника, который не может проверить. Поэтому проверка подписи принадлежит механизму, а не вопросу на экране, — и провал в ней — это именованный отказ, а не предупреждение, которое можно принять кликом.
Нумерация — подсчитанная, а не унаследованная
Счётчики в прозе репозитория продолжают известный дрейф: frontmatter
docs/adr/index.mdобъявляет число ADR, которое диск не подтверждает. Ни одна сессия этой фазы не доверяет числу, записанному в прозе.
Метод, который каждая сессия этой фазы повторяет перед резервированием любого номера, — и который FASE 28 уже выполняла, дополненный недостававшей второй половиной:
# ADRs e RNs em dev — o piso
git ls-tree --name-only dev docs/adr/ | grep -oE '^[0-9]{4}' | sort -n | tail -1
git grep -h -E '^### RN-' dev -- 'docs/business-rules.md' 'docs/business-rules/*.md' \
| grep -oE 'RN-[0-9]+' | sed 's/RN-//' | sort -n -u | tail -1
# o que está EM VOO — branches locais e remotas ainda não mergeadas
for b in $(git branch --format='%(refname:short)'; git branch -r --format='%(refname:short)'); do
git merge-base --is-ancestor "$b" dev 2>/dev/null && continue
echo "$b: $(git ls-tree --name-only "$b" docs/adr/ | grep -oE '^[0-9]{4}' | sort -n | tail -1)"
done
Измерено в начале сессии 1 против dev: 147 ADR (максимальный 0147) и
382 RN (максимальная 521). И в полёте, в тот же момент: ADR 0148, выделенный
двумя ветками одновременно, и RN 510, 522 и 523 — 522 и 523
тоже дублированы между двумя ветками каждая.
Диапазон, зарезервированный для фазы, — с RN-524 по RN-538, а ADR — 0149, 0150, 0151 и 0152. Прыжок через 510/522/523 и через 0148 намеренный.
Фиксация трёх наблюдённых коллизий — это суть, а не деталь: FASE 28
написала, что подсчёт только dev вернул бы уже выделенный номер, и эта
сессия обнаружила тот же режим отказа, происходящий трижды одновременно.
Подсчёт dev — условие необходимое, но не достаточное; вторая половина
команды выше — то, чего не хватало, и поэтому она входит сюда.
Все три были разрешены позже, и критерием была дата. Кто выделил первым, тот сохраняет номер: три ветки от 2026-09-06 сохранили
ADR 0148,RN-522иRN-523, а ветки от 2026-09-08 перенумеровались вADR 0153,RN-539иRN-540— выше диапазона этой фазы, вне досягаемости чего-либо в полёте. Одна из них уже была второй перенумерацией той же RN: она родилась 522, стала 523, чтобы уйти от коллизии, и попала в другую — потому что тот просмотр охватил удалённые ветки, а ждавшая её была локальной.
Стоит повторить, почему коллизия живёт так долго: docs:build не ловит
дублированный якорь. onBrokenAnchors: 'throw' проваливает ссылку на якорь,
которого не существует; при двух одинаковых якорях Docusaurus разрешает первый, а
второй становится недостижимым, молча.
Что измерение исправило в срезе
План этой фазы родился из среза, написанного до измерения. Пять его пунктов не пережили встречи с диском, и они зафиксированы, потому что план, который переписывается, чтобы совпасть с результатом, перестаёт учить:
workspace.verifiedне существует — ни как событие, ни как сообщение канала, ни как маршрут. Существуетworkspace_confirm, и он однонаправленный:terminal_channel.ex:356-373отвечает{:noreply, socket}и ничего не отправляет обратно runner. Пара с ответом имеет в репозитории другую форму —exec/exec_result.- Пункт 5 ADR 0146 не версионирует никакой
install.sh— он версионируетscripts/dev/consentir-base.mjs. Принцип («никогда не генерируется api, никогда не скачивается во время выполнения», со следствием, что api не получает маршрута, выдающего скрипт для хоста) обобщаем, и ADR 0150 его расширяет; но цитировать его так, будто он уже говорит об установщике, значило бы приписать ADR фразу, которой в нём нет. docker-compose.prod.ymlне потребляет GHCR. Все сервисы указывают наbrabo-*:prodс локальным блокомbuild:, и заголовок файла говорит о себе: «o que este arquivo NÃO é: um manifesto de produção endurecido». Единственный потребитель.release/images.json— kustomize.make test-restore— это путь Kubernetes (требуетkubectlи CronJobbrabo-backup), аdocker/backup/backup.shпокрывает только Postgres. Считать его «доказательством миграции» в установке через compose значило бы обещать то, чего диск не поддерживает, — отсюда существование ADR 0152.- Реестр BRB не принадлежит репозиторию. Он живёт в vault мейнтейнера,
с 33 пунктами и критерием готовности по каждому; здесь появлялись лишь два id,
в прозе. При измерении шести, унаследованных фазой: BRB-032 и BRB-033 описывают
дефекты, которые уже не воспроизводятся, а BRB-004 — не то, что подсказывает id, — он требует
сторонний образ по digest, а не тег, который закрепил #419. Поэтому
реестр этого репозитория хранит доказательство
(измеренное состояние,
arquivo:linha), а не finding. - «Один runner на проект» навязан в пяти независимых местах, а не
только в
Registry: топикterminal:<projectId>, socket idrunner_socket:<kind>:<project_id>:<user_id>, тикет с областью проекта, колонкаrunner_device_keys.project_id NOT NULLи имя unit сервиса. Поэтому runner на машину вышел из этой фазы.
Сессии
По одному результату на каждую. Таблица — для чтения, а не для приоритета: упорядочивает колонка «зависит от», и FASE 28 зафиксировала случай, когда реальный порядок разошёлся.
| # | результат | зависит от | собственные запреты |
|---|---|---|---|
| 1 | этот документ, ADR 0149-0152, реестр BRB и диапазон RN | — | не трогает apps/, docker/, scripts/, .github/, deploy/; не редактирует принятый ADR |
| 2 | подпись в release.yml и build-runner-binaries.yml; checksums.txt; проверка в runbook | 1 | не пишет install.sh; не меняет содержимое образов; не трогает прокси api (см. ниже) |
| 3 | install.sh: проверка происхождения, обнаружение, вопрос, маркер — и прокси GET /runner-releases/binary начинает проверять | 2 | ничего не поднимает — ни compose, ни migrate, ни runner |
| 4 | install.sh: источник GHCR × локальная сборка, секреты, compose установки, проверенный подъём | 3 | не устанавливает runner; не мигрирует предыдущую установку |
| 5 | резервное копирование и восстановление томов, работающие против compose | 1 | не трогает CronJob k8s и deploy/k8s/test-restore.sh |
| 6 | install.sh: миграция предыдущей установки, доказанная сессией 5 | 4, 5 | ни при каких условиях не удаляет базу пользователя и папку зеркала |
| 7 | runner: согласованная база в guard.ts; install.sh устанавливает runner | 4 | не трогает сопряжение по проекту; не создаёт новую учётную запись |
| 8 | протокол: возможность и сообщение создания папки, собственный предикат | 7 | не изменяет RunnerReadiness; не вводит proposed_action |
| 9 | ✅ web: выбор через fs_list_dir в режиме runner, ожидание с тремя состояниями (RN-533) | 8 | не трогает путь режима mounted (RN-504, через api) |
| 10 | ✅ E2E на чистой машине, install.sh в подписанном манифесте, docmap, docs:check (RN-534) | 6, 9 | не ослабляет gate ради прохождения E2E |
Две стороны проверки не входят вместе, и разделение — это реальная зависимость. Сессия 2 поставляет того, кто производит подпись; потребители входят в 3-й, потому что оба существующих —
install.sh, сверяющий собственный хэш, и проксиGET /runner-releases/binary, сверяющий отдаваемый бинарник, — читают один и тот же подписанныйchecksums.txtи могут быть написаны только после того, как он появится. Пока 3-я не закрыта, прокси продолжает отдавать байты GitHub, ничего не проверяя, ровно как раньше: сессия 2 не ухудшает этот путь, но и не исправляет его.
Сессия 4 не вызывает
docker/smoke.sh, и это изменение намеренно. План говорил «migrate, smoke». Ни то, ни другое не пережило встречи с диском, и по противоположным причинам: migrate — не шаг, потому что compose уже выстраивает его в цепочку (apiзависит отmigrate-apiсservice_completed_successfully, иup --waitждёт) — второе место, приказывающее мигрировать, было бы вторым источником той же истины; аsmoke.shсобирает образы (--build) и роняет стек в конце, что противоположно тому, чего хочет установка. Вместо него установщик спрашивает, прежде чем утверждать: обращается к/healthapi и engine и только тогда говорит, что установил, — мерка, которуюreset-total.shвыучил на горьком опыте (BRB-033).
Чего эта фаза НЕ касается
Объявлено во всех четырёх ADR и повторено здесь, потому что именно это придаёт смысл остальному:
decide.tsи абсолютные потолки политики разрешений — ни один не получает исключения, даже настраиваемогоproposed_actionкак источник каждого внешнего эффекта агента- неизменяемость event log
- барьер образа в трёх режимах (ADR 0135)
- зеркало (RN-515/516/517) — binding папки — это другое, с другим сообщением; не сливать
- пять слоёв сдерживания broker (ADR 0130)
docker-compose.prod.yml, который остаётся compose для проверки образов
Пробелы, которые фаза затрагивает и не решает
- Runner на машину — пять связей выше. Объявлено как объём следующей фазы, с уже выполненным измерением.
- Учётные данные git, отбрасываемые в
docker execв режимеrunner— ADR 0130/0145. Начальный clone через новое сообщение выполняется на ХОСТЕ, по пути, который уже несёт учётные данные, и поэтому не затрагивается; пробел остаётся там, где был. - Отзыв достаёт до
{projeto, usuário}, никогда до{chave}(RN-520). При сохранённом в этой фазе сопряжении по проекту стоимость не меняется — но когда FASE 30 сделает runner на машину, отзыв станет ронять все проекты этой машины. Стоимость объявлена здесь, чтобы не быть обнаруженной там. - Broker не публикуется — у
docker-bake.hclчетыре цели, иscripts/ci/images-manifest.tsпринимает четыре. В установке через GHCR profilecontainer-brokerнедостижим, а режимmountedподнимает контейнер через broker (ADR 0144). ADR 0150 решает, что установщик с этим делает. - BRB-017 (обязательство GPL для инструментов, встроенных в образ engine, P1) остаётся там, где был, — и фаза делает его более конкретным, а не менее: установщик распространяет этот образ на машины третьих лиц.
- BRB-004 (сторонние образы по digest) затронут: установщик
разрешает СОБСТВЕННЫЕ образы по digest, тогда как
neo4j,pgvectorиollamaостаются по тегу. См. реестр BRB.