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

ADR 0155 — Первая учётная запись рождается в терминале, а локальный агент поднимается сопряжённым

Статус​

Accepted 2026-09-27 (AT-108), после реализации — тем же путём, что ADR 0055. Родился Proposed в сессии 1 ФАЗЫ 30 (2026-09-11) и был отработан RN-543..552 в сессиях 2–8 (E2E на чистой машине, RN-549, — сквозное доказательство), с закрытием, описанным в historico-de-fases.md и в fase-30-runner-por-maquina.md. Там, где текст ниже расходится с кодом, решение НЕ переписывалось: расхождение названо в «Примечаниях к принятию», в конце.

Второе из двух решений ФАЗЫ 30. Зависит от ADR 0154: без идентичности машины здесь нечего регистрировать.

Контекст​

install.sh завершается с поднятым compose и бинарником runner на диске. Человек открывает браузер и видит экран входа в установке, где никого не существует.

И путь выхода оттуда сломан в новой установке. Измерено:

  • .env, который генерирует установщик, не содержит ни одной переменной e-mail (install.sh, heredoc, записывающий одиннадцать ключей);
  • MAIL_TRANSPORT сводится к значению по умолчанию log (docker/docker-compose.install.yml:200), и compose в комментарии рядом говорит, что "o comportamento continua log-only mesmo aqui";
  • регистрация требует подтверждения e-mail, и docblock apps/api/src/scripts/provisionar-usuario.ts уже фиксирует последствие прямым текстом: "o fluxo normal de registro exige verificar e-mail — que com o MailSender log-only não fecha sozinho".

То есть кто устанавливает одной строкой, регистрируется, ничего не получает, и единственный выход — выуживать ссылку из docker compose logs api. Установка, которая заканчивается словами «Готово» и никого не впускает.

provisionarUsuario существует ровно для этой дыры — создаёт пользователя с уже подтверждённым паролем — и сегодня у него два вызывающих: демонстрационный seed и smoke. Ни один из них не является настоящей установкой.

Решение​

Установщик создаёт первую учётную запись, с явным согласием в терминале, и поднимает локального агента уже сопряжённым.

1. Только ПЕРВАЯ учётная запись и только когда нет ни одной​

Шаг выполняется, когда в установке нет ни одного пользователя. Если есть хоть один, установщик не предлагает — это не создатель учётных записей, это завершение новой установки. При миграции (RN-530) пользователи приходят в restore, и шаг молчит по тому же критерию.

Маршрут внутренний и аутентифицирован BRABO_SERVICE_TOKEN — тем же секретом, который установщик только что сгенерировал и записал в .env с режимом 600, — и отказывает, если пользователь уже существует. Публичный маршрут «создать первого owner» не появляется: публичный маршрут, создающий owner, — это гонка между тем, кто установил, и тем, кто просканировал порт.

2. Без подтверждения e-mail, и основание — то, что даёт полномочия​

Учётная запись рождается подтверждённой, через provisionarUsuario. Это не ослабляет обычную регистрацию, которая остаётся байт в байт: подтверждение e-mail доказывает «этот человек контролирует этот ящик», а тот, кто запускает установщик, уже доказал нечто более сильное — контролирует машину, .env с пятью секретами и демон Docker. Требовать от него более слабое доказательство по каналу, о котором установка знает, что он выключен, — театр.

Установщик говорит об этом в выводе, вместо того чтобы человек сам обнаружил, что его учётная запись отличается от остальных.

3. Согласие явное, и без TTY шаг не происходит​

Та же мерка, что в RN-511 и consentir-base.mjs: шаг спрашивает e-mail и пароль, а без интерактивного терминала сообщает, а не решает. Не существует пути, при котором учётная запись рождалась бы с паролем, выбранным скриптом, — ни сгенерированным, ни по умолчанию.

Пароль читается без эха и подтверждается. Минимальный отказ (длина) — тот же, что в домене, вызываемый тем же кодом; установщик не несёт второй мерки пароля, которая бы разошлась.

4. Ключ машины регистрируется там же, и сервис поднимается с ним​

После создания учётной записи установщик регистрирует ключ устройства машины (ADR 0154), записывает приватный ключ с kid внутри (RN-475, дефект, из-за которого автоматический режим никогда не аутентифицировался) и устанавливает сервис — unit на машину из ADR 0154, пункт 4.

Пара генерируется на машине; приватный ключ никуда не едет. Это тот же дизайн, что в браузере (ADR 0118), по той же причине, и он сохраняет верной фразу, которой RN-519 отказывает представлению maintainer: «приватный ключ устройства никогда не покидает браузер» — становится «никогда не покидает машину».

Уточнено в реализации (RN-552). Этот пункт не говорил, какими учётными данными установщик регистрирует ключ, и обе кандидатуры были реальны. Маршрут — POST /internal/machine-device-keys — родился с service token, тем же, что для первой учётной записи: это тот же установщик, в ту же минуту, и токен доказывает контроль над МАШИНОЙ, а именно это и хочет доказать этот шаг. Альтернатива — учётные данные только что созданного пользователя — противоречила бы пункту 5 этого ADR («установщик ни за кого не входит») и дала бы паролю, прочитанному в TTY, второе использование, тогда как Последствия обещают, что он используется и отбрасывается. Цена — утёкший service token начинает мочь ФАБРИКОВАТЬ долговременные учётные данные доступа — объявлена в docs/security-surface.md, и в маршруте живут три ограничения: в теле нет userId (владелец — ЕДИНСТВЕННЫЙ пользователь установки, 409 при нуле или более чем одном), регистрация ЗАМЕНЯЕТ активные ключи машины владельца (переустановленная машина проходит; тысячи ключей не существует), а JWK с d отклоняется по имени, а не записывается. Уточнено, а не опровергнуто, и письменно, потому что этот ADR ещё Proposed: принятый ADR никогда не редактируется, новый ссылается на старый.

