Права в Devin Local выдаются не инструментам вообще, а конкретным действиям с конкретным scope. Пять типов - Read, Write, Exec, Fetch и mcp - и у каждого свой язык совпадений. Пока не различаешь эти языки, любое правило либо шире намерения, либо уже него, и агент делает не то, что вы имели в виду.
Наивный ход - думать о правах крупными мазками: "разреши читать", "разреши команды", "дай интернет". Кажется, что тип и есть единица разрешения, а scope - деталь. Из этого растёт привычка писать Read или Exec целиком и удивляться, когда агент прочёл лишнее или запустил не ту команду.
Ломается это на том, что единица разрешения - пара тип плюс scope, и у каждого типа scope свой. Read(glob) и Write(glob) берут glob по файлам; путь-каталог покрывает всё его содержимое. Exec(prefix) берёт префикс команды, который должен совпасть как целое слово. Fetch(pattern) берёт URL-паттерн. mcp__server__tool адресует конкретный инструмент конкретного MCP-сервера. Пять типов - пять разных способов сказать "именно это".
Отдельно живёт семантика glob, и на ней спотыкаются чаще всего. Звёздочка совпадает с одним сегментом пути и не переходит через разделитель каталога; двойная звёздочка рекурсивна и границы каталогов пересекает. Read() означает workspace относительно текущего каталога, а абсолютные системные пути надо задавать явно, через ведущий разделитель. Тильда раскрывается в домашний каталог. Разница между одной и двумя звёздочками - это разница между "здесь" и "везде глубже", и путать её дорого. Каталог в scope, к слову, покрывает всё своё содержимое, поэтому Read(src) и Read(src/) на практике означают близкое, а вот Write(config) и Write(config/*.json) - совсем разное по радиусу.
Своя тонкость у Exec. Префикс совпадает как целое слово: Exec(git) покроет git status и git commit, но не gitk и не github-cli. Это спасает от случайного расширения - разрешение на git не расползётся на однокоренные бинарники. Но это же требует точности: если нужно лишь чтение статуса, префикс Exec(git) всё равно охватит и mutating subcommands, и сужать надо до Exec(git status), Exec(git diff).
Fetch и mcp - про выход за пределы файлов. Fetch(pattern) решает, к каким URL или доменам агент вправе обратиться по HTTP; это граница исходящей сети на уровне прав, отдельная от сетевого фильтра sandbox. mcp__server__tool решает, каким именно инструментом какого сервера можно пользоваться: не "весь MCP", а конкретный tool. Обе категории легко выдать слишком широко, потому что польза видна сразу, а радиус - нет. Полезно помнить, что Fetch и сетевой фильтр sandbox - два разных рубежа: первый решает, вправе ли агент вообще обратиться к URL, второй - долетит ли пакет из клетки; надёжная политика опирается на оба, а не путает их.
Почему права дробят до пары тип плюс scope, а не выдают инструментами целиком. Потому что риск живёт не в инструменте, а в его применении к конкретной цели. Read сам по себе безобиден - Read(.env*) уже нет. Exec как категория нейтрален - Exec(rm) необратим. Дробление позволяет разрешить полезное и запретить опасное внутри одного типа, чего грубая выдача "весь Read, весь Exec" не умеет.
На практике помогает прогонять каждое действие через один вопрос: какой тип, какой scope и в какой список - allow, ask или deny. Чтение src - Read(src/*) в allow. Запись .env - Write(.env) в deny. Запуск теста - Exec(npm run test) в allow. npm install - в ask, потому что тянет из сети и меняет дерево. git commit - в ask как mutating. Production deploy - в deny или вовсе вне прав. Одно действие - один осознанный адрес. Записанные так ответы заодно превращаются в готовый черновик секций allow, ask и deny - от разбора на бумаге до конфига остаётся один шаг.
Проверять scope стоит противоположным действием, а не только тем, что разрешали. Разрешили Read(src/*) - убедитесь, что чтение вне src не проходит; положили Write(.env) в deny - попробуйте спровоцировать запись и увидьте блок. Совпадение "разрешённое идёт, запрещённое стоит" на паре встречных проверок доказывает, что glob и префикс сузили ровно то, что нужно.
Типичные провалы - о перепутанном языке scope. "Агент прочитал лишнее" - Read(**) захватил больше, чем думали, или absolute-путь дали без нужды. "Команда не запустилась" - префикс не совпал как целое слово. "MCP не работает" - разрешение выписано на сервер, а не на его tool, или наоборот. "Fetch мимо" - паттерн домена не совпал с реальным URL. Признак один: право обсуждают, назвав тип, но не scope. Назовите и scope - и почти всякое "мимо" объяснится границей совпадения.