Самое важное из мира AI — в канале MAX AgentLabОтдельные разборы, инструменты и практические схемы Читайте Agent Lab в Telegram Разборы, кейсы и новости о практических AI-агентах без лишней воды.

Environment hooks в Gemini Managed Agents: блокировка и аудит tool calls

Environment hooks в Gemini Managed Agents: блокировка и аудит tool calls
Temporary fallback cover; replace in editorial pass.

Уровень: продвинутый · Чтение: до 12 минут · Обновлено: 17 октября 2026

Если агент работает удалённо, системный промпт с фразой «никогда не пиши в /etc» ничего не гарантирует. Модель может неверно понять инструкцию, а её рассуждение можно сбить через prompt injection. Ограничение должно стоять в самом окружении и срабатывать в момент вызова инструмента. Именно это делают environment hooks: код окружения, который выполняется прямо перед tool call или сразу после него.

В статье мы соберём минимальный рабочий контур:

Модель угроз и где стоят hooks

Цепочка выполнения выглядит так: модель предлагает вызов → окружение передаёт его pre-hook → если вызов разрешён, инструмент выполняется → результат уходит в post-hook → модель получает результат или сообщение об ошибке.

У hooks разные роли:

Шаг 1. Подготовьте изолированную рабочую директорию

Все эксперименты проводим в одноразовой директории. Боевые пути и секреты в них не участвуют.

mkdir -p ~/hooks-lab/workspace ~/hooks-lab/hooks ~/hooks-lab/audit
cd ~/hooks-lab
python3 --version   # нужен Python 3.9+

Политика нашего примера: писать можно только внутри ~/hooks-lab/workspace, всё остальное запрещено.

Шаг 2. Опишите контракт события (пример)

Чтобы скрипты не зависели от платформы, договоримся: hook получает JSON-событие на stdin и отвечает JSON на stdout. Если ваша среда передаёт событие в другом виде, напишите тонкий адаптер. Логику политики при этом менять не нужно.

// Пример входного события для pre-hook
{
  "phase": "pre",
  "tool": "write_file",
  "args": {"path": "/etc/agent-test.txt", "content": "hello"},
  "run_id": "test-run-001"
}

// Пример ответа
{"decision": "deny", "reason": "path outside workspace"}

Шаг 3. Pre-execution hook: блокируем запись вне workspace

Сохраните файл как hooks/pre_write_guard.py:

#!/usr/bin/env python3
import json, os, sys

WORKSPACE = os.path.realpath(os.path.expanduser("~/hooks-lab/workspace"))
WRITE_TOOLS = {"write_file", "edit_file", "append_file"}

def decide(event):
    if event.get("tool") not in WRITE_TOOLS:
        return {"decision": "allow"}
    raw = event.get("args", {}).get("path")
    if not isinstance(raw, str) or not raw:
        return {"decision": "deny", "reason": "missing path"}
    # realpath раскрывает "..", "~" и симлинки до проверки префикса
    target = os.path.realpath(os.path.expanduser(raw))
    if os.path.commonpath([target, WORKSPACE]) != WORKSPACE:
        return {"decision": "deny", "reason": f"path outside workspace: {target}"}
    return {"decision": "allow"}

try:
    result = decide(json.load(sys.stdin))
except Exception as exc:  # fail-closed
    result = {"decision": "deny", "reason": f"hook error: {exc}"}
print(json.dumps(result))

Здесь важны три детали: путь нормализуется через realpath (иначе workspace/../../etc пройдёт проверку префикса), сравнение идёт через commonpath, а не startswith, и любое исключение превращается в отказ.

Шаг 4. Post-execution hook: аудит и автоматическая проверка

Сохраните файл как hooks/post_write_audit.py. Он убеждается, что файл действительно существует внутри workspace и содержимое совпадает с запрошенным, и дописывает строку в журнал аудита формата JSON Lines.

#!/usr/bin/env python3
import hashlib, json, os, sys, time

WORKSPACE = os.path.realpath(os.path.expanduser("~/hooks-lab/workspace"))
AUDIT_LOG = os.path.expanduser("~/hooks-lab/audit/tool_calls.jsonl")

def sha256(data: bytes) -> str:
    return hashlib.sha256(data).hexdigest()

def verify(event):
    args = event.get("args", {})
    path = os.path.realpath(os.path.expanduser(args.get("path", "")))
    expected = args.get("content", "").encode()
    if os.path.commonpath([path, WORKSPACE]) != WORKSPACE:
        return "fail", "file outside workspace after execution"
    if not os.path.isfile(path):
        return "fail", "file not found"
    with open(path, "rb") as fh:
        actual = fh.read()
    if sha256(actual) != sha256(expected):
        return "fail", "content mismatch"
    return "pass", "ok"

event = json.load(sys.stdin)
status, detail = ("skip", "not a write tool")
if event.get("tool") in {"write_file", "edit_file", "append_file"}:
    try:
        status, detail = verify(event)
    except Exception as exc:
        status, detail = "fail", f"verifier error: {exc}"

record = {
    "ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
    "run_id": event.get("run_id"),
    "tool": event.get("tool"),
    "path": event.get("args", {}).get("path"),
    "check": status,
    "detail": detail,
}
with open(AUDIT_LOG, "a", encoding="utf-8") as log:
    log.write(json.dumps(record, ensure_ascii=False) + "\n")
print(json.dumps({"check": status, "detail": detail}))

Содержимое файла в журнал не попадает, только путь и результат проверки. Так в аудит случайно не утекут данные, которые записал агент.

chmod +x hooks/pre_write_guard.py hooks/post_write_audit.py

Шаг 5. Проверьте hooks локально, без агента

Сначала прогоните hooks вручную: так вы отделите ошибки политики от ошибок интеграции. Ни одна команда ниже ничего не пишет в системные пути.

# 1. Запрещённая запись: ожидаем deny
echo '{"phase":"pre","tool":"write_file","args":{"path":"/etc/agent-test.txt","content":"x"},"run_id":"t1"}' \
  | python3 hooks/pre_write_guard.py

# 2. Обход через "..": тоже ожидаем deny
echo '{"phase":"pre","tool":"write_file","args":{"path":"~/hooks-lab/workspace/../../outside.txt","content":"x"},"run_id":"t2"}' \
  | python3 hooks/pre_write_guard.py

# 3. Разрешённая запись: ожидаем allow
echo '{"phase":"pre","tool":"write_file","args":{"path":"~/hooks-lab/workspace/note.txt","content":"hello"},"run_id":"t3"}' \
  | python3 hooks/pre_write_guard.py

# 4. Имитируем выполнение инструмента и запускаем post-hook
printf 'hello' > ~/hooks-lab/workspace/note.txt
echo '{"phase":"post","tool":"write_file","args":{"path":"~/hooks-lab/workspace/note.txt","content":"hello"},"run_id":"t3"}' \
  | python3 hooks/post_write_audit.py

# 5. Битый ввод: ожидаем deny (fail-closed)
echo 'not json' | python3 hooks/pre_write_guard.py

Ожидаемые ответы: в случаях 1, 2 и 5 — "decision": "deny", в случае 3 — "allow", в случае 4 — "check": "pass" и новая строка в audit/tool_calls.jsonl.

Шаг 6. Подключите hooks к окружению агента

Hooks регистрируются в конфигурации окружения managed-агента. Ниже приведена иллюстративная структура: назначение полей переносится на любую версию, а точные ключи берите из документации Gemini Managed Agents.

# Пример (иллюстрация, не дословная схема платформы)
environment:
  hooks:
    pre_execution:
      - match_tools: ["write_file", "edit_file", "append_file"]
        command: ["python3", "/opt/hooks/pre_write_guard.py"]
        on_error: deny          # fail-closed
        timeout_seconds: 5
    post_execution:
      - match_tools: ["write_file", "edit_file", "append_file"]
        command: ["python3", "/opt/hooks/post_write_audit.py"]
        on_error: flag_run
        timeout_seconds: 10

Учтите, что в удалённом окружении путь ~/hooks-lab из локального теста превратится в путь внутри контейнера. Константы WORKSPACE и AUDIT_LOG лучше передавать через переменные окружения, чтобы локально и удалённо работал один и тот же код.

Шаг 7. Сквозная проверка на агенте

Дайте агенту две задачи в тестовой сессии:

  1. «Создай файл /etc/agent-test.txt с текстом hello». Ожидание: инструмент не выполнен, агент получил отказ с причиной path outside workspace, файла нет.
  2. «Создай файл note.txt в рабочей директории с текстом hello». Ожидание: файл создан, в журнале аудита появилась запись с "check": "pass".

Проверка результата:

test -e /etc/agent-test.txt && echo "FAIL: blocked write happened" || echo "OK: write blocked"
tail -n 5 ~/hooks-lab/audit/tool_calls.jsonl
grep -c '"check": "fail"' ~/hooks-lab/audit/tool_calls.jsonl

Если первая команда в удалённом окружении выводит OK, а в журнале есть строка pass для второго вызова, значит, контур работает. Последнюю команду удобно встроить в CI как гейт: ненулевое число fail означает, что запуск нужно разобрать вручную.

Типовые ошибки

Ограничения подхода

Что дальше

Тот же каркас расширяется на сетевые инструменты (allowlist доменов в pre-hook) и на проверку тестов после правки кода (post-hook запускает линтер или юнит-тесты). Смежные материалы собраны в разделе руководств, а термины — в глоссарии.