temporality
Изменить страницу↗
Инженерные документы

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

#2. Наследование при делегировании (§6.2–6.3)

Сейчас дети получают конфиг через run-config, который ПЕРЕСЧИТЫВАЕТ политику по org-позиции. Это может ослабить доступ, если политику изменили между стартом родителя и ребёнка. В P0:

Так «изменения политики не повышают полномочия уже запущенного процесса» — всё дерево делегирования работает на снимке инициатора.

#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, образ песочницы):

Находки, определившие дизайн:

  1. execve ЧИТАЕТ бинарник — read-правила должны покрывать системные каталоги, иначе не запустится ни один инструмент. Лаунчер выдаёт /bin /sbin /usr /lib* /etc /opt на чтение, /dev /proc /sys /run /tmp /scratch /var — полностью (технические каталоги, §5.3).
  2. Landlock-правила — чистые allow'ы, вычитание невозможно: deny-подобласть внутри дерева неизбежно лишает предков прав на СОЗДАНИЕ (иначе создание в предке покрыло бы и запрет). Компилятор отдаёт creation-права только subtree-однородным записываемым каталогам — это максимум выразимости механизма, а не упрощение.
  3. Name-маски (src/*.go) на уровне ОС: существующие файлы — точно (per-file правила), СОЗДАНИЕ новых имён — консервативный отказ. Gateway ядра при этом точен. Расхождение лаунчер пишет в stderr («policy approximation») — без незаметных упрощений.
  4. Харнесс exec-пламбинга сам упирается в правила (/dev/null для stdin) — при диагностике учитывать, что это enforcement работает.
  5. 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)