MODULE_04 · УРОК 4.2

Local-реализация

Protocol сам по себе не читает файлы. Нужен скучный бэкенд — тот же subprocess, что был в модуле 2, только спрятан за Sandbox. Скучный — хорошо: это база, с которой сравнивают memory и cloud.

ЧТО ПОЛУЧИТСЯ sandbox_local.py с create_local_sandbox(dir). Агент снова запускается и ведёт себя как в модуле 3 — но plumbing другой.

Слова, которые встретятся

  • Local sandbox — команды на вашем диске, в вашей рабочей папке. Изоляции нет: rm -rf всё ещё возможен (если approval пропустит).
  • Фабрикаcreate_local_sandbox возвращает объект, который удовлетворяет Sandbox.

Быстрый путь

  1. Создать sandbox_local.py.
  2. В main.py: sandbox = create_local_sandbox(cwd), agent.run(..., deps=sandbox).
  3. Прогнать знакомые промпты — результат как раньше.

Какие файлы трогаем

Файл Что делать
sandbox_local.py Создать. create_local_sandbox — read_file + exec + stop.
main.py Править. Создать sandbox, передать в tools и в deps=.
local_bash.py Можно удалить или оставить — логику exec переносим в sandbox_local.
prompts.py sandbox_type: sandbox.type (обычно "local").

Шаг 1. Реализация

sandbox_local.py

import subprocess
from dataclasses import dataclass
from pathlib import Path

from sandbox import ExecResult


@dataclass
class LocalSandbox:
    type: str
    working_directory: str
    _root: Path

    async def read_file(self, path: str) -> str:
        return (self._root / path).read_text(encoding="utf-8")

    async def exec(self, command: str) -> ExecResult:
        try:
            proc = subprocess.run(
                command,
                shell=True,
                cwd=self._root,
                capture_output=True,
                text=True,
                timeout=30,
            )
            out = (proc.stdout or "") + (proc.stderr or "")
            return {"stdout": out.strip(), "exit_code": proc.returncode}
        except subprocess.TimeoutExpired:
            return {"stdout": "Таймаут: больше 30 секунд.", "exit_code": 124}

    async def stop(self) -> None:
        pass  # локально нечего убирать


def create_local_sandbox(directory: Path) -> LocalSandbox:
    root = directory.resolve()
    return LocalSandbox(
        type="local",
        working_directory=str(root),
        _root=root,
    )

exec не бросает исключение на ненулевой exit code — tools ждут объект с stdout и кодом. Как в модуле 2.

Шаг 2. Подключить в main.py

from sandbox_local import create_local_sandbox

sandbox = create_local_sandbox(cwd)
needs_approval = create_approval({"mode": "interactive"})
bash_fn = create_bash_tool(sandbox, needs_approval)

instructions = build_system_prompt({
    "working_directory": sandbox.working_directory,
    "sandbox_type": sandbox.type,
    "tool_names": tool_names,
    ...
})

async def main() -> None:
    result = await agent.run(prompt, deps=sandbox)
    print(result.output)
    await sandbox.stop()

На время отладки: print(f"Sandbox: {sandbox.type}") — должно быть local.

ЕСЛИ ПОВЕДЕНИЕ ИЗМЕНИЛОСЬ На тех же промптах ответ должен совпадать с модулем 3. Ищите tool, который всё ещё лезет в Path / subprocess мимо sandbox.

Проверьте

uv run python main.py . "Прочитай main.py, первые 5 строк"
uv run python main.py . "Найди TODO в *.py"
uv run python main.py . "Выполни: ls -la"

В stderr (если добавили print): Sandbox: local. Tools работают, approval на опасных командах — как в модуле 2.

Готово, если:

  • sandbox_local.py экспортирует create_local_sandbox
  • Агент запускается с deps=sandbox
  • read / grep / bash работают как в модуле 3
  • sandbox.type == "local" попадает в промпт