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

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.ymlcosign 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 и CronJob brabo-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 id runner_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; проверка в runbook1не пишет install.sh; не меняет содержимое образов; не трогает прокси api (см. ниже)
3install.sh: проверка происхождения, обнаружение, вопрос, маркер — и прокси GET /runner-releases/binary начинает проверять2ничего не поднимает — ни compose, ни migrate, ни runner
4install.sh: источник GHCR × локальная сборка, секреты, compose установки, проверенный подъём3не устанавливает runner; не мигрирует предыдущую установку
5резервное копирование и восстановление томов, работающие против compose1не трогает CronJob k8s и deploy/k8s/test-restore.sh
6install.sh: миграция предыдущей установки, доказанная сессией 54, 5ни при каких условиях не удаляет базу пользователя и папку зеркала
7runner: согласованная база в guard.ts; install.sh устанавливает runner4не трогает сопряжение по проекту; не создаёт новую учётную запись
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) и роняет стек в конце, что противоположно тому, чего хочет установка. Вместо него установщик спрашивает, прежде чем утверждать: обращается к /health api и 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 profile container-broker недостижим, а режим mounted поднимает контейнер через broker (ADR 0144). ADR 0150 решает, что установщик с этим делает.
  • BRB-017 (обязательство GPL для инструментов, встроенных в образ engine, P1) остаётся там, где был, — и фаза делает его более конкретным, а не менее: установщик распространяет этот образ на машины третьих лиц.
  • BRB-004 (сторонние образы по digest) затронут: установщик разрешает СОБСТВЕННЫЕ образы по digest, тогда как neo4j, pgvector и ollama остаются по тегу. См. реестр BRB.