Permission rules - это то, что превращает пожелание "будь осторожен" в проверяемое правило. Три списка - deny, ask и allow - решают судьбу каждого вызова инструмента, и порядок между ними не косметический, а строгий: сначала deny, потом ask, потом allow. Понять этот порядок важнее, чем выучить синтаксис: синтаксис можно подсмотреть, а неверная модель приоритета молча искажает каждое правило разом.
Наивный ход - написать пощедрее allow, чтобы агент не дёргал по мелочам, и добавить пару deny "на всякий случай", думая, что широкий allow и точечный deny уживутся по здравому смыслу. Ожидание: правила складываются как удобно человеку, и если что-то одновременно разрешено и запрещено, победит более свежее или более конкретное.
Ломается это на фактическом порядке проверки. Правила смотрятся строго: deny блокирует, ask требует подтверждения, allow автоподтверждает, а при отсутствии совпадения действует default - запрос. Если один и тот же вызов попал под несколько правил, выигрывает более строгий исход: deny всегда бьёт ask и allow. Никакого "более конкретное побеждает" - конкретность не отменяет строгости.
К порядку внутри одного конфига добавляется порядок между источниками. Организационные правила стоят выше сессионных, те - выше project-local (.devin/config.local.json), те - выше project (.devin/config.json), а ниже всех пользовательский конфиг. И поверх всего - жёсткое правило: организационные deny не переопределяются ни проектом, ни пользователем. Ваш щедрый allow бессилен против org-deny, и это by design. Практический вывод из иерархии простой: свой конфиг вы пишете как самый слабый голос в хоре, и рассчитывать он может только на то, что выше не запрещено.
Рабочий принцип - начинать узко. Разрешать конкретные безопасные команды, а не целые инструменты: не Exec целиком, а Exec(git status), Exec(git diff), Exec(npm run test), Exec(npm run lint); читать - отдельными префиксами. В ask держать то, что меняет рабочее дерево: Write(src/), Write(tests/), Exec(git). В deny - необратимое и секретное: Write(.env), Write(**/.pem), Exec(rm), Exec(sudo). Ниже - как это выглядит в .devin/config.json. Смысл узкого старта не в недоверии к агенту, а в дешевизне проверки: список из десятка явных allow читается глазами за минуту, а широкое "разреши всё в проекте" проверить нельзя в принципе.
Тонкость, которую легко проглядеть: Exec(git) охватывает не только чтение, но и mutating subcommands - commit, push, reset. Разрешив Exec(git) целиком, вы заодно разрешили и то, что хотели оставить под вопросом. Поэтому для статуса и диффа лучше отдельные узкие префиксы в allow, а общий Exec(git) держать в ask, где он поднимет карточку перед изменением.
Почему порядок именно deny, ask, allow, а не наоборот. Потому что безопасная система должна ошибаться в сторону запрета: если правила противоречат, дешевле лишний раз спросить или заблокировать, чем молча разрешить необратимое. Строгий приоритет deny делает запрет предсказуемым - его нельзя случайно перекрыть разрешением, добавленным позже или в другом файле. Предсказуемость запрета и есть то, ради чего заводят правила.
Цена неверной модели приоритета конкретна. Написать широкий allow и надеяться, что deny где-то подстрахует, ещё работает, потому что deny сильнее. А вот обратное - положиться на allow там, где org-конфиг молча держит deny, - оставит вас с агентом, который "почему-то не делает разрешённое". И самое дорогое: Exec(git) в allow, принятый за безопасный, однажды сделает commit или push без вопроса.
Проверять правила стоит не глазами по конфигу, а поведением на границе. Выдайте агенту действие, которое должно спрашивать, и убедитесь, что карточка поднялась; выдайте то, что должно блокироваться, и убедитесь, что оно заблокировано, а не проскочило. Читать конфиг и запускать проверочный вызов - разные способы, и совпадение их ответов и есть доказательство, что правила расставлены как задумано. Такую проверку стоит повторять после каждой правки конфига и после подключения нового источника правил: одна добавленная org-политика способна тихо изменить исход, который вчера был другим.
Типичные провалы - о неверной модели приоритета. "Запретил, а оно выполнилось" - почти всегда значит, что deny написан не на тот scope, а не что deny слаб. "Разрешил, а спрашивает" - выше по иерархии стоит ask или org-политика. "Правило не действует" - оно в пользовательском конфиге, перекрытом проектным или организационным. Признак один: спор с правилами ведут, не назвав ни источник правила, ни его точный scope. Назовите оба - и порядок объяснит исход.
// .devin/config.json
{
"permissions": {
"allow": [
"Read(src/**)",
"Read(tests/**)",
"Exec(git status)",
"Exec(git diff)",
"Exec(npm run test)",
"Exec(npm run lint)"
],
"ask": [
"Write(src/**)",
"Write(tests/**)",
"Exec(git)"
],
"deny": [
"Write(.env*)",
"Write(**/*.pem)",
"Exec(rm)",
"Exec(sudo)"
]
}
}