Интерфейс песочницы
Инструменты работают — и слишком много знают. 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 про бэкенд не знают.
Быстрый путь
- Создать
sandbox.py— Protocol и типExecResult. - В
tools_bash.py— фабрика принимаетSandbox, неBashOperations. - В
main.py—deps_type=Sandbox, tools зовутctx.deps. - Проверить синтаксис:
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_file | tool read |
exec | grep, bash |
stop | main.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).
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иExecResultread/grep/bashходят через sandboxdeps_type=Sandboxу агентаpy_compileпроходит