Webhooks Architecture: Why so complex?

Эта система вебхуков спроектирована не просто для уведомлений ("что-то случилось"), а для двусторонней интеграции с AI-агентами.

Обычные вебхуки (fire-and-forget) хороши для Slack-уведомлений, но плохи для автоматического редактирования контента. Мы строим систему, где агенты могут читать, править и коммитить изменения безопасно и эффективно.

Ключевые отличия от "обычных" вебхуков

Фича Зачем нужна (The "Why")
Синхронный ответ Скорость. Агент может вернуть исправления (фиксы опечаток, форматирование) прямо в ответе на вебхук. Экономит 3 лишних HTTP-запроса (Auth -> Get -> Commit) и секунды времени.
Защита от рекурсии (depth) Безопасность. Если агент исправит заметку, это вызовет новый вебхук. Без защиты (max_depth) два агента могут зациклить сервер насмерть. Система сама останавливает цепочку.
Short API Tokens (JWT) Security. Вместо того чтобы давать агенту "ключ от всех дверей" (Master Key), мы даем временный токен (на 1 час) с правами только на текущие заметки. Если токен украдут, ущерб минимален.
Подпись HMAC Доверие. Гарантирует, что данные пришли именно от нас, а не от хакера. Стандарт индустрии (Stripe, GitHub).

Сценарии

  1. AI-Линтер (Sync):

    • Пользователь сохраняет пост.
    • Вебхук летит к Линтеру.
    • Линтер находит ошибку и сразу в ответе возвращает исправленный текст.
    • Сервер применяет правку.
    • Пользователь доволен: "Оно само исправилось!"
  2. Генератор дайджестов (Cron + Async):

    • Раз в сутки Cron Webhook будит Агента-Журналиста.
    • Агент получает токен с доступом read_patterns: ["blog/*"].
    • Агент не спеша читает посты, пишет саммари и пушит новый пост blog/digest.md.

Ещё сценарии

  1. Индексатор (Change + Chain):

    • Линтер (max_depth1) правит пост, пушит с depth1.
    • Индексатор (max_depth=2) видит правку линтера, обновляет поисковый индекс.
    • Линтер НЕ видит правку индексатора (depth2 > max_depth=1). Цепочка остановлена.
  2. Модератор (Change + Event Filter):

    • Webhook с on_create: true, on_update: false, on_remove: false.
    • Срабатывает только на новые заметки. Проверяет содержимое, помечает или скрывает нежелательное.
  3. Бэкап (Cron + Async):

    • Раз в неделю Cron Webhook будит Бэкап-агента.
    • Агент получает токен с read_patterns: ["*"], write_patterns: [] (read-only).
    • Читает все заметки через API, экспортирует в S3/Git.

Ключевые решения

Решение Почему
next_run_at обновляется атомарно с delivery Предотвращает дубликаты при крэше между enqueue и update
TTL shortapitoken = max(timeout_seconds, appconfig TTL) Токен не истечёт раньше таймаута
ON DELETE CASCADE убрать из delivery таблиц Soft delete (disabled_at) делает cascade бесполезным. Cleanup cron чистит deliveries
response_schema — серверная константа Не хранится в БД, сервер включает в payload при отправке
Delivery cleanup: 30 дней webhook_delivery_logs — 1 день (тела ответов агентов бывают в сотни КБ), deliveries — 30 дней, job_statuses — 30 дней
HTTP клиент: fasthttp.Client Проект уже на fasthttp. DoTimeout + AcquireRequest/ReleaseRequest
HTTPS не форсируется, но рекомендуется HTTP допустим для localhost/dev. Документировать риск для production
Batch rollback при expected_hash mismatch Атомарность: все изменения агента применяются или не применяются. Агент получает previous_error при retry

Цепочки доставок (trace)

Один прогон агента порождает следующий: cron пишет заметку → срабатывает change-вебхук → его агент пишет ещё одну → срабатывает следующий. Всё это может обслуживаться разными инстансами fleet, но проходит через trip2g, поэтому цепочка целиком видна здесь.

Каждая строка доставки (обе таблицы) несёт четыре колонки:

Колонка Смысл
trace Идентификатор цепочки — сериализованный корень, cron:4242. Корень несёт собственный id, потомки наследуют его без изменений
parent_kind + parent_id Доставка, чья запись вызвала эту. У корня — null. Пара, а не один id: автоинкременты у двух таблиц независимы
depth_reached Глубина этой доставки в цепочке. Не путать с max_depth вебхука — то настроенный лимит

Родитель не вычисляется по данным: он уже едет в запросе. Скоупленный api_token доставки несёт DeliveryKind/DeliveryID, они штампуются в appreq, и в момент создания дочерней доставки handlenotewebhooks берёт их оттуда. Читается только trace родителя — одним запросом по первичному ключу.

Корнями становятся: cron-запуск (по расписанию или вручную), ручной триггер change-вебхука и любая запись, сделанная человеком, api-ключом или синхронизацией вольта — то есть всё, за чем не стоит доставка.

trace уезжает и в payload доставки, чтобы агент мог пометить им свои собственные логи.

Смотреть: админка → System → «Цепочки доставок». Список — admin { deliveryTraces }, отдельная цепочка — admin { deliveryTrace(trace:) }, оба собирают строки из обеих таблиц доставок.

Отдельная таблица трейсов не заводилась: агрегаты (токены, длительность) считаются на чтении, а чистка достаётся бесплатно от тех же 30-дневных DELETE.

Тестирование

Уровень Инструмент Что покрывает
Unit Go + fasthttputil.InMemoryListener HMAC, shortapitoken, agent response parsing, write access, delivery job, depth
E2E Playwright request API (e2e/webhooks.spec.js) Full flow: pushNotes → webhook fires → verify payload/HMAC → agent response → changes applied

E2E тесты используют debug endpoints (/debug/test_webhook, /debug/wait_all_jobs) и pushNotes через GraphQL (не obsidian-sync CLI). HMAC верификация через Node.js crypto.createHmac.

Подробности: shared_webhooks.md → раздел "Тестирование".

Почему свои debug endpoints, а не готовые решения

Рассматривали request-baskets и webhook-tester, но отказались:

  • Нет ARM-образов — request-baskets только linux/amd64, а dev-среда на ARM.
  • Мало звезд на github — оба в плачевном состоянии, без активной поддержки.
  • Избыточная зависимость — свои endpoints это десяток строк кода, полный контроль над поведением (статус-коды, задержки, кастомные ответы), никаких проблем с совместимостью.

Документация

Подробные технические спецификации:

  • Change Webhooks — триггеры на изменение заметок (create/update/remove).
  • Cron Webhooks — запуск агентов по расписанию.
  • Shared Infrastructure — общие механизмы: Auth (Short Tokens), HMAC подпись, формат ответов, Retry, Debugging, Тестирование.
  • Job Statuses — единый UI для отслеживания всех фоновых задач.