5. Сервис поднимается без проекта, и это нормальное состояние​

Установленный агент запрашивает GET runner/projects (ADR 0154, пункт 3) и ничего не находит, потому что проекта ещё нет. Он ждёт, и это не ошибка: не выдаёт отказа, не выходит с ненулевым кодом, а service status говорит «работает, нет проектов в режиме runner» — одно из четырёх состояний, которые RN-518 уже различает, а не пятое.

Когда человек создаст первый проект в режиме runner в web, агент уже там. Это и есть то «привязать папку позже», к которому стремится фаза, и оно происходит через workspace_create (RN-532), без того чтобы кто-то возвращался в терминал.

Что этот ADR явно отвергает​

  • Создание учётной записи, когда одна уже существует. См. пункт 1.
  • Публичный маршрут первого owner. См. пункт 1.
  • Пароль, сгенерированный скриптом, или по умолчанию. См. пункт 3. Установщик, выбирающий пароль, производит установки с одинаковым паролем.
  • Ослабление подтверждения e-mail обычной регистрации. Пункт 2 создаёт путь для названного случая; другой он не трогает.
  • Самостоятельное включение SMTP. Установщик не спрашивает почтовый сервер и не записывает MAIL_*: MAIL_TRANSPORT=log остаётся объявленным значением по умолчанию, здесь как и в продакшене. Он лишь перестаёт зависеть от него, чтобы установка завершилась.
  • Запрос учётных данных LLM. Это вне рамок: установка завершается пригодной к использованию, а ключ provider — решение того, кто будет тратить (RN-058).

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

  • Установка одной строкой начинает по-настоящему завершаться: она заканчивается учётной записью, возможным входом и работающим локальным агентом. Сегодня она заканчивается входом, который никто не может пройти, не читая лог контейнера.
  • Установщик начинает обращаться с паролем. Он читается без эха, используется и отбрасывается — никогда не записывается в .env, никогда в маркер, никогда в лог. Маркер ($XDG_STATE_HOME/brabo/) получает e-mail созданного owner, который является идентификацией, а не секретом, и больше ничего.
  • Второй путь создания пользователя начинает существовать в продакшене. Он узкий (первая учётная запись, внутренний маршрут, service token) и исчезает, как только в установке появляются люди, — но это новая поверхность, и она объявлена.
  • provisionarUsuario получает третьего вызывающего, и первого, который не является автоматизацией тестов. Его docblock сегодня говорит, что он существует для seed и smoke; он начнёт говорить ещё и для установки.
  • ФАЗА 29 становится согласованной сама с собой. Она отвергла установщик одной строкой «пока артефакты не подписаны» и подписала до установки; эта фаза закрывает другой конец — установить не значит доставить байты, значит сделать пригодным к использованию.

Примечания к принятию (2026-09-27)​

Сверено с кодом при принятии. Решение остаётся как есть; это пункты, в которых текст и код расходятся, и где код их разрешил:

  • Пункт 2 и Последствия — «учётная запись рождается подтверждённой, через provisionarUsuario», который «получает третьего вызывающего». Текст не учёл, что скрипт provisionarUsuario ОТКАЗЫВАЕТСЯ работать с NODE_ENV=production (apps/api/src/scripts/provisionar-usuario.ts:37), а установщик работает именно в продакшене. Решением было извлечь ядро — тройку записей, благодаря которой учётная запись рождается подтверждённой, — в ProvisionarUsuarioUseCase (apps/api/src/application/use-cases/auth/provisionar-usuario.use-case.ts:43) и оставить отказ в СКРИПТЕ, который он и защищает (известный пароль, созданный без участия человека). Нового вызывающего получил use case, через маршрут первой учётной записи (apps/api/src/application/use-cases/auth/criar-primeira-conta.use-case.ts:85); у скрипта остались два прежних, seed и smoke, и BRABO_FORCE_SEED НЕ является путём установщика (RN-546).
  • Пункт 4 — какими учётными данными регистрируется ключ машины. Уже уточнено в самом тексте, пока он был Proposed: service token, через POST /internal/machine-device-keys (RN-552). Пара рождается в терминале двумя командами, brabo-runner device-key create и finish --id, потому что kid — это id записи на сервере и существует только после неё (RN-551).
  • Пункт 5 — «не выходит с ненулевым кодом». Ожидание при нуле проектов существует (RN-550, исправившая выход с 0 из RN-544 именно за противоречие этому пункту), но у него есть потолок в ДЕСЯТЬ подряд запросов без полезного ответа, и тогда агент выходит с 1, называя число (apps/runner/src/espera-de-projetos.ts:75, выход в apps/runner/src/index.ts:1547). Пустой список не считается отказом — только api, который не отвечает.
  • Пункт 5 — «service status говорит "работает, нет проектов в режиме runner"». status не знает о проекте: он отвечает с диска и от менеджера, четырьмя состояниями RN-518 (apps/runner/src/servico.ts:246), а ожидающая unit машины отображается как rodando. «Нет проектов в режиме runner» говорит ЛОГ самого агента (apps/runner/src/index.ts:1529). Пункт об отсутствии пятого состояния выполняется; текст, приписанный status, — нет.