Профили песочниц (Sandbox Profiles) [L1-360]
#1. Цель
Дать агентам разные среды исполнения команд: не один образ на всю инсталляцию, а каталог профилей (Go, Node, Python, браузерные E2E), которые администратор собирает и закрепляет, а владелец агента назначает агенту по типу задачи.
#2. Проблематика текущего состояния
- Поле
sandbox_profileкарточки агента (restricted | standard | privileged) мертво: оно сохраняется, показывается в UI и передаётся в компиляцию промпта, но ни разу не читается. Реальные ограничения контейнера одинаковы для всех агентов. - Образ песочницы один на инсталляцию (
KERNEL_SANDBOX_IMAGEилиworkspace_setting.sandbox_image):golang:1.27-alpine+ git, ssh, curl, jq, bash, python3, ripgrep. QA-агенту нужен Node и Playwright — их в образе нет и без смены образа быть не может. - Кэш-монтирование
/cacheзашито под Go (GOCACHE,GOMODCACHE): другим тулчейнам кэши не достаются. - Ресурсы контейнера (память, CPU, размер tmpfs) — глобальные env-параметры раннера; браузерным сборкам нужно больше, а повышать всем нельзя.
#3. Основные понятия
#3.1. Профиль песочницы (Sandbox Profile)
Сущность workspace:
| Поле | Смысл |
|---|---|
id |
стабильный слаг (node, browser-e2e) |
name, description |
человекочитаемые название и назначение |
image |
digest-pinned ссылка на образ (repo@sha256:…); пусто, пока не собран |
toolchains |
теги тулчейнов (go, node, python, browser) — для UI и подсказок |
memory, cpus, scratch_size, tmp_size |
ресурсные переопределения; пусто = значения инсталляции |
cache_env |
KEY=VALUE-переменные кэшей, указывающие в /cache (NPM_CONFIG_CACHE=/cache/npm) |
builtin |
встроенный профиль каталога; нельзя удалить, можно пересобрать и подстроить ресурсы |
#3.2. Резолвинг на старте рана
Цепочка: профиль агента → пусто = профиль инсталляции (образ раннера,
текущее поведение без изменений). Разрешение происходит при построении
RunInput (все пути запуска: задачи, триггеры, чат, делегирование через
run-config). Результат — SandboxSpec (образ + ресурсы + кэш-переменные),
который едет с раном до границы исполнения.
#3.3. Граница исполнения
run_command собирает sandbox.Request из SandboxSpec: образ,
ресурсы и кэш-переменные применяются на каждый вызов команды. Профиль
не попадает в промпт и не виден модели как отдельная сущность — среда
исполнения это инфраструктура, а не знание агента.
#4. Принципы
- Безопасность не зависит от профиля. Изоляция одна для всех:
read-only rootfs,
--cap-drop=ALL,no-new-privileges, pids-limit, noexec/tmp, uid 10001. Профиль меняет только тулчейн и ресурсы. - Только digest. Любой образ — встроенной сборки или внешний —
закреплён полным
@sha256:…и запускается с--pull=never(docs/sandbox-security-matrix.md). Тег без digest не принимается. - Молчаливый фолбэк запрещён. Если агенту назначен профиль, чей образ не собран/отсутствует, команда падает с явной ошибкой «профиль не собран», а не выполняется в чужом окружении: тихая подмена тулчейна хуже видимого сбоя.
- Профиль — не привилегия. «Привилегированных» профилей нет:
мёртвая тройка
restricted/standard/privilegedисчезает. Уровни доступа остаются там, где они реально работают:network_access,read_only, орг-политики, квоты бюджета. - Сборка — административное действие. Пересборка образа профиля меняет то, что могут исполнять команды агентов, поэтому попадает в аудит и журнал событий.
- Рецепт композируется на известных базах — кроме оператора.
Кастомный профиль по умолчанию собирается платформа из известных баз
(§5.1), базовые образы не выдумываются. Оператор (и только оператор,
роль writer+) может задать полный рецепт — текст Dockerfile в поле
dockerfile_override(§5.4), включая любойFROMи multi-stage сборки. Это осознанный обмен: mutable-теги внутри редактируемогоFROMпринимаются на себя оператором; компенсирующий контроль не снимается — результат сборки по-прежнему пинится@sha256:…и запускается с--pull=never. Модель такого рецепта не пишет: поле отсутствует в контракте черновика визарда (§5.2).
#5. Каталог встроенных профилей
| id | Основа | Тулчейны | Особенности |
|---|---|---|---|
node |
node:22-alpine + git, curl, jq, bash, ripgrep |
node | NPM_CONFIG_CACHE=/cache/npm |
python |
python:3.12-alpine + git, curl, jq, bash, ripgrep |
python | PIP_CACHE_DIR=/cache/pip, UV_CACHE_DIR=/cache/uv |
browser-e2e |
mcr.microsoft.com/playwright:v1.49.0-jammy |
node, browser | память 2g, scratch 1g, /cache/ms-playwright под браузеры |
Профиль по умолчанию (пустое значение) — текущий образ инсталляции;
отдельной строки каталога для него нет, UI показывает его как
«По умолчанию». Dockerfile-ы встроенных профилей живут в коде ядра
(kernel/sandbox), как и базовый EmbeddedDockerfile.
#5.1. Кастомные профили на рецепте (base + packages)
Кастомный профиль — это либо внешний digest-pinned образ, либо рецепт:
известная база (base ∈ base, node, python, browser-e2e) плюс
дополнительные OS-пакеты (packages). Рецепт собирается платформой:
kernel/sandbox.CustomDefinition берёт Dockerfile базы и добавляет один
слой установки пакетов (apk для alpine-баз, apt для browser-e2e); тег —
temporality-sandbox-<profile-id>:latest, digest закрепляется в строке
профиля после сборки.
Ограничения, общие для API и UI:
baseиimageвзаимоисключающие: у рецепта образ появляется только через сборку — рукописный digest при заданномbaseотвергается;packagesтребуютbase; имена пакетов — строгий allowlist[a-z0-9][a-z0-9.+_-]*(интерполяция в RUN-слой = защита от инъекций);- правка рецепта (
base/packages/dockerfile_override) сбрасывает закреплённыйimage— digest перестал соответствовать рецепту, профиль ждёт пересборки; правка runtime-параметров (memory, cpus, scratch/tmp, cache_env, docker_socket) не сбрасывает собранный образ — «нужна сборка» появляется только после изменения рецепта; - колонки
base,packages— в squashed baseline0001_baseline_v2026.2.21.
#5.2. Конструктор из описания (визард)
Тот же паттерн, что у визардов агентов и команд: оператор описывает
задачи агента обычным языком, модель-супервайзер (POST …/builder)
возвращает черновик — базу из закрытого списка, пакеты, тулчейны и лимиты
по классу нагрузки (light 512m/0.5 → builds 2g/2 → e2e 4g/4, потолок
8g/8). Черновик не сохраняется сам: он открывается в обычной форме
профиля, оператор правит и сохраняет, сборка — штатным build-эндпоинтом.
#5.3. Docker-сокет хоста (DooD, опция профиля)
Флаг docker_socket у кастомного профиля монтирует сокет хостового
демона (/var/run/docker.sock) в каждую песочницу профиля вместе с
дополнительной группой, равной GID сокета: пользователь песочницы
получает ровно тот доступ к демону, который хост выдал этой группе —
не больше. DOCKER_HOST закрепляется на смонтированный сокет, флаги
изоляции (--cap-drop=ALL, no-new-privileges, read-only rootfs)
сохраняются — профиль получает доступ к демону, но не привилегии
контейнера.
Это осознанный размен изоляции: сокет демона эквивалентен root на
хосте (-v /:/host, --privileged-сиблинги). Включать только для
доверенной автоматизации, которой нужен сам docker (например, агенты,
развивающие compose-стек платформы). Настоящий DinD (dockerd внутри
песочницы) сознательно не поддерживается — ему нужен privileged-
контейнер, уничтожающий модель изоляции.
Ограничения:
- только кастомные профили; builtin-строки флаг не принимают;
- флаг не входит в промпт визарда (§5.2) — модель не может его предложить и не видит его в черновиках; решает оператор в форме профиля;
- при старте команды сокет проверяется: отсутствие сокета при включённом флаге — явная ошибка команды, а не загадочный сбой монтирования;
- колонка
docker_socket— в squashed baseline0001_baseline_v2026.2.21.
#5.4. Операторский рецепт (свой Dockerfile)
У кастомного профиля вместо base/packages может стоять
dockerfile_override — полный текст Dockerfile, который оператор пишет
сам («operator recipe», решение D1 задачи полного редактирования,
docs/tasks/2026-10-21-sandbox-dockerfile-editing):
- источники рецепта взаимоисключающие (D2.1):
base[+packages]XORdockerfile_overrideXOR внешнийimage; смешение — ответ 422 с явной ошибкой, молчаливые подмены запрещены; - валидация текста (D3, авторитетная в
kernel/sandbox.OverrideDefinition, зеркально — при сохранении): непустой, ≤ 16 KiB, UTF-8 без NUL, минимум одинFROM(multi-stage разрешён), запрещеныADD,COPYбез--fromиONBUILD— сборка идёт без контекста (docker build -t <tag> -, Dockerfile через stdin); каждая ошибка называет строку; POST …/previewпринимает опциональныйdockerfile_overrideи возвращает либо валидированный текст, либо 422 с ошибкой построчно;POST …/buildсобирает текст verbatim, тег —temporality-sandbox-<id>:latest; пиннинг digest и--pull=neverне меняются;- любая правка текста сбрасывает закреплённый
imageпрофиля (D2.3) — профиль ждёт пересборки; - builtin-профили override не принимают (D2.4): каталог едет с обновлениями ядра;
- модель-визард не заполняет рецепт: поля нет в контракте черновика (§5.2, D2.2); при отказе apk/apt в логе сборки ошибка называет ненайденный пакет и подсказку apt↔apk-алиаса (D4);
- ужесточение по design-ревью безопасности (D7): в тексте дополнительно
запрещены parser-директивы
# syntax=/# escape=(смена frontend — сетевой pull произвольного frontend-образа на хост) иRUN … --network=host(команда сборки в network-неймспейсе хоста) — каждая ошибка называет строку; проверка собранного образа (verifyPinned) переопределяет entrypoint (--entrypoint), чтобы конфигурация образа не исполнялась на проверочном шаге; - миграция 0002 добавляет колонку
dockerfile_override.
#6. API
| Метод | Путь | Доступ | Смысл |
|---|---|---|---|
| GET | /v1/workspace/sandbox-profiles |
reader | каталог с флагом available (образ есть в локальном Docker) |
| POST | /v1/workspace/sandbox-profiles |
writer | создать кастомный профиль: внешний digest-pinned образ или рецепт (база + доп. пакеты) |
| PUT | /v1/workspace/sandbox-profiles/{id} |
writer | изменить кастомный; для встроенного — только ресурсы (memory, cpus, scratch, tmp, cache_env), идентичность каталога и image не трогаются |
| DELETE | /v1/workspace/sandbox-profiles/{id} |
writer | удалить кастомный (встроенный — 409) |
| POST | /v1/workspace/sandbox-profiles/{id}/build |
writer | собрать образ: у встроенного — по каталог-рецепту, у кастомного с base — по составному рецепту; digest закрепляется в строке |
| POST | /v1/workspace/sandbox-profiles/{id}/pull |
writer | скачать внешний образ и закрепить его digest в профиле |
| POST | /v1/workspace/sandbox-profiles/builder |
writer | конструктор: текстовое описание задач агента → черновик профиля от модели-супервайзера (не сохраняет, только предлагает) |
События журнала: sandbox.profile.built, sandbox.profile.created,
sandbox.profile.updated, sandbox.profile.deleted; те же действия
пишутся в аудит. Назначение профиля агенту — обычное изменение карточки
агента.
#7. Наблюдаемость
exec_contextрана несётsandbox_profile— видно, в какой среде работал ран.- События
run_commandне меняются: образ детерминирован профилем изexec_context, дублировать его в каждой команде не нужно.
#8. UI
- Настройки → Песочницы: каталог профилей (название, тулчейны, digest, ресурсы, статус «собран/не собран»), кнопка «Собрать» для встроенных и кастомных на рецепте, создание/удаление кастомных; правка ресурсов — и для встроенных (memory, cpus, scratch, tmp, cache_env), и для кастомных. Пересборка встроенного профиля сохраняет настроенные ресурсы. Кнопка «Из описания» открывает конструктор (§5.2): описание задач → черновик в форме с обоснованием от модели; у кастомного профиля в форме выбор базы и доп. пакетов вместо ручного образа.
- Карточка агента: вместо мёртвого селекта — выбор профиля из каталога со статусом сборки; «По умолчанию» = образ инсталляции.
- Визард создания агента больше не предлагает уровень песочницы: среда исполнения назначается профилем осознанно, а не генерируется.
#9. Фазы реализации
#Фаза 1 — реестр и назначение (реализовано)
- Сущность
workspace_sandbox_profile(squashed baseline0001_baseline_v2026.2.21); мёртвые значенияrestricted/standard/privilegedв карточках агентов мигрируются в пустое. - Каталог встроенных профилей (node, python, browser-e2e) с сидингом.
SandboxSpecвRunInput/ToolRequest; образ, ресурсы и кэш-переменные вsandbox.Request.- Резолвинг профиля на старте рана,
sandbox_profileвexec_context. - API каталога + сборка + аудит/события; валидация при сохранении агента.
- UI: страница «Песочницы», селект профиля в карточке агента.
#Фаза 2 — управление образами (частично)
- Конструктор образов из компонентов (по мотивам devcontainer features): базовый профиль + слои тулчейнов без ручного написания Dockerfile — ✅.
- Внешние реестры образов: pull через digest с pinning — ✅ (
POST …/pull); сканирование образов перед закреплением — нет. - Версии образов профиля: digest в карточке + история сборок — ✅.
#Фаза 3 — политики
- Org-политики разрешённых профилей по структурным единицам.
- Альтернативные рантаймы изоляции (gVisor) как опция профиля — только при реальной мультиарендной нагрузке.
#10. Критерии готовности (фаза 1)
- Агенту назначен
browser-e2e, образ собран →run_command node -vотвечает версией Node;/cache/npmсохраняется между командами. - Профиль назначен, образ не собран →
run_commandвозвращает явную ошибку про несобранный профиль; ран не стартует в чужом образе. restricted/standard/privilegedнигде не принимаются: сохранение агента с таким значением — 422.- Все образы в
sandbox.Request— только@sha256:…; тест на отказ от тега. exec_contextрана с профилем содержитsandbox_profile.- Сборка профиля доступна только writer; пишет аудит и событие журнала.
go test ./... -count=1зелёный; UI собирается, локализация ru/en.
#11. Открытые вопросы
- Проектный профиль по умолчанию (проект → свой базовый образ) — нужен ли, если у команды один основной тулчейн? Решение отложить до запросов.
- Автосборка недостающего профиля при назначении (долгий docker build в момент сохранения карточки агента) — пока намеренно нет.
- Предустановка тулчейнов нескольких типов в одном образе (Nix/devbox) как альтернатива каталогу — оценить в фазе 2 по реальным болям.
#12. Архитектурные ограничения
- Рантайм остаётся Docker: замена на microVM (Firecracker, Kata) не рассматривается до мультиарендных инсталляций.
- Значения кэш-переменных указывают только в
/cache— персистентный пер-проектный кэш вне git-workspace; значения вне/cacheотвергаются. - Digest-закрепление обязательно для всех профилей, включая кастомные.
#13. Деплой: воркспейс и оркестраторы
Контракт sibling-контейнеров (ядро стартует песочницу через host-демон и передаёт хостовый абсолютный путь воркспейса) задаёт модель деплоя:
- docker compose (стенд/self-hosted, текущий путь): корень песочниц —
bind-mount постоянного каталога хоста (по умолчанию
/srv/temporality/sandbox,TEMPORALITY_SANDBOX_ROOTпереопределяет). Дефолт/tmpбыл ошибкой и убран: systemd-tmpfiles вычищал воркспейсы при перезагрузке хоста. - Kubernetes (дорожная карта, целевая архитектура): bind-mount —
антипаттерн (привязка пода к ноде, нет multi-replica), поэтому песочничная
команда становится Job'ом через K8s API, воркспейс — PVC
(ReadWriteMany; либо RWO + co-scheduling). Раннер уже за интерфейсом
sandbox.Swappable— добавляется реализацияkernel/sandbox/k8s.go; отдельный спроектированный релиз, не конфигурация. - Docker-in-Docker sidecar — компромиссный вариант для k8s без K8s API прав: воркспейс — PVC пода, но dind требует privileged.
Миграция существующих воркспейсов при переезде на k8s — копирование
каталога песочниц в PVC (пути внутри /workspace не меняются).