@restranger/repi-permission-system-core0.0.1-ca9b82c
Security-hardened permission gateway, bash analysis, and path access control for pi-coding-agent
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
@restranger/repi-permission-system-core
Шлюз авторизации для pi: перехватывает tool_call, разбирает
запрос (файл, путь, команда bash) и возвращает allow / deny / ask / rewrite по правилам из
плоского JSON-конфига. Плагин самостоятельный: ему нужен только @restranger/repi-lib и хост.
Главная идея: неявных запретов нет. Всё, что не описано правилом, даёт ask; deny ставит
только пользователь — обычным правилом в конфиге.
pi install npm:@restranger/repi-permission-system-core
# или из этого репозитория
pi --extension ./packages/permission-system-core/src/index.ts
Что происходит при вызове инструмента
- Запрос (
PermissionRequest:tool,input,cwd,sessionId,signal) попадает вPermissionEngine.check(). - Для
bashкоманда разбирается Tree-sitter вExecutionUnit-ы (src/bash-ast.ts, см.src/vendor/README.md) и каждый юнит проверяется отдельно: правилаpathдля его путей и редиректов, правилаbash, границы внешних каталогов, сессионные гранты. - Для файловых инструментов путь каноникализируется (
realpathближайшего существующего предка), проверяются правилаpathи правилаexternal_directory*. - Побеждает самое строгое совпавшее правило:
deny > ask > rewrite > allow. Не «первое совпадение». - Если итог —
ask, плагин показывает диалог; без диалога, при таймауте или ошибке решение становитсяdeny(fail-closed). rewriteприменяется кinputсобытияtool_call(сейчас так работает только NoCD, см. ниже).
Модули
| Файл | Роль |
|---|---|
src/bash-ast.ts |
разбор bash на AST, ExecutionUnit, редиректы, вложенный код; analyzeBash, analyzeBashAsync, pathArguments |
src/bash-unwrap.ts |
снятие обёрток (sudo, timeout, env, xargs, command, …) |
src/engine.ts |
PermissionEngine: правила, «самое строгое», гранты, трансформеры |
src/config-loader.ts |
поиск, валидация и слияние слоёв конфига |
src/boundary.ts |
границы внешних каталогов, порядок строгости поверхностей |
src/nocd.ts |
детект cd <dir> && … и рерайт в cwd |
src/read-only.ts |
commandIsReadOnly() / isReadOnlyRequest() |
src/secrets.ts |
чистые хелперы путей и глобов (без списков секретов) |
src/grants.ts |
сессионные гранты |
src/prompt-ui.ts |
inline-диалог подтверждения |
src/plugin.ts |
рантайм pi-расширения, публикация re:permissions@1 |
src/vendor/tree-sitter-bash.wasm |
вендоренная грамматика Bash |
Конфиг
Слои ищутся в таком порядке и сливаются так, что приоритет растёт сверху вниз (сначала пользовательские, затем проектные):
$PI_CODING_AGENT_DIR/extensions/repi-permissions.json # по умолчанию ~/.pi/agent
$PI_CODING_AGENT_DIR/extensions/repi-permissions/config.json
$PI_CODING_AGENT_DIR/extensions/repi-permissions/permissions.json
$XDG_CONFIG_HOME/repi/permissions.json # по умолчанию ~/.config/repi
<project>/.pi/extensions/repi-permissions.json
<project>/.pi/extensions/repi-permissions/config.json
<project>/.pi/extensions/repi-permissions/permissions.json
<project>/.repi/permissions.json
Проектные слои идут последними и могут только ужесточать политику: из них применяются
deny и ask, а allow, rewrite и trustedTools игнорируются (репозиторий не должен
разрешать себе то, что запретил пользователь). askTimeoutMs и doublePressToConfirm из
проектного слоя применяются как есть.
Шаблон
.pi/extensions/repi-permissions.jsonв этом репозитории лежит в проектном слое, поэтому егоallow-правила не действуют: разрешения нужно держать в пользовательском слое ($PI_CODING_AGENT_DIR/extensions/repi-permissions.jsonили$XDG_CONFIG_HOME/repi/permissions.json).
Схема
| Ключ | Тип | Смысл |
|---|---|---|
* |
action | глобальный фолбэк для всего, что не совпало; по умолчанию ask |
path |
glob → action | проектные глобы: файловые инструменты и пути-аргументы bash-команд |
bash |
паттерн → action | команда → действие; матчится после снятия обёрток |
external_directory |
action или glob → action | общий набор правил для путей вне корня проекта |
external_directory_read |
action или glob → action | то же для чтения (если задан, заменяет общий) |
external_directory_write |
action или glob → action | то же для записи/правки |
askTimeoutMs |
число > 0 | таймаут диалога, по умолчанию 30000 |
trustedTools |
string[] | инструменты, для которых ask повышается до allow (явный deny сильнее) |
doublePressToConfirm |
boolean | требовать второе нажатие y/s/n/r в TUI |
action — одно из allow, deny, ask, rewrite. Действие rewrite умеет только NoCD
(оно подставляет modifiedInput.cwd); rewrite, заданный правилом в конфиге, вырождается в ask, потому
что замена команды из правила не выводится.
Пример
{
"askTimeoutMs": 30000,
"doublePressToConfirm": false,
"*": "ask",
"path": {
"**": "allow",
".pi/extensions/**": "deny",
".repi/**": "deny",
"**/*.pem": "deny",
"**/id_rsa*": "deny",
"**/auth.json": "deny"
},
"bash": {
"*": "ask",
"ls *": "allow",
"pwd": "allow",
"rg *": "allow"
},
"external_directory": { "*": "ask" },
"external_directory_read": { "/nix/store/*": "allow", "/tmp/pi/*": "allow" }
}
Поверхности external_* можно записать одним действием: "external_directory_write": "deny".
Старая форма и ошибки
Вложенная форма {"permission": {…, "options": {…}}} ещё принимается и нормализуется при
загрузке; плоские ключи выигрывают у вложенных. Неизвестный ключ — ошибка конфига, а не
молчаливое игнорирование; нечитаемый или битый файл — тоже ошибка, и плагин запускается с
DENY_ALL_CONFIG (deny-all), показывая уведомление.
Как матчатся правила
Глобы путей. * не переходит через /, ** переходит. Ведущий **/ может совпасть с
нулём сегментов, поэтому **/*.pem покрывает и key.pem в корне, и certs/key.pem. Замыкающий
/* покрывает всё поддерево: /nix/store/* — это «что угодно внутри /nix/store». Шаблон
path пишется относительно корня проекта, а путь проверяется в четырёх написаниях (абсолютное,
относительное, ./относительное, базовое имя), чтобы одно правило ловило src/a.ts, ./src/a.ts
и /abs/src/a.ts. * и ** в path напрямую не матчатся — это фолбэк, если ничего не совпало.
Паттерны bash. * матчит что угодно, включая пробелы; сравнение идёт по всей строке
(ls * покрывает ls -la, а pwd — только pwd). Паттерн проверяется против трёх кандидатов
юнита: исходного текста (raw), argv после снятия обёрток (targetArgv.join(" ")) и имени
команды (name). "*" — фолбэк.
Юниты bash. &&, ||, |, ;, &, переводы строк, $(…), <(…), subshell, функции, циклы,
case, sh -c '…', eval, find -exec дают отдельные юниты; обёртки (sudo, timeout, env,
nice, xargs, command, doas, …) снимаются, чтобы правило видело настоящую команду. cd
внутри последовательности меняет cwd последующих юнитов. Пути-аргументы и редиректы (включая
heredoc) проверяются теми же правилами path, что и файловые инструменты.
Внешние каталоги. Путь вне корня проекта проверяется по external_directory_read /
external_directory_write (или по общему external_directory), иначе — по глобальному *.
Правка (edit) — это и чтение, и запись: побеждает более строгое из двух.
Статически не проверяемое. $VAR в опасной позиции, curl … | bash, heredoc/herestring в
интерпретатор, source <(…)/bash <(…), неразрешимый cd (cd -, cd $VAR, голый pushd),
вложенность глубже 4 и синтаксические ошибки помечаются unverifiable или tooComplex и дают
ask — никогда молчаливый allow. Если WASM-парсер не загрузился, команда тоже уходит в ask
(bash:parser-unavailable), а не в allow.
NoCD. Ведущий cd <dir> && … превращается в решение rewrite с modifiedInput.cwd, если
<dir> остаётся внутри корня проекта. При выходе за корень рерайта нет: остаток команды
проверяется с реальным cwd, поэтому правила external_directory* видят настоящие пути.
ask, fail-closed и таймаут
- Диалог берётся из
re:ui@1, если плагин@restranger/repi-uiустановлен, иначе изctx.ui. - TUI: inline-диалог с фактами запроса (инструмент, правило, команда/путь, риск, причина) и
вариантами
Yes/Yes, for this session/No/No, provide reason(горячие клавишиy/s/n/r);doublePressToConfirmтребует второго нажатия. RPC используетselect/inputс теми же вариантами. - Нет диалога (headless,
print/json), таймаут (askTimeoutMs, по умолчанию30000мс),abortили исключение →deny. Таймаут закрывает и сам диалог. - Ошибка пайплайна, прерванный
signal,bashбезcommand,bashс посторонними file-аргументами →denyсruleIdизpipeline:/bash:. - Трансформер решений (режимы, другие плагины) может сузить решение, но не поднять
denyиз конфига или пайплайна: такие решения «заблокированы».
trustedTools, гранты, read-only
trustedTools— список инструментов, для которыхaskповышается доallow(ruleId: options:trusted-tool). Правила при этом всё равно вычисляются, поэтому явныйdeny(включаяpath-deny) сильнее доверия. Дляbashмеханика тоже работает, но разбор AST не пропускается: запрет на команду всё равно победит.- Сессионные гранты. «Yes, for this session» сохраняет грант в
SessionGrantStore: для bash — точная строка команды, для файла — канонический путь. Грант для команды сравнивается без глобов (одно одобрение не расширяется доrm -rf *), для пути — с глобами. Гранты живут в памяти, переживают/compact, сбрасываются наsession_shutdownи/reload. Catch-all и гранты на оболочки/интерпретаторы (sh,sudo,eval,xargs,python, …) отвергаются, аdenyиз конфига сильнее любого гранта. API:grantSession,clearSessionGrants,listSessionGrants. - Read-only.
commandIsReadOnly(command, cwd)(модульsrc/read-only.ts) консервативен:false, если команду не удалось разобрать, если онаunverifiable/tooComplex, если есть пишущий редирект или юнит с-delete/-exec/-i/-o. Интерпретатор считается прозрачным только дляsh -c '…'/eval, чей скрипт разложен на юниты:bash script.sh, голыйshи heredoc/herestring в оболочку — не read-only.isReadOnlyRequest(req)возвращаетtrueдля не-bash запросов и делегирует вcommandIsReadOnlyдля bash.
Что экспортирует движок
src/index.ts реэкспортирует всё публичное:
- рантайм:
createPermissionRuntime,createPermissionPlugin,createPermissionPluginInstance,default(entry point дляpi.extensions); - движок:
PermissionEngine(check,explain,isReadOnly,grantSession,clearSessionGrants,listSessionGrants,setDecisionTransformer,addDecisionTransformer,onDecision,getPolicy,setConfig); ask:resolveAsk,isAskDecision,askViaUi,mapToolCallPayload,synthesizeGrantPattern;- AST:
analyzeBash,analyzeBashAsync,ensureBashParser,isBashParserReady,bashParserError,disposeBashParser,clearBashAnalysisCache,pathArguments,isPathLikeToken,unwrapCommand,tokenizeCommand,checkNoCdRewrite; - конфиг:
loadPermissionConfig,loadPermissionConfigFromLayers,findConfigPaths,validatePermissionConfig,normalizePermissionConfig,permissionOptions,PermissionConfigError,DENY_ALL_CONFIG; - правила и пути:
resolveSurfaceRule,mostRestrictive,mostRestrictiveOf,evaluateBoundary,operationForTool,isExternalPath,matchPathGlob,matchGlobPart,matchCommandPattern,resolveCanonicalPath,expandHome,extractPathCandidate,pathDynamism,looksLikePath; - гранты и read-only:
SessionGrantStore,isUnsafeGrantPattern,commandIsReadOnly,isReadOnlyRequest; - типы контракта:
PermissionPolicy,PermissionConfig,PermissionSurfaceRules,PermissionAction,PermissionDecision,PermissionRequest,PermissionExplanationи др.
Сервис публикуется как re:permissions@1 на сессию с capabilities
permission-gate, decision-transformers, read-only-check, decision-events.
Чего больше нет
Жёстко зашитого слоя «hard deny» нет: списки вроде rm -rf /, find -delete, записи в блочные
устройства, .env/*.pem/id_rsa, запрета записи наружу и cd за пределы проекта из кода
удалены. Регулярок вместо AST тоже нет. Всё это теперь выражается обычными правилами:
{
"path": { "**/.env": "deny", "**/*.pem": "deny", "**/id_rsa*": "deny" },
"bash": { "rm -rf /*": "deny", "find * -delete": "deny" }
}
Без такого правила команда спросит подтверждение, а не будет заблокирована молча.
Зависимости
web-tree-sitter@^0.25.10— обычная runtime-зависимость (WASM-рантайм).tree-sitter-bash.wasmвендорен вsrc/vendor/и поставляется вместе с пакетом: грамматика должна быть ABI-совместима с рантаймом (язык версии 15 требуетweb-tree-sitter >= 0.25). Подробности, источник и SHA-256 —src/vendor/README.md.@restranger/repi-lib— контракты и seam.