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

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):

  1. «Всегда разрешать» выключал автоматический режим. Семь dev agents получили шаблон *: auto_approve в 06:48:28–34 и через несколько секунд получили строку terminal: auto_approve, записанную «Всегда разрешать» dev agent (RN-509). Репозиторий возвращал СПЕЦИФИЧНОЕ правило раньше шаблона, с источником especifica, а decide() распознаёт автоматический режим только с источником curinga (RN-603). Результат: 97 запросов «составная команда хотя бы с одним сегментом, не покрытым allow» и 37 «область» при горящем на экране автоматическом режиме.
  2. Область сравнивалась с папкой, где команда НЕ выполняется. Все 37 «область» были /work/... или /tmp, сравниваемые с корнем ХОСТА (/home/<usuario>/projetos-brabo/<projeto>). При контейнере running engine выполняет команду ВНУТРИ него (RN-492/RN-502), в /work, и переводит cwd хоста в /work только потом, в terminal_executor.ex.
  3. Автоматический режим не говорил на экране, что он разрешает. Карточка пакета из 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, никогда аргументы).
  • Что доказательство изоляции НЕ покрывает — объявлено, а не исправлено здесь (решение владельца было описать, а не латать политику):
    1. В compose ПРОДАКШЕНА (docker/docker-compose.prod.yml) ничто на хосте не проверяет, что BRABO_PROJECTS_BASE не содержит папку, откуда запускается compose (checkout или папку .env). preflight.mjs выполняется только в pnpm dev, а caminhoDeWorkspaceLocalValido сравнивает с cwd СОБСТВЕННОГО api (/app), который никогда не является путём хоста. С такой базой проект mounted, чья папка — checkout, монтирует checkout в /work — с автопилотом или без.
    2. В УСТАНОВКЕ install.sh отклоняет базу, перекрывающую папку установки, только при согласии (consentir_base), без собственного теста; отредактированный позже .env повторно не проверяется.
    3. В режиме runner ничто не сравнивает папку проекта (или базу локального агента) с checkout Brabo или с папкой установки на машине пользователя — api сравнивает с собственным cwd, а guard.ts runner ограничивает $HOME только в Linux.
    4. segmentoDeProjetoValidado (packages/docker-port) принимает . как сегмент: <raiz>/. проходит через raizDeProjetoValidada как сам корень и смонтировал бы ВСЕ проекты этого корня. Api никогда не производит . (монтируемый сегмент выходит нормализованным, а управляемый — [A-Za-z0-9_-]), так что сегодня это недостижимо; но broker существует, чтобы не зависеть от корректности вызывающего. Checkout не достигается (корень его не содержит), достигаются ДРУГИЕ проекты.
    5. Сеть egress — это bridge: контейнер достигает портов, которые публикует машина, включая порты api и engine. Файлов — нет; сервисов — да, за аутентификацией.
  • «Всегда разрешать» на составной команде начинает разрешать СЛЕДУЮЩИЕ составные команды с теми же глаголами и подкомандами — включая, для глагола без подкоманды (cat, rm, cd), глагол с ЛЮБЫМ аргументом. Это то расширение, которое AT-170 просила решить, а не вывести; вне автопилота область пути по-прежнему ограничивает, где, а потолки по-прежнему срабатывают после файла.
  • ТИПИЗИРОВАННЫЕ git_push и pr_open не являются потолком в decide(): активация засевает для них auto_approve по dev-<modulo> (ADR 0053), и шаблон их тоже одобряет; удерживает их то, что «Всегда разрешать» отказывается их записывать (AT-320), и потолок merge в защищённую ветку. Здесь не изменилось.