ADR 0189 — Автопилот: автоматический режим одобряет всё, кроме потолков, область сравнивается с реальной папкой исполнения, а «Всегда разрешать» записывает глагол + подкоманду
Статус
Accepted. 2026-10-01 (AT-259, AT-255, AT-258 и AT-257 — последняя закрывает AT-170, — история HS-066, эпик EP-030, раунд 35; решение владельца от 01.10, записанное в задачах). Ссылается на ADR 0167 (автоматический режим освобождает от области пути) и ADR 0055 (область пути в политике терминала), не редактируя их: 0167 не отменяется — автоматический режим по-прежнему разрешает любую команду, в том числе вне папки, — а 0055 остаётся потолком, теперь сравниваемым с папкой, где команда ВЫПОЛНЯЕТСЯ.
Контекст
Реальное использование 2026-09-29 измерило три дефекта одной и той же
политики (analise-uso-real.md, пункты 9 и 11):
- «Всегда разрешать» выключал автоматический режим. Семь dev agents
получили шаблон
*: auto_approveв 06:48:28–34 и через несколько секунд получили строкуterminal: auto_approve, записанную «Всегда разрешать» dev agent (RN-509). Репозиторий возвращал СПЕЦИФИЧНОЕ правило раньше шаблона, с источникомespecifica, аdecide()распознаёт автоматический режим только с источникомcuringa(RN-603). Результат: 97 запросов «составная команда хотя бы с одним сегментом, не покрытым allow» и 37 «область» при горящем на экране автоматическом режиме. - Область сравнивалась с папкой, где команда НЕ выполняется. Все 37
«область» были
/work/...или/tmp, сравниваемые с корнем ХОСТА (/home/<usuario>/projetos-brabo/<projeto>). При контейнереrunningengine выполняет команду ВНУТРИ него (RN-492/RN-502), в/work, и переводитcwdхоста в/workтолько потом, вterminal_executor.ex. - Автоматический режим не говорил на экране, что он разрешает. Карточка
пакета из RN-661 говорила, что он НЕ
разрешает; переключатель на карточке агента и
ApprovalCardимели разные фразы, и ни одна не говорила, чтоgit commitи локальная ветка проходят.
Задача AT-259 предлагала отменить ту часть RN-603, которая освобождает от области. Владелец 01.10 решил обратное: «RN-603 НЕ отменяется — поскольку команда выполняется в контейнере, автоматический режим продолжает разрешать любую команду, в том числе вне папки, при условии, что это сохраняется так, чтобы с кодом, который выполняет Brabo, не возникло никаких проблем».
Измерено в dev до изменения (тесты, которые проваливались на сегодняшнем
коде): decide.spec.ts (5 случаев корня в контейнере),
agent-autonomy.repository.spec.ts (специфичное, затеняющее шаблон) и
approve-always-action.use-case.spec.ts (измеренный сценарий, сквозной).
Не воспроизвелось: автопилот уже одобрял git commit,
git checkout -b/switch -c/branch и типы git_commit/
git_branch_create при включённом шаблоне и уже удерживал
push/PR/merge/deploy, sudo/doas, container_remove, instruction_patch
и параллелизм — AT-259 закрывается только через AT-255 и экран.
Решение
1. Автоматический режим — это АВТОПИЛОТ
Шаблон *: auto_approve одобряет ВСЁ, включая синтезированную составную
команду из permissions.json и путь вне папки (RN-603, сохранена), а также
git commit и создание ЛОКАЛЬНОЙ ветки. Вне его, никогда не одобряемые
автоматически, остаются: git push/PR/merge/deploy через терминал
(RN-418), sudo/doas, merge в защищённую
ветку, container_remove, instruction_patch и
parallelize/raise_max_parallel. Явный deny по-прежнему побеждает, и
ask, ЗАПИСАННЫЙ в файле, по-прежнему запрашивает — это правило
пользователя, как уже было в RN-603.
2. «Всегда разрешать» не выключает автопилот (самая простая форма)
Специфичное auto_approve под шаблоном auto_approve разрешается как
ШАБЛОН, в ТОМ ЖЕ репозитории, который уже разрешал приоритет
(DrizzleAgentAutonomyRepository.resolve). Из двух форм, которые предлагала
AT-255, — не записывать специфичное при наличии шаблона или чтобы шаблон
действовал для исключений со специфичным auto_approve, — выбрана вторая, по
трём причинам: она исправляет строки, УЖЕ записанные при реальном
использовании (первая лишь предотвратила бы следующие); клик по-прежнему
записывает специфичное, которое остаётся в силе, если переключатель вернётся в
ручной режим (с областью, как всегда); и никакой новый путь не рождается —
decide() не меняет правило, лишь получает правильный источник. Специфичное,
говорящее ДРУГОЕ (require_approval, deny), по-прежнему побеждает шаблон.
AutonomiaResolvida получает especifica (режим специфичной строки, когда
она есть), и именно по нему, а не по разрешённому режиму, «Всегда разрешать»
решает, записан ли уже шаблон агента.
3. Область сравнивается с РЕАЛЬНОЙ папкой исполнения
При зарегистрированном контейнере running в проекте container или
mounted корнем потолка области является /work (точка монтирования — папка
и .worktrees проекта) плюс /tmp контейнера. cwd хоста переводится в
/work по ТОМУ ЖЕ правилу, что и в engine (cwd_para_container/2): корень
становится /work, то, что под ним, становится /work/..., то, что вне
него, остаётся как было. В mounted перевод биективен (bind-mount — это
тождество папки). Без контейнера корень — это папка проекта на хосте, а /tmp
хоста остаётся ВНЕ — он принадлежит всей машине, включая Brabo.
Это действует для тех, кто НЕ на автопилоте, — специфичного правила
terminal, — потому что автопилот через область не проходит.
projectScopeRoot не меняется и НЕ объединяется с permissionsFilePath
(RN-478): корень хоста остаётся входом, а корень в контейнере ВЫВОДИТСЯ из
него в path-scope.ts. ПОЛ автоодобрения контейнера (RN-493) остаётся только
для режима container; новое поле DecideContext (execucaoNoContainer) —
это то, ГДЕ выполняется команда, и это другое.
runner остаётся за рамками, намеренно: выбор хост-или-контейнер внутренний
для runner (ADR 0137), а перезапущенный runner при работающем контейнере
маршрутизирует на ХОСТ (RN-558) — api не знает, что из двух произойдёт, а
освобождение /tmp для того, кто может быть на хосте, означало бы освобождение
/tmp машины пользователя.
4. Экран говорит, что автопилот разрешает и чего не разрешает
ОДИН список (OQueOPilotoLibera) в ТРЁХ местах, где включается
автоматический режим: карточка пакета в Исполнителях (раскрыт),
ApprovalCard и переключатель на карточке агента (свёрнут, под уже
существовавшей фразой). Половина «не разрешает» — текст, который уже был у
RN-661, переиспользованный; половина «разрешает» рождается здесь.
5. Гарантия владельца: изоляция, доказанная
Условие решения — никаких проблем с кодом, который выполняет Brabo, — это не
политика, а изоляция контейнера, и она доказана от начала до конца в
apps/broker/src/contencao-do-brabo.spec.ts: контейнер проекта монтирует ОДНУ
папку, всегда в /work, СТРОГО ниже корня broker (управляемого или базы), без
сокета Docker, без --privileged, без сети хоста, а пустой/абсолютный/с
обходом сегмент отклоняется до любого вызова Docker. Вместе с защитами, не
дающими корню содержать checkout (baseSobrepoeOCheckout в preflight.mjs,
протестированная в scripts/dev/base-de-projetos.spec.ts), и отказом
выполнять без контейнера (RN-502/RN-507) команда автопилота не достигает
checkout Brabo.
6. Единица «Всегда разрешать»: глагол + подкоманда, по сегменту
Решение владельца от 01.10 (AT-257, закрывающая AT-170 — пункт 6 ADR 0055,
оставленный там открытым): клик перестаёт записывать всю команду, байт в байт,
и записывает ОДИН шаблон на каждый СЕГМЕНТ команды, в единице ГЛАГОЛ +
ПОДКОМАНДА (npm test, git status); аргумент, не являющийся словом,
обобщается до глагола (cat src/x.ts → cat); глагол + флаг остаётся
ТОЧНЫМ (это форма, где находки Z/AD говорят, что глагол и вызов расходятся); а
единица, являющаяся префиксом потолка RN-418 (git remote, gh pr), остаётся
точной или исчезает, если таковым является даже точная. Четыре подвопроса
AT-170 получают ответ: единица — эта; она НЕ наследует allowlist глаголов (ни
один список глаголов не рождается, и сомнение между аргументом и подкомандой
решается в узкую сторону); записывает та же роль, что и сегодня; а приоритет с
шаблоном — из пункта 2. Подробности в
RN-675.
Последствия
- Сценарий, измеренный 29.09, перестаёт запрашивать одобрение: шаблон +
«Всегда разрешать» остаётся на автопилоте (тест на отсутствие регрессии в
approve-always-action.use-case.spec.ts). - Специфичное правило
terminal: auto_approveперестаёт запрашивать одобрение для/work/...и/tmpпри работающем контейнере и начинает запрашивать для пути ХОСТА в команде (которого внутри контейнера не существует — engine переводит толькоcwd, никогда аргументы). - Что доказательство изоляции НЕ покрывает — объявлено, а не исправлено
здесь (решение владельца было описать, а не латать политику):
- В compose ПРОДАКШЕНА (
docker/docker-compose.prod.yml) ничто на хосте не проверяет, чтоBRABO_PROJECTS_BASEне содержит папку, откуда запускается compose (checkout или папку.env).preflight.mjsвыполняется только вpnpm dev, аcaminhoDeWorkspaceLocalValidoсравнивает сcwdСОБСТВЕННОГО api (/app), который никогда не является путём хоста. С такой базой проектmounted, чья папка — checkout, монтирует checkout в/work— с автопилотом или без. - В УСТАНОВКЕ
install.shотклоняет базу, перекрывающую папку установки, только при согласии (consentir_base), без собственного теста; отредактированный позже.envповторно не проверяется. - В режиме
runnerничто не сравнивает папку проекта (или базу локального агента) с checkout Brabo или с папкой установки на машине пользователя — api сравнивает с собственнымcwd, аguard.tsrunner ограничивает$HOMEтолько в Linux. segmentoDeProjetoValidado(packages/docker-port) принимает.как сегмент:<raiz>/.проходит черезraizDeProjetoValidadaкак сам корень и смонтировал бы ВСЕ проекты этого корня. Api никогда не производит.(монтируемый сегмент выходит нормализованным, а управляемый —[A-Za-z0-9_-]), так что сегодня это недостижимо; но broker существует, чтобы не зависеть от корректности вызывающего. Checkout не достигается (корень его не содержит), достигаются ДРУГИЕ проекты.- Сеть
egress— этоbridge: контейнер достигает портов, которые публикует машина, включая порты api и engine. Файлов — нет; сервисов — да, за аутентификацией.
- В compose ПРОДАКШЕНА (
- «Всегда разрешать» на составной команде начинает разрешать СЛЕДУЮЩИЕ
составные команды с теми же глаголами и подкомандами — включая, для глагола
без подкоманды (
cat,rm,cd), глагол с ЛЮБЫМ аргументом. Это то расширение, которое AT-170 просила решить, а не вывести; вне автопилота область пути по-прежнему ограничивает, где, а потолки по-прежнему срабатывают после файла. - ТИПИЗИРОВАННЫЕ
git_pushиpr_openне являются потолком вdecide(): активация засевает для нихauto_approveпоdev-<modulo>(ADR 0053), и шаблон их тоже одобряет; удерживает их то, что «Всегда разрешать» отказывается их записывать (AT-320), и потолок merge в защищённую ветку. Здесь не изменилось.