Хранилище песочниц: инвентаризация, квоты и уборка (Sandbox Storage)
Дата: 2026-10-16. Отвечает на два наблюдения из эксплуатации:
- Агенты обмениваются данными через персистентную ФС проекта, и она превращается в неуправляемый объём («сотни гигабайт за недели»), в котором невозможно разобраться.
- Агент заявляет в отчёте «создан артефакт docs/…/runbook.md», имея в виду
файл в воркспейсе, — реестр артефактов при этом пуст, потому что тулы
artifact_create/…не вызваны.
#1. Цель
Сделать персистентное хранилище песочниц управляемым:
- видно — сколько занимает каждый проект, что выросло, что занимает место внутри (top путей), насколько близко к квоте;
- чистится — по явной политике, самим харнесом, без ручного
rmна сервере по ssh; - лимитируется — квота на проект, превышение видно и мешает росту;
- здоровье сервера — диск, CPU, память, объём docker-хранилища — на дашборде, как сигналы деградации;
- артефакт значит артефакт — рабочий продукт для человека регистрируется в реестре; файл в воркспейсе — черновик, а не артефакт.
#2. Проблематика (на момент проектирования)
Фаза 1 (инвентаризация, снапшоты, файловый API) закрыла пункты ниже; уборка и квоты — следующие фазы.
Раскладка сегодня (kernel/sandbox/docker.go, kernel/agent/activities.go):
| Область | Хост-путь | В контейнере | Жизнь |
|---|---|---|---|
| Воркспейс проекта | <root>/projects/<project>/ |
/workspace |
персистентная, общая на проект |
| Кэш тулчейнов | <root>/cache/<project>/ |
/cache |
персистентная, без лимита |
| Загрузки пользователя | <root>/projects/<p>/uploads/ |
/workspace/uploads |
персистентная |
| Входящие intake | <root>/projects/<p>/inbox/ |
/workspace/inbox |
персистентная |
| Стейджинг каналов | <root>/inbound/… |
— | удаляется после обработки |
| /tmp | tmpfs | /tmp |
умирает с контейнером |
| /scratch | <root>/scratch/<project>/<run>/ |
/scratch |
пер-рановая: файлы живут между вызовами run_command одного рана, свипаются по TTL после него |
Проблемы:
- Общий воркспейс на проект — это и обменник, и помойка. Все раны и все
агенты проекта пишут в одно дерево;
run_command/write_file/edit_fileв нём автоподтверждаются (activities.go, sandbox.workspace.v1). Никто не отвечает на вопрос «что здесь лежит и зачем». - Нет инвентаризации. Единственный файловый API — upload/download
конкретного файла (
/v1/workspace/projects/{id}/files). Ни размеров, ни дерева, ни истории роста. - Нет уборки. Кэши тулчейнов (
node_modules,go-mod, pip, playwright) растут неограниченно; брошенные воркспейсы удалённых проектов живут вечно. Исключение —inbound/, который чистится после обработки. - Нет квот. Docker не ограничивает bind-mount; tmpfs ограничены, а воркспейс и кэш — нет. Один проект может занять весь диск, и все раны всех проектов упадут разом.
- Слепое здоровье сервера. Диск/CPU/память не видны нигде; деградация обнаруживается по падению ранов.
- Артефакты-самозванцы. Файл, названный агентом артефактом, не имеет версии, статуса, согласования и прав; ссылка на него живёт в тексте отчёта.
#3. Основные понятия
#3.1. Область хранения (storage area)
Именованный корень под KERNEL_SANDBOX_ROOT: workspace (проекты),
cache (кэши тулчейнов), inbound (стейджинг). Обход и квоты оперируют
областями; вне корня харнес ничего не трогает.
#3.2. Снапшот (storage snapshot)
Результат обхода области: {path, size_bytes, files, dirs, mtime},
агрегированный до глубины N (по умолчанию 3) с топ-подкаталогами.
Снапшот — кэшируемый артефакт вычисления, а не событие на каждый чих:
пересчитывается по крону (раз в час) и по кнопке.
#3.3. Политика удержания (retention policy)
Декларативные правила, кому можно умереть:
cache: не трогать последниеcache_min_age(по умолчанию 7 дней с даты изменения), суммарный капcache_quota_per_project(по умолчанию 20 GiB); при превышении удаляются самые старые подкаталоги кэша до входа в кап.workspace: только мусор по маскам (*.tmp,*.log,.out/,dist/,coverage/…) старшеworkspace_trash_age(по умолчанию 14 дней). Git-контент (отслеживаемые файлы,.git/) не трогается никогда.- Никогда не трогаются:
uploads/,inbox/, пути, чей mtime свежееbusy_grace(по умолчанию 24 ч), и воркспейсы проектов с активными ранами.
Политики — настройки инсталляции (workspace_setting), не код.
#3.4. Квота (quota)
Мягкий лимит: фаза 1 — env KERNEL_STORAGE_DEFAULT_QUOTA_BYTES
(по умолчанию 50 GiB на проект); per-project настройка
(workspace_quota_per_project) и событие storage.quota_exceeded —
фаза 2, не реализованы. Жёсткая грань (фаза 3) — отказ новых
run_command проекта с явной ошибкой «превышена дисковая квота проекта»
до уборки.
#4. Принципы
- Серверная агрегация. Обход ФС и подсчёт размеров — в ядре, с лимитом глубины и времени; фронтенд только рендерит (тот же вывод, что docs/system-dashboard.md §1).
- Ничего не удаляется молча. Каждое удаление — событие журнала
storage.evictedс путём, размером, причиной и политикой; ручная уборка — только operator и только с аудитом. - Уборка не конкурирует с работой. Sweeper проверяет активные раны
проекта (журнал/Temporal) и пропускает занятые; обход идёт с
nice-приоритетом и ограничением по времени. - Всё в sandbox root. Никаких удалений и обходов вне
KERNEL_SANDBOX_ROOT; защита от выходов за корень — та же, что у файлового API. - Файл ≠ артефакт. Рабочий продукт для человека регистрируется тулами артефактов; хранилище — транспорт и кэш, а не система документов.
#5. API
#5.1. Обзор хранилища
GET /v1/workspace/storage (reader, видимость проектов как у списка ранов):
{
"generated_at": "…",
"root": "/srv/temporality/.sandbox",
"disk": { "total_bytes": 1e12, "free_bytes": 2.3e11 },
"projects": [{
"project": "omni",
"workspace_bytes": 41200000000, "workspace_files": 210000,
"cache_bytes": 8100000000,
"quota_bytes": 53687091200, "quota_used": 0.77,
"top_paths": [{ "path": "node_modules", "bytes": 2.1e10 }],
"last_activity_at": "…"
}]
}
Источник — свежий снапшот (кэш в памяти ядра; ?refresh=1 — пересчёт).
#5.2. Детали проекта
GET /v1/workspace/storage/projects/{id}?depth=4 — дерево с размерами и
mtimes до глубины N, топ файлов по размеру. Reader.
#5.3. Ручная уборка (фаза 2, не реализована)
POST /v1/workspace/storage/projects/{id}/sweep
{ "dry_run": true, "areas": ["cache", "workspace"] } (operator).
Возвращает план и факт: что удалено/будет удалено, сколько байт освобождено.
Аудит + событие. Реализованный файловый API (фаза 1): GET dir/file,
POST files, DELETE file — на /v1/workspace/storage/....
#5.4. Здоровье сервера
GET /v1/system/health (reader):
{
"disk": [{ "path": "/srv/temporality/.sandbox", "total_bytes": 1e12, "free_bytes": 2.3e11 }],
"cpu": { "load1": 3.2, "load5": 2.8, "cores": 16 },
"memory": { "total_bytes": 6.7e10, "available_bytes": 1.2e10 },
"docker": { "images_bytes": 2.4e10, "containers_running": 12 }
}
Источник: statfs по корням, /proc/stat+/proc/loadavg, /proc/meminfo
(в стандартном Docker это хостовые значения), docker system df — по
существующему docker-бинарному пути ядра.
#5.5. События журнала
storage.snapshot— итоги планового обхода (по проектам, свободное место) — реализовано;storage.evicted— каждое удаление (путь, байты, политика) — фаза 2;storage.quota_exceeded— проект вышел за квоту — фаза 2;storage.swept— сводка цикла уборки (освобождено байт, длительность) — фаза 2.
#6. Sweeper в ядре
Не внешний cron: ядро уже владеет docker-бинарем, journal и настройками.
- Тикер в
agent-kernel(старт при буте): обход-снапшот раз в час (storage.snapshot,KERNEL_STORAGE_SWEEPER, default on); политика уборки (квоты → кэш → мусор, удаления) — фаза 2, пока обход ничего не удаляет. - Порядок (план): снапшот → квоты (
storage.quota_exceeded) → политика кэша → политика мусора воркспейса →storage.swept. - Каждый шаг логируется slog-ом и журналом; ошибка обхода одного проекта не останавливает остальные.
#7. UI
#7.1. Дашборд (docs/system-dashboard.md, вкладка «Обзор»)
- Виджет «Хранилище»: занятость диска, топ-5 проектов по объёму,
полосы квот (цвет при >80 %/100 %), кнопка «Разобрать» → страница
хранилища. Данные из
GET /v1/workspace/storage. - Виджет «Сервер»: load average, память, свободное место, объём docker-образов. Красные пороги — как у health-полосы.
#7.2. Страница «Хранилище» (Настройки → Хранилище, operator)
Таблица проектов (объёмы, квота, последняя активность, статус последней
уборки) + дерево/топ путей проекта + dry-run и запуск уборки, история
storage.evicted рядом. Для reader — read-only.
#7.3. Карточка проекта
Строка «хранилище: 41 GiB / 50 GiB» со ссылкой на страницу хранилища.
#8. Дисциплина артефактов
Файл в воркспейсе агент создаёт законно (это транспорт); проблема — подмена понятий в отчёте. Решение из двух частей:
- Контракт в системном промпте/навыках: «рабочий продукт для человека —
только через
artifact_create/artifact_submit; файл в /workspace — черновик». Проверяется контуром качества (judge смотрит, зарегистрирован ли заявленный артефакт). - Мост
artifact_attach(фаза 3): новый тул — «сделай артефакт из файла воркспейса». Ядро читает файл из sandbox root, создаёт draft-версию артефакта с телом иorigin_run; дальше обычный жизненный цикл (docs/artifact-lifecycle.md). Это закрывает кейс «агент дописал секцию вdocs/…/runbook.mdи назвал это артефактом» одним вызовом вместо пересылки содержимого через промпт.
#9. Фазы реализации
#Фаза 1 — наблюдаемость (read-only)
- Обход ФС со снапшотами и кэшем;
GET /v1/workspace/storage, детали проекта;GET /v1/system/health. - Виджеты «Хранилище» и «Сервер» на дашборде; события
storage.snapshotпо крону (без удалений). - Критерий: на стенде с реальным проектом видно top-путей и рост за окно.
#Фаза 2 — уборка и мягкие квоты
- Политики retention в
workspace_setting; sweeper (кэш → мусор) + событияstorage.evicted/storage.swept; ручной sweep с dry-run; квоты с предупреждениемstorage.quota_exceeded. - Страница «Хранилище» в настройках.
- Критерий: уборка кэша приводит проект под кап без ручных действий; каждое удаление видно в журнале.
#Фаза 3 — жёсткие квоты и мост артефактов
- Отказ
run_commandпри превышении жёсткой квоты (проверка по свежему снапшоту на старте рана, не на каждую команду). - Тул
artifact_attach; контракт артефактов в промптах и judge-критериях. - Критерий: превышение квоты не валит соседние проекты; заявленный агентом артефакт находится в реестре.
#10. Открытые вопросы
- Удалять ли воркспейсы вместе с удалением проекта (сейчас — нет) и какой grace period.
- Инфраструктурные квоты (xfs project quotas / zfs) против прикладных — прикладные проще, но ловят превышение постфактум.
- Метрики хоста: своего
/proc-парсера достаточно, или в больших инсталляциях ждём существующий node-exporter/Prometheus и только ссылку? - Нужно ли агентам самим тулы инвентаризации (
duв песочнице уже есть через run_command — вероятно, нет).
#11. Архитектурные ограничения
- Обход ФС живёт в ядре и только под
KERNEL_SANDBOX_ROOT; лимит глубины и времени обязателен. - Sweeper — не Temporal workflow: это фоновая подработка процесса ядра, идемпотентная и перезапускаемая.
- Хранилище не становится СУБД: без ACL на файлы, без версионирования файлов — для этого есть реестр артефактов.
- Журнал — единственный источник правды об удалениях; UI не выполняет операций с ФС напрямую.