MODULE_04 · УРОК 4.1

Интерфейс песочницы

Инструменты работают — и слишком много знают. read лезет в Path.read_text, grep — в subprocess, bash — в LocalBashOps. Хотите завтра гонять агента в копии проекта или в облачной VM — переписывать три tools. Сначала опишем контракт «что умеет среда», потом подставим реализацию.

ЧТО ПОЛУЧИТСЯ Файл sandbox.py с Protocol Sandbox. read, grep и bash зовут sandbox.read_file / sandbox.exec, а не файловую систему напрямую. Реализации ещё нет — это нормально.

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

  • Sandbox (песочница) — абстракция среды, где агент читает файлы и гоняет команды. Не «контейнер Docker», а интерфейс в коде.
  • Protocol — «утка»: если у объекта есть нужные методы, он подходит. Без наследования от базового класса.
  • Бэкенд — конкретная реализация: local, memory, cloud. Tools про бэкенд не знают.

Быстрый путь

  1. Создать sandbox.py — Protocol и тип ExecResult.
  2. В tools_bash.py — фабрика принимает Sandbox, не BashOperations.
  3. В main.pydeps_type=Sandbox, tools зовут ctx.deps.
  4. Проверить синтаксис: uv run python -m py_compile ….

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

Файл Что делать
sandbox.py Создать. Protocol Sandbox, ExecResult.
tools_bash.py Править. create_bash_tool(sandbox, needs_approval)sandbox.exec.
main.py Править. Tools через sandbox; deps_type=Sandbox. Пока без create_local_sandbox — агент не запустится end-to-end.
local_bash.py Не удалять — станет деталью local-бэкенда в 4.2.

Шаг 1. Контракт в sandbox.py

sandbox.py

from typing import Protocol, TypedDict


class ExecResult(TypedDict):
    stdout: str
    exit_code: int


class Sandbox(Protocol):
    type: str
    working_directory: str

    async def read_file(self, path: str) -> str: ...
    async def exec(self, command: str) -> ExecResult: ...
    async def stop(self) -> None: ...

Все методы async — даже если local внутри синхронный. Cloud-бэкенд реально async; одна сигнатура для всех.

Поля expires_at и snapshot добавим позже (урок 4.4) — только у cloud. Сейчас интерфейс минимальный: читать, выполнять, остановить.

Зачем каждый метод

Метод Кто зовёт
read_filetool read
execgrep, bash
stopmain.py в конце (пока no-op)
type, working_directoryлоги, промпт (sandbox_type)

Шаг 2. Фабрика bash

В tools_bash.py замените BashOperations на Sandbox:

tools_bash.py — суть

from sandbox import Sandbox, ExecResult


def create_bash_tool(
    sandbox: Sandbox,
    needs_approval: Callable[[str], bool],
) -> Callable[..., str]:
    async def bash(command: str) -> str:
        """… тот же docstring …"""
        if needs_approval(command):
            return (
                f'Заблокировано: "{command}" требует подтверждения '
                f"(режим политики в main.py)."
            )
        result = await sandbox.exec(command)
        return result["stdout"] or "(нет вывода)"

    return bash

ExecResult переехал в sandbox.py. В local_bash.py пока оставьте импорт из sandbox (или поправите в 4.2).

Шаг 3. Tools в main.py

Agent(..., deps_type=Sandbox). В read:

from sandbox import Sandbox

@agent.tool
async def read(ctx: RunContext[Sandbox], path: str, ...) -> str:
    # проверка пути: resolve от working_directory sandbox
    root = Path(ctx.deps.working_directory).resolve()
    abs_path = (root / path).resolve()
    if not str(abs_path).startswith(str(root)):
        return "Ошибка: путь вне рабочей директории."
    if not abs_path.is_file():
        return f"Ошибка: файл не найден: {path}"

    content = await ctx.deps.read_file(path)
    # … нумерация строк как раньше …

В grep — собрать команду grep -rn … и:

    result = await ctx.deps.exec(cmd)
    lines = [ln for ln in result["stdout"].strip().splitlines() if ln]

Проверка «файл существует» остаётся в tool (или в sandbox — на ваш вкус; мы держим в tool, как в модуле 3).

ПОБЕДА — ПОРТАБЕЛЬНОСТЬ, НЕ ПОВЕДЕНИЕ После рефактора (когда появится local в 4.2) агент на тех же промптах ведёт себя так же. Если что-то изменилось — где-то остался прямой вызов subprocess / read_text.
ЕЩЁ НЕ ЗАПУСКАЕТСЯ Реализации Sandbox нет — main.py упадёт при agent.run. Это ожидаемо. Следующий урок — create_local_sandbox.

Проверьте

uv run python -m py_compile sandbox.py tools_bash.py main.py

Без ошибок синтаксиса. В tools не должно остаться read_text / subprocess для read/grep/bash (кроме того, что спрячете внутри local-бэкенда в 4.2).

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

  • sandbox.py экспортирует Sandbox и ExecResult
  • read / grep / bash ходят через sandbox
  • deps_type=Sandbox у агента
  • py_compile проходит