omp packages

@restranger/repi-permission-system-core0.0.1-ca9b82c

Security-hardened permission gateway, bash analysis, and path access control for pi-coding-agent

restranger Published 1 hour ago MIT ~598 downloads/mo

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.

Did it work for you?

Result

One report per person. A new report replaces your old one.

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

Что происходит при вызове инструмента

  1. Запрос (PermissionRequest: tool, input, cwd, sessionId, signal) попадает в PermissionEngine.check().
  2. Для bash команда разбирается Tree-sitter в ExecutionUnit-ы (src/bash-ast.ts, см. src/vendor/README.md) и каждый юнит проверяется отдельно: правила path для его путей и редиректов, правила bash, границы внешних каталогов, сессионные гранты.
  3. Для файловых инструментов путь каноникализируется (realpath ближайшего существующего предка), проверяются правила path и правила external_directory*.
  4. Побеждает самое строгое совпавшее правило: deny > ask > rewrite > allow. Не «первое совпадение».
  5. Если итог — ask, плагин показывает диалог; без диалога, при таймауте или ошибке решение становится deny (fail-closed).
  6. 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.