Завершаем toolbox
У агента уже есть read и grep. Следующий шаг —
дать ему терминал: ls, git status, тесты…
Но тот же терминал умеет и rm -rf / (Ахахах!). В этом уроке добавим
инструмент bash и простую защиту: «разрешено только то,
что в списке».
bash, который запускает только безопасные команды
из белого списка. Остальные не выполняются: функция возвращает текст
«заблокировано», и модель должна честно сказать об этом вам —
а не притворяться, что всё прошло успешно.
Слова, которые встретятся
-
Toolbox — набор инструментов агента
(
read+grep+bash. -
Allowlist (белый список) — список того, что
разрешено. Всё остальное по умолчанию запрещено.
Здесь это
SAFE_PREFIXES. -
Gate / «шлюз» — проверка перед опасным
действием. Как шлагбаум: команда подходит → пропускаем;
не подходит → не запускаем и возвращаем объяснение.
Не отдельная библиотека, а несколько строк
ifв коде. - Approval (подтверждение) — когда система спрашивает человека: «разрешить эту команду?». Полноценно сделаем позже (модуль 8). Сейчас вместо вопроса — жёсткий отказ с текстом.
Быстрый путь
- Добавьте инструмент
bashи список разрешённых начал команд. - Если команда не из списка — не запускайте её, верните понятное сообщение.
- Проверьте: на опасный запрос модель пишет про блокировку, а не «готово, удалил».
Упражнение 1.3
-
Список вроде:
ls,pwd,git status,git log,git diff— в основном команды «посмотреть», а не «сломать». -
Сравнивайте по началу строки:
ls -laначинается сls— значит, подходит. - Поставьте таймаут (например 30 секунд), чтобы зависшая команда не держала агента вечно.
-
В docstring снова: WHEN TO USE / WHEN NOT TO USE / DO NOT USE FOR —
чтобы модель не читала файлы через
cat, а шла вread.
Почему не включаем «спросить пользователя» прямо сейчас
В Pydantic AI у инструмента есть флаг requires_approval=True:
«перед запуском нужно согласие человека». Звучит идеально, но сам по себе
флаг ничего не спрашивает — он только помечает вызов.
Если вы не написали код, который реально останавливается и ждёт «да/нет»,
модель может не получить нормальный ответ от инструмента и
додумать: «Файлы удалены». Для вас это выглядит как успех — хотя команда
даже не запускалась. Это хуже, чем честный отказ.
bash: не в списке → сразу вернуть строку с причиной.
Спросить «разрешить?» научимся в модуле 8.
Проверка прямо в коде инструмента
Идея простая: до вызова subprocess смотрим на команду.
Можно — запускаем. Нельзя — возвращаем текст. Этот текст попадает
обратно в диалог как обычный результат инструмента; модель его читает
и может пересказать вам без вранья.
фрагмент для main.py (добавьте к read и grep)
import subprocess
from pathlib import Path
from pydantic_ai import Agent, RunContext
# Белый список: команда должна НАЧИНАТЬСЯ с одной из этих строк
SAFE_PREFIXES = [
"ls", "cat", "echo", "pwd", "which", "find",
"head", "tail", "wc",
"git log", "git status", "git diff",
]
def is_safe(command: str) -> bool:
cmd = command.strip()
return any(cmd.startswith(prefix) for prefix in SAFE_PREFIXES)
@agent.tool
def bash(ctx: RunContext[Path], command: str) -> str:
"""Выполнить shell-команду в рабочей директории.
WHEN TO USE: сборка, тесты, git, список файлов в каталоге.
WHEN NOT TO USE: чтение известного файла (используй read);
поиск по тексту в проекте (используй grep).
DO NOT USE FOR: чтение файлов (read), поиск по коду (grep).
"""
if not is_safe(command):
allowed = ", ".join(SAFE_PREFIXES)
return (
f'Заблокировано: "{command}" не из белого списка. '
f"Автоматически можно только: {allowed}."
)
try:
proc = subprocess.run(
command,
shell=True,
cwd=ctx.deps,
capture_output=True,
text=True,
timeout=30,
)
out = (proc.stdout or "") + (proc.stderr or "")
if proc.returncode != 0:
return f"Exit {proc.returncode}: {out.strip()}"
return out.strip() or "(нет вывода)"
except subprocess.TimeoutExpired:
return "Таймаут: команда дольше 30 секунд."
Ключевой момент: при блокировке вы всё равно что-то возвращаете — обычную строку. Не молчите и не «проглатываете» вызов. Тогда у модели есть факты, а не дыра, которую она заполняет фантазией.
Проверьте
uv run python main.py . "Перечисли файлы в этой директории"
uv run python main.py . "Выполни команду: rm -rf .venv"
Первый запрос — что-то вроде ls или find, и вы
видите список файлов. Второй — сообщение о блокировке.
Плохой исход: модель пишет «готово, удалил», хотя команда не бежала.
find … -exec rm … вместо прямого rm.
Наш простой «начинается с …» ловит rm, но не любой трюк
с find. В бою списки усложняют (опасные шаблоны, regex).
Здесь оставляем простой вариант — чтобы было ясно, зачем
проверка, а не чтобы закрыть все дыры сразу.
Три инструмента — один агент
| Инструмент | Зачем | Как не навредить |
|---|---|---|
read |
Читать файл | Не больше 500 строк за раз |
grep |
Искать по проекту | Не больше 50 совпадений |
bash |
Команды в терминале | Только из белого списка |
Docstring подсказывает, какой инструмент взять. Лимиты берегут «память» диалога (контекст). Белый список бережёт ваш диск.
Готово, если:
- Безопасные команды (
ls,git status) реально выполняются черезbash - Запрос «прочитай файл» по-прежнему идёт в
read, а не вbash+cat rm -rf,sudoи прочее вне списка возвращают текст блокировки- Модель сообщает о блокировке, а не притворяется, что команда прошла
pip install X. Разрешить?»
Да — выполнить и вернуть вывод. Нет — вернуть «пользователь отказал».
Это как раз сценарий с approval из модуля 8.