Хуки
Хуки PRX предоставляют событийную систему расширений, позволяющую реагировать на события жизненного цикла во время выполнения агента. Каждый значимый момент в цикле агента -- начало хода, вызов LLM, вызов инструмента, возникновение ошибки -- генерирует событие хука. Действия прикрепляются к этим событиям через конфигурационный файл hooks.json, манифесты WASM-плагинов или HTTP API.
Хуки работают по принципу "отправил и забыл". Они никогда не блокируют цикл агента, никогда не изменяют поток выполнения и никогда не внедряют данные обратно в разговор. Это делает их идеальными для аудит-логирования, сбора метрик, внешних уведомлений и автоматизации побочных эффектов без внесения задержек или точек отказа в основной конвейер агента.
Существует три бэкенда выполнения хуков:
- Shell-хуки -- Запускают внешнюю команду с передачей полезной нагрузки события через переменную окружения, временный файл или stdin. Настраиваются в
hooks.json. - Хуки WASM-плагинов -- Вызывают функцию
on-event, экспортируемую WASM-плагином. Объявляются в манифестеplugin.tomlплагина. - Хуки шины событий -- Публикуют во внутреннюю шину событий по теме
prx.lifecycle.<event>. Всегда активны; настройка не требуется.
События хуков
PRX генерирует 8 событий жизненного цикла. Каждое событие несёт JSON-полезную нагрузку с контекстно-специфичными полями.
| Событие | Когда генерируется | Поля полезной нагрузки |
|---|---|---|
agent_start | Цикл агента начинает новый ход | agent (string), session (string) |
agent_end | Цикл агента завершает ход | success (bool), messages_count (number) |
llm_request | Перед отправкой запроса к LLM-провайдеру | provider (string), model (string), messages_count (number) |
llm_response | После получения ответа LLM | provider (string), model (string), duration_ms (number), success (bool) |
tool_call_start | Перед началом выполнения инструмента | tool (string), arguments (object) |
tool_call | После завершения выполнения инструмента | tool (string), success (bool), output (string) |
turn_complete | Полный ход завершён (все инструменты обработаны) | (пустой объект) |
error | Любая ошибка во время выполнения | component (string), message (string) |
Схемы полезной нагрузки
Все полезные нагрузки являются JSON-объектами. Структура верхнего уровня оборачивает событийно-специфичные поля:
{
"event": "llm_response",
"timestamp": "2026-03-21T08:15:30.123Z",
"session_id": "sess_abc123",
"payload": {
"provider": "openai",
"model": "gpt-4o",
"duration_ms": 1842,
"success": true
}
}Поля event, timestamp и session_id присутствуют в каждом событии хука. Объект payload варьируется в зависимости от типа события, как описано в таблице выше.
Конфигурация
Shell-хуки настраиваются в файле hooks.json, размещённом в каталоге рабочего пространства (том же каталоге, что и config.toml). PRX следит за изменениями этого файла и горячо перезагружает конфигурацию без необходимости перезапуска.
Базовая структура
{
"hooks": {
"<event_name>": [
{
"command": "/path/to/script",
"args": ["--flag", "value"],
"env": {
"CUSTOM_VAR": "value"
},
"cwd": "/working/directory",
"timeout_ms": 5000,
"stdin_json": true
}
]
}
}Каждое имя события отображается на массив действий хука. К одному событию можно прикрепить несколько действий; они выполняются конкурентно и независимо.
Полный пример
{
"hooks": {
"agent_start": [
{
"command": "/usr/local/bin/notify",
"args": ["--channel", "ops", "--title", "Agent Started"],
"timeout_ms": 3000
}
],
"llm_response": [
{
"command": "python3",
"args": ["/opt/hooks/log_latency.py"],
"stdin_json": true,
"timeout_ms": 2000
}
],
"tool_call": [
{
"command": "/opt/hooks/audit_tool_usage.sh",
"env": {
"LOG_DIR": "/var/log/prx/audit"
},
"timeout_ms": 5000
}
],
"error": [
{
"command": "curl",
"args": [
"-X", "POST",
"-H", "Content-Type: application/json",
"-d", "@-",
"https://hooks.slack.com/services/T00/B00/xxxxx"
],
"stdin_json": true,
"timeout_ms": 10000
}
]
}
}Поля действий хуков
Каждый объект действия хука поддерживает следующие поля:
| Поле | Тип | Обязательное | По умолчанию | Описание |
|---|---|---|---|---|
command | string | Да | -- | Абсолютный путь к исполняемому файлу или имя команды из очищенного PATH |
args | string[] | Нет | [] | Аргументы, передаваемые команде |
env | object | Нет | {} | Дополнительные переменные окружения, объединяемые с очищенной средой выполнения |
cwd | string | Нет | каталог рабочего пространства | Рабочий каталог для порождаемого процесса |
timeout_ms | number | Нет | 30000 | Максимальное время выполнения в миллисекундах. Процесс убивается (SIGKILL) при превышении |
stdin_json | bool | Нет | false | При true полная JSON полезная нагрузка события передаётся процессу через stdin |
Замечания по command
Поле command подвергается проверке безопасности перед выполнением. Оно не должно содержать метасимволов shell (;, |, &, `, $()) -- они отклоняются для предотвращения shell-инъекций. Если вам нужны возможности shell, оберните их в файл скрипта и укажите command на этот скрипт.
Относительные пути разрешаются относительно каталога рабочего пространства. Однако для предсказуемости рекомендуется использовать абсолютные пути.
Доставка полезной нагрузки
Действия хуков получают полезную нагрузку события через три канала одновременно. Эта избыточность гарантирует, что скрипты на любом языке могут получить доступ к данным через наиболее удобный метод.
1. Переменная окружения (ZERO_HOOK_PAYLOAD)
JSON-строка полезной нагрузки устанавливается как переменная окружения ZERO_HOOK_PAYLOAD. Это самый простой метод доступа для shell-скриптов:
#!/bin/bash
# Чтение полезной нагрузки из переменной окружения
echo "$ZERO_HOOK_PAYLOAD" | jq '.payload.tool'Ограничение размера: 8 КБ. Если сериализованная полезная нагрузка превышает 8 КБ, переменная окружения не устанавливается, и полезная нагрузка доступна только через временный файл и каналы stdin.
2. Временный файл (ZERO_HOOK_PAYLOAD_FILE)
Полезная нагрузка записывается во временный файл, и путь к файлу устанавливается в переменной окружения ZERO_HOOK_PAYLOAD_FILE. Временный файл автоматически удаляется после завершения процесса хука.
import os, json
payload_file = os.environ["ZERO_HOOK_PAYLOAD_FILE"]
with open(payload_file) as f:
data = json.load(f)
print(f"Tool: {data['payload']['tool']}, Success: {data['payload']['success']}")Этот канал не имеет ограничения размера и является рекомендуемым методом для полезных нагрузок, которые могут быть большими (напр., tool_call с подробным выводом).
3. Стандартный ввод (stdin)
Когда stdin_json установлен в true в действии хука, JSON полезной нагрузки передаётся процессу через stdin. Это полезно для команд, которые нативно читают из stdin, таких как curl -d @- или jq.
#!/bin/bash
# Чтение из stdin (требуется stdin_json: true в конфигурации хука)
read -r payload
echo "$payload" | jq -r '.payload.message'Переменные окружения
Каждый процесс хука получает следующие переменные окружения, помимо ZERO_HOOK_PAYLOAD и ZERO_HOOK_PAYLOAD_FILE:
| Переменная | Описание | Пример |
|---|---|---|
ZERO_HOOK_EVENT | Имя события, вызвавшего этот хук | tool_call |
ZERO_HOOK_SESSION | Идентификатор текущей сессии | sess_abc123 |
ZERO_HOOK_TIMESTAMP | Временная метка события в формате ISO 8601 | 2026-03-21T08:15:30.123Z |
ZERO_HOOK_PAYLOAD | Полная полезная нагрузка как JSON-строка (не задаётся если >8 КБ) | {"event":"tool_call",...} |
ZERO_HOOK_PAYLOAD_FILE | Путь к временному файлу с полезной нагрузкой | /tmp/prx-hook-a1b2c3.json |
Среда выполнения очищается перед запуском процесса хука. Конфиденциальные и опасные переменные окружения удаляются (см. Безопасность ниже), и доступны только перечисленные выше переменные плюс переопределения env из действия хука.
Хуки WASM-плагинов
WASM-плагины могут подписываться на события хуков, экспортируя функцию on-event, определённую в WIT (WebAssembly Interface Types) интерфейсе PRX.
WIT-интерфейс
interface hooks {
/// Вызывается при срабатывании подписанного события.
/// Возвращает Ok(()) при успехе, Err(message) при неудаче.
on-event: func(event: string, payload-json: string) -> result<_, string>;
}Параметр event -- это имя события (напр., "tool_call"), а payload-json -- полная полезная нагрузка, сериализованная как JSON-строка, идентичная тому, что получают shell-хуки.
Паттерны подписки на события
Плагины объявляют, какие события они хотят получать, в своём манифесте plugin.toml с использованием сопоставления паттернов:
| Паттерн | Совпадения | Пример |
|---|---|---|
| Точное совпадение | Одно конкретное событие | "tool_call" |
| Суффикс с подстановкой | Все события, совпадающие с префиксом | "prx.lifecycle.*" |
| Универсальный | Каждое событие | "*" |
Пример манифеста плагина
[plugin]
name = "audit-logger"
version = "0.1.0"
description = "Logs all lifecycle events to an audit trail"
[[capabilities]]
type = "hook"
events = ["agent_start", "agent_end", "error"]
[[capabilities]]
type = "hook"
events = ["prx.lifecycle.*"]Один плагин может объявить несколько блоков [[capabilities]] с разными паттернами событий. Объединение всех совпавших событий определяет, какие события получает плагин.
Модель выполнения
Хуки WASM-плагинов выполняются внутри WASM-песочницы с теми же ограничениями ресурсов, что и другие функции плагинов. На них распространяются:
- Лимит памяти: Определяется в конфигурации ресурсов плагина (по умолчанию 64 МБ)
- Таймаут выполнения: Такой же, как
timeout_msдля shell-хуков (по умолчанию 30 секунд) - Без доступа к файловой системе: Если не предоставлен явно через WASI-возможности
- Без сетевого доступа: Если не предоставлен явно через флаги возможностей
Если WASM-хук возвращает Err(message), ошибка логируется, но не влияет на цикл агента. Хуки всегда работают по принципу "отправил и забыл".
Интеграция с шиной событий
Каждое событие хука автоматически публикуется во внутреннюю шину событий по теме prx.lifecycle.<event>. Это происходит независимо от того, настроены ли shell- или WASM-хуки.
Формат тем
prx.lifecycle.agent_start
prx.lifecycle.agent_end
prx.lifecycle.llm_request
prx.lifecycle.llm_response
prx.lifecycle.tool_call_start
prx.lifecycle.tool_call
prx.lifecycle.turn_complete
prx.lifecycle.errorТипы подписок
Внутренние компоненты и плагины могут подписываться на темы шины событий тремя способами:
- Точная:
prx.lifecycle.tool_call-- получает только событияtool_call - С подстановкой:
prx.lifecycle.*-- получает все события жизненного цикла - Иерархическая:
prx.*-- получает все события домена PRX (жизненный цикл, метрики и т.д.)
Ограничения полезной нагрузки
| Ограничение | Значение |
|---|---|
| Максимальный размер полезной нагрузки | 64 КБ |
| Максимальная глубина рекурсии | 8 уровней |
| Модель диспетчеризации | "Отправил и забыл" (async) |
| Гарантия доставки | Не более одного раза |
Если событие хука вызывает другое событие хука (напр., скрипт хука вызывает инструмент, который генерирует tool_call), счётчик рекурсии увеличивается. На глубине 8 уровней дальнейшие генерации событий тихо отбрасываются для предотвращения бесконечных циклов.
HTTP API
Хуками можно управлять программно через HTTP API. Все эндпоинты требуют аутентификации и возвращают JSON-ответы.
Список всех хуков
GET /api/hooksОтвет:
{
"hooks": [
{
"id": "hook_01",
"event": "error",
"action": {
"command": "/opt/hooks/notify_error.sh",
"args": [],
"timeout_ms": 5000,
"stdin_json": false
},
"enabled": true,
"created_at": "2026-03-20T10:00:00Z",
"updated_at": "2026-03-20T10:00:00Z"
}
]
}Создание хука
POST /api/hooks
Content-Type: application/json
{
"event": "llm_response",
"action": {
"command": "python3",
"args": ["/opt/hooks/track_latency.py"],
"stdin_json": true,
"timeout_ms": 3000
},
"enabled": true
}Ответ (201 Created):
{
"id": "hook_02",
"event": "llm_response",
"action": {
"command": "python3",
"args": ["/opt/hooks/track_latency.py"],
"stdin_json": true,
"timeout_ms": 3000
},
"enabled": true,
"created_at": "2026-03-21T08:00:00Z",
"updated_at": "2026-03-21T08:00:00Z"
}Обновление хука
PUT /api/hooks/hook_02
Content-Type: application/json
{
"event": "llm_response",
"action": {
"command": "python3",
"args": ["/opt/hooks/track_latency_v2.py"],
"stdin_json": true,
"timeout_ms": 5000
},
"enabled": true
}Ответ (200 OK): Возвращает обновлённый объект хука.
Удаление хука
DELETE /api/hooks/hook_02Ответ (204 No Content): Пустое тело при успехе.
Переключение хука
PATCH /api/hooks/hook_01/toggleОтвет (200 OK):
{
"id": "hook_01",
"enabled": false
}Этот эндпоинт переключает состояние enabled. Отключённые хуки остаются в конфигурации, но не выполняются при срабатывании их события.
Безопасность
Выполнение хуков подчиняется нескольким мерам безопасности для предотвращения эскалации привилегий, утечки данных и отказа в обслуживании.
Заблокированные переменные окружения
Следующие переменные окружения удаляются из среды выполнения хуков и не могут быть переопределены через поле env в действиях хуков:
| Переменная | Причина |
|---|---|
LD_PRELOAD | Вектор атаки через внедрение библиотек |
LD_LIBRARY_PATH | Манипуляция путём поиска библиотек |
DYLD_INSERT_LIBRARIES | Внедрение библиотек на macOS |
DYLD_LIBRARY_PATH | Манипуляция путём библиотек на macOS |
PATH | Предотвращение перехвата PATH; предоставляется минимальный безопасный PATH |
HOME | Предотвращение подмены домашнего каталога |
Валидация ввода
- Отклонение нулевых байтов: Любое значение
command,args, ключаenvили значенияenv, содержащее нулевой байт (\0), отклоняется. Это предотвращает атаки внедрения нулевых байтов, которые могут обрезать строки на уровне ОС. - Отклонение метасимволов shell: Поле
commandне должно содержать;,|,&,`,$(или другие метасимволы shell. Это предотвращает shell-инъекции, даже если команда случайно передана через shell. - Обход путей: Поле
cwdпроверяется на отсутствие выхода за пределы каталога рабочего пространства через компоненты...
Применение таймаутов
Каждый процесс хука подчиняется настроенному timeout_ms (по умолчанию 30 секунд). При превышении лимита:
- Процессу отправляется
SIGTERM - После 5-секундного льготного периода отправляется
SIGKILL - Хук помечается как превысивший таймаут во внутренних метриках
- Цикл агента не затрагивается
Изоляция ресурсов
Процессы хуков наследуют те же ограничения cgroup и пространства имён, что и выполнение инструмента shell, когда активен бэкенд песочницы. В режиме песочницы Docker хуки выполняются в отдельном контейнере без сетевого доступа по умолчанию.
Примеры
Хук аудит-логирования
Логирование каждого вызова инструмента в файл для аудита соответствия:
{
"hooks": {
"tool_call": [
{
"command": "/opt/hooks/audit_log.sh",
"env": {
"AUDIT_LOG": "/var/log/prx/tool_audit.jsonl"
},
"timeout_ms": 2000
}
]
}
}/opt/hooks/audit_log.sh:
#!/bin/bash
echo "$ZERO_HOOK_PAYLOAD" >> "$AUDIT_LOG"Хук уведомления об ошибках
Отправка событий ошибок в канал Slack:
{
"hooks": {
"error": [
{
"command": "curl",
"args": [
"-s", "-X", "POST",
"-H", "Content-Type: application/json",
"-d", "@-",
"https://hooks.slack.com/services/T00/B00/xxxxx"
],
"stdin_json": true,
"timeout_ms": 10000
}
]
}
}Хук метрик задержки LLM
Отслеживание времени ответа LLM для мониторинговых панелей:
{
"hooks": {
"llm_response": [
{
"command": "python3",
"args": ["/opt/hooks/metrics.py"],
"stdin_json": true,
"timeout_ms": 3000
}
]
}
}/opt/hooks/metrics.py:
import sys, json
data = json.load(sys.stdin)
payload = data["payload"]
provider = payload["provider"]
model = payload["model"]
duration = payload["duration_ms"]
success = payload["success"]
# Отправка в StatsD, Prometheus pushgateway или любой бэкенд метрик
print(f"prx.llm.duration,provider={provider},model={model} {duration}")
print(f"prx.llm.success,provider={provider},model={model} {1 if success else 0}")Отслеживание жизненного цикла сессий
Отслеживание начала и завершения сессий агента для аналитики использования:
{
"hooks": {
"agent_start": [
{
"command": "/opt/hooks/session_tracker.sh",
"args": ["start"],
"timeout_ms": 2000
}
],
"agent_end": [
{
"command": "/opt/hooks/session_tracker.sh",
"args": ["end"],
"timeout_ms": 2000
}
]
}
}Связанные разделы
- Выполнение shell -- инструмент shell, который часто оборачивают хуки
- Интеграция MCP -- протокол внешних инструментов, генерирующий события
tool_call - Плагины -- система WASM-плагинов, включая возможности хуков
- Наблюдаемость -- метрики и трассировка, дополняющие хуки
- Безопасность -- песочница и движок политик, управляющие выполнением хуков