Sandbox Abstraction

Инструменты агента вызывают интерфейс среды исполнения, а не fs и child_process напрямую. Тогда за одним контрактом стоят несколько сред — локальная, в памяти, облачная, — и смена среды не трогает ни один инструмент. Это Orchestrator Adapter, применённый не к фреймворку оркестрации, а к тому, где агент исполняется.

Проблема, которую это решает

Инструмент, написанный прямо на системных вызовах, знает лишнее: read знает про readFileSync, bash — про execSync, и оба знают, что работают на Node. В момент, когда исполнять надо где-то ещё — в удалённой машине, в виртуальной файловой системе, в контейнере, — переписывать приходится каждый инструмент.

Порядок здесь важнее самого приёма: контракт пишется до реализации. Сначала отвечают на вопрос «что среда обязана уметь, чтобы любой инструмент мог её вызвать», и только потом заводят первую реализацию.

Контракт: чем меньше, тем лучше

Минимальный набор, вокруг которого всё держится:

Метод или поле Зачем Обязателен
readFile(path) прочитать файл да
exec(command){ stdout, exitCode } выполнить команду да
stop() завершиться аккуратно да, пустая реализация допустима
type какая среда — для логов и подсказки да
workingDirectory база для путей да
expiresAt момент истечения аренды нет, только у облачной
snapshot() сохранить состояние нет, только у облачной

Три решения в этом контракте стоят того, чтобы их назвать:

Все методы асинхронны, даже когда реализация синхронна. Локальная среда оборачивает синхронные вызовы без нужды, но облачная асинхронна по-настоящему, а разные сигнатуры у реализаций одного контракта — источник постоянной боли на стороне вызывающего.

Необязательные возможности объявляются необязательными, а не заглушками. Локальная среда не истекает, виртуальная не умеет снимков. Пометить expiresAt и snapshot как опциональные честнее, чем заставлять каждую реализацию возвращать заглушку, которую нельзя отличить от настоящего ответа.

Интерфейс делается настолько малым, насколько получится. Всё, что добавлено сейчас, обязана поддерживать каждая реализация — и та, которой это не нужно, и та, которая появится через год. Это то же соображение, что и в Orchestrator Adapter: абстракция дорожает не в момент написания, а в момент второй реализации.

Наглядный случай — добавить writeFile. Сразу возникает вопрос, что делать средам, куда писать не полагается: заводить ещё один необязательный метод, разделять интерфейс на «читающий» и «пишущий» или бросать исключение. У каждого ответа своя цена, и платится она везде сразу.

Три реализации и чем они различаются

Локальная В памяти Облачная
Стоимость бесплатно бесплатно поминутно
Задержка вызова микросекунды микросекунды десятки-сотни миллисекунд
Изоляция нет частичная: читает настоящее, пишет в память полная, отдельная машина
Сохранность постоянная исчезает при остановке снимок и восстановление
Ограничение по времени нет нет жёсткое, обычно 30–60 минут

Реализация в памяти — не упрощение, а отдельный инструмент. Виртуальная файловая система с семантикой копирования при записи: агент читает настоящие файлы, но всё, что он пишет, живёт в памяти и исчезает при остановке. Это даёт то, чего не дают остальные две, — возможность запускать агента на своём коде, не доверяя ему этот код. Отсюда её главные применения: исследование чужого репозитория, тесты харнесса и разбор недоверенного кода.

Ловушка у неё ровно одна и повторяемая: точка монтирования. Каталог, отданный виртуальной среде, монтируется не в корень и не по своему исходному пути, а по фиксированному внутреннему адресу, и каждый путь внутри приходится переводить. Забывается это всеми и всегда.

Проверка, что рефакторинг был структурным

Свойство, ради которого приём и делается, проверяется одним наблюдением: после перевода инструментов на интерфейс поведение агента на тех же запросах не меняется. Те же инструменты, те же результаты, другая проводка под ними.

Если поведение изменилось — маршрутизация, вывод, новая ошибка, — значит рефакторинг протёк: где-то инструмент по-прежнему тянется к системному вызову мимо контракта. Это дешёвый и надёжный тест, потому что он не требует ни стенда, ни разметки: достаточно прогнать те же запросы, что и до правки.

Выигрыш при этом появляется не здесь, а на второй реализации — когда её подключают, не тронув ни одного инструмента.

Хуки жизненного цикла

Создать среду — половина работы. Свежая удалённая машина не знает вашего пользователя git, в ней нет зависимостей и переменных окружения; а перед выключением надо решить, что делать с несохранённой работой. Для этого — три необязательных хука:

Хук Когда Типичное содержимое
afterStart среда создана и принимает команды настройка git, установка зависимостей, копирование окружения
beforeStop перед выключением проверить незакоммиченное, сохранить, сделать снимок
onTimeout среда упёрлась в лимит времени обычно то же, что beforeStop, плюс запись в журнал

onTimeout вызывается харнессом, а не вами — этим он отличается от первых двух. Локальной среде хуки почти не нужны; облачная без них неупотребима. Подробнее про то, что происходит дальше, — Sandbox Lifecycle.

Граница с соседними заметками

Три заметки про песочницу отвечают на три разных вопроса, и путать их дорого:

  • Agent Sandboxing — чем изолировать: контейнер, микровиртуальная машина, gVisor, WASM. Про надёжность периметра.
  • Эта заметка — какой контракт между инструментами и средой. Про подменяемость.
  • Sandbox Lifecycle — как эксплуатировать арендованную среду: состояния, стоимость простоя, снимки. Про деньги и потерю работы.

Изоляция и подменяемость независимы: локальная реализация не изолирует вовсе, но контракту удовлетворяет; облачная изолирует полностью и удовлетворяет тому же контракту.

Связано с

  • Orchestrator Adapter — тот же приём на другой оси: там граница с фреймворком оркестрации, здесь со средой исполнения
  • Agent Sandboxing — чем изолировать, в отличие от того, какой контракт
  • Sandbox Lifecycle — что происходит со средой дальше: состояния, стоимость, снимки
  • Tool Calling — инструменты, которые этот контракт вызывают
  • Agent Harness — обвязка, для которой среда исполнения одна из точек расширения
  • Bounded Tool Output — второй контракт того же слоя: сколько инструменту позволено вернуть