Filesystem Policy: заметки к реализации (ветка feat/filesystem-policy)
Сопроводительные заметки к ТЗ docs/filesystem-policy.md:
решения, принятые при встраивании в существующую кодовую базу, влияние на
уже развёрнутые инсталляции и известные риски. Обновляется по мере этапов
(P0 → P1 → P2).
#0. Что уже есть в кодовой базе (корреляция с ТЗ)
| ТЗ | Существующий механизм | Что делаем |
|---|---|---|
| §3.3 структура правил | workspace.Policy (org_policy), измерения моделей/MCP/бюджетов/mode-ов |
Добавляем Filesystem *FilesystemRules (read/write/deny) |
| §3.4 наследование | EffectivePolicy(unitIDs...) → MergePolicies (рестриктивное слияние) |
Файловые правила сливаются тем же проходом |
| §6.1 security context | agent.RunInput несёт policy-констрейнты (RequireToolApproval, ReadOnly, бюджеты); applyRunPolicy в cmd/agent-kernel/main.go решает политику один раз при старте |
Туда же морозим Filesystem + PolicyVersion |
| §6.2 делегирование | kernel.resolve_agent → run-config с actor_id; 4 точки спавна детей (workflow.go plan×2 + delegate, teamrun.go) |
Ребёнок НАСЛЕДУЕТ снимок родителя (ResolveAgentRequest), не пересчитывает |
| §5.2 gateway | kernel/agent/filetools.go: resolveWorkspacePath уже держит containment + symlink-resolve |
Перед операцией — проверка AllowRead/AllowWrite |
| §5.3 sandbox | kernel/sandbox/docker.go: /workspace bind (:ro для read-only), /scratch run-scoped, /tmp tmpfs, cap-drop |
Landlock-обёртка поверх команды (этап P1) |
| §4.1 каноническое пространство | $KERNEL_SANDBOX_ROOT/projects/<project-id> → /workspace (uploads, артефакты, файлы агентов) |
Не меняем — пространство уже каноническое |
| §7 claims/worktree | PlanTask (plan DAG) |
P2: workspace_claims |
#1. Семантика, зафиксированная в P0
nil/отсутствующее измерение = без ограничений (§3.5). Ряд политики безfilesystemне сужает ничего; поведение инсталляции без файловых правил байт-в-байт прежнее.- Пустой непустой список (
[]) = ничего не разрешено — та же развилкаnilvs[], что уже используется вPolicyAllowsList. Нормализация при записи: отправленный пустой список читается какnil(«не задано»). deny— абсолютный приоритет, объединяется ОБЪЕДИНЕНИЕМ по уровням: запрет предка не отменяется дочерней политикой.read/write— семантическое ПЕРЕСЕЧЕНИЕ областей путей (§3.4). Вместо алгебры glob-масок (не замкнуты относительно пересечения) эффективная политика хранит слои: слой = OR-набор масок одной политики, между слоями — AND. Путь разрешён, если попадает во ВСЕ слои. Это точное пересечение без преобразования масок; пример из ТЗ (Global**∩ Unitsrc/**∩ Projectsrc/backend/**=src/backend/**) работает именно так.- Матчер: нормализованные относительные пути,
/как разделитель,**(включая переход через/),*,?,[...]. Регистр значим..., абсолютные пути и пустые маски отвергаются на валидации. writeбезreadне бывает (решение на ревью): область записи — ПЕРЕСЕЧЕНИЕ write- и read-областей; путь, исключённый read-правилами, никогда не бывает доступен на запись, как бы ни читались write-маски. Противоречие между измерениями СУЖАЕТ до пустоты, а не расширяет чтение (fail-closed).PolicyVersion— стабильный отпечаток (sha256/12 hex) нормализованной эффективной политики; пишется вRunInput, вrun.startedи в аудит.
#2. Наследование при делегировании (§6.2–6.3)
Сейчас дети получают конфиг через run-config, который ПЕРЕСЧИТЫВАЕТ политику по org-позиции. Это может ослабить доступ, если политику изменили между стартом родителя и ребёнка. В P0:
applyRunPolicyморозитFilesystem+PolicyVersionвRunInput;ResolveAgentRequestнесёт снимок родителя;ResolveAgentпереопределяет им ребёнка (снимок побеждает свежий пересчёт);- продолжение после turn-limit (
TurnLimitRestarts) и team-слоты идут через тот жеresolve_agent→ тот же механизм.
Так «изменения политики не повышают полномочия уже запущенного процесса» — всё дерево делегирования работает на снимке инициатора.
#3. Влияние на существующие инсталляции
Файлы. Каноническое пространство не меняется (/workspace проекта),
поэтому накопленные файлы, uploads, артефакты и ссылки на них в чате
сохраняют смысл. Миграция файлов не нужна.
Знания и опыт агентов. Knowledge/hints ссылаются на пути и содержимое — пути стабильны (§4.2), ничего не инвалидируется. Единственное изменение поведения: если оператор задаст deny-область, а знания агента ведут в неё — агент получит отказ на операцию (и это ожидаемая семантика, отказ должен сопровождаться внятным сообщением и аудитом, чтобы не выглядеть как поломка).
Политики. Колонка filesystem добавляется аддитивно (JSONB, NULL).
MergePolicies для строк без правил не меняет результат. Существующие
инсталляции после апгрейда ведут себя одинаково до первого файлового правила.
Бегущие workflow (Temporal). RunInput/ToolRequest — JSON-пейлоады;
новые поля с omitempty реплеятся без ошибок, старые run-ы получают
nil = без ограничений. Совместимость в обе стороны сохраняется.
Роль-ограничения (reviewer/qa/read-only). Действующее поведение
(requireWritableWorkspace, :ro маунт) сохраняется как КОНСЕРВАТИВНОЕ
наложение: роль может только СУЗИТЬ (запретить запись), никогда — выдать.
ТЗ запрещает роль как основание выдачи полномочий — сужение этому не
противоречит и защищает старые инсталляции, где роль-ограничения
рассчитываются.
#4. Landlock: результаты экспериментов (этап P1)
Проба: scripts/landlock-probe (самодостаточная, raw-сисвызовы 444–446 —
номера едины на всех архитектурах Linux). Лаунчер: cmd/landlock-launcher
(компилятор правил — kernel/sandbox/landlock).
Проверено на стенде (OrbStack, ядро 7.2.12, Landlock ABI 10, образ песочницы):
- enforcement: чтение/запись/создание внутри разрешённых областей проходят, всё вне — EACCES; deny-зона недоступна и для чтения и для записи;
- наследование: ребёнок, exec'd после
restrict_self, работает под теми же правилами; - изоляционный контур песочницы (
--read-only --cap-drop=ALL --security-opt=no-new-privileges --user, без сети) — Landlock работает; - сисвызовы Landlock не блокируются seccomp-профилем Docker;
- rename/unlink внутри write-области работают (REFER/TRUNCATE включаются по ABI ≥ 2/3, на старых ядрах просто не обрабатываются — без E2BIG);
- WSL2 и современные дистрибутивы Linux Landlock поддерживают (проверено по документации ядра — 5.13+, включая ядра WSL2).
Находки, определившие дизайн:
execveЧИТАЕТ бинарник — read-правила должны покрывать системные каталоги, иначе не запустится ни один инструмент. Лаунчер выдаёт/bin /sbin /usr /lib* /etc /optна чтение,/dev /proc /sys /run /tmp /scratch /var— полностью (технические каталоги, §5.3).- Landlock-правила — чистые allow'ы, вычитание невозможно: deny-подобласть внутри дерева неизбежно лишает предков прав на СОЗДАНИЕ (иначе создание в предке покрыло бы и запрет). Компилятор отдаёт creation-права только subtree-однородным записываемым каталогам — это максимум выразимости механизма, а не упрощение.
- Name-маски (
src/*.go) на уровне ОС: существующие файлы — точно (per-file правила), СОЗДАНИЕ новых имён — консервативный отказ. Gateway ядра при этом точен. Расхождение лаунчер пишет в stderr («policy approximation») — без незаметных упрощений. - Харнесс exec-пламбинга сам упирается в правила (
/dev/nullдля stdin) — при диагностике учитывать, что это enforcement работает. - Fail-closed: лаунчер при любой невозможности применить правила отказывается
запускать команду (exit 125); явный лабораторный обход — только
KERNEL_SANDBOX_LANDLOCK=offна стороне ядра (обёртка не ставится вовсе).
Швы интеграции: снимок политики доезжает через sandbox.Request
(kernel/sandbox/policy.go — wire-зеркало без зависимости runner'а от
storage-пакета), лаунчер стейджится в $KERNEL_SANDBOX_ROOT/.bin/ (единственное
host==container дерево) и монтируется в контейнер; отпечаток политики входит
в fingerprint переиспользуемого контейнера. Легаси-запросы без политики дают
байт-в-байт прежний argv (тест landlock_wrap_test).
Осталось в P1: аудит-события отказов в Journal (§8) и паритетный прогон «запрет в read_file == запрет в shell» в CI-матрице (Linux amd64/arm64, WSL2).
#5. План этапов (соответствие ТЗ §10)
- P0 (эта ветка): модель правил + матчер + рестриктивное слияние,
снимок политики в
RunInput/ToolRequest, наследование при делегировании,PolicyVersionв журнале, тесты идентичности и совместимости. - P1 (в работе, эта ветка): enforcement — ✅ файловые инструменты ядра
(gateway + тип отказа
FilesystemDenied), ✅ Landlock-исследование и лаунчер (fail-closed), ✅ обвязка песочницы (стейджинг, mount, env, fingerprint); осталось: аудит в Journal и CI-матрица паритета. - P2: workspace_claims в
PlanTask, детекция пересечений, git worktree, интеграция с проверками и журналированием. - P3: не-Git изоляция, UI правил и эффективных полномочий, диагностика конфликтов.