Skip to content

Latest commit

 

History

History
153 lines (119 loc) · 5.68 KB

File metadata and controls

153 lines (119 loc) · 5.68 KB

Outbound webhooks

The gateway POSTs a signed JSON envelope to subscriber URLs when domain events happen. This page is the public reference (event catalogue, payloads, signing, management API) and the technical reference (delivery semantics, package layout) for the webhook surface.

Overview

Event type Emitted when Emitter (Step 3.9)
ingest.completed a document finishes ingesting gateway ingest route
audit.policy_violation a policy decision denies an action PolicyWriter
drift.detected embedding / data drift detected Phase 5 (event type defined now)
eval.regression an eval run regresses Phase 5 (event type defined now)

Subscribers register a URL + the event types they want; every matching active subscription in the tenant receives the event.

The event envelope

{
  "id": "evt_5f3c…",
  "type": "ingest.completed",
  "tenant_id": "acme",
  "created_at": "2026-06-04T10:00:00Z",
  "data": { "...": "event-specific payload" },
  "request_id": "",
  "principal_id": "svc",
  "trace_id": ""
}

data depends on type (e.g. an IngestResult dump for ingest.completed). id is stable across retries — dedupe on it (see delivery semantics).

Signing + verification

Each delivery carries these headers:

Header Value
X-AgentContextOS-Signature t=<unix_ts>,v1=<hex_hmac_sha256>
X-AgentContextOS-Timestamp the signed unix timestamp
X-AgentContextOS-Event the event type
X-AgentContextOS-Delivery the event id

The HMAC is computed over <timestamp>.<raw_body> keyed by the subscription's secret. Verify it (and reject replays) with the shipped helper:

from rag_webhooks import verify

ok = verify(raw_request_body, subscription_secret, signature_header, tolerance_s=300)

tolerance_s rejects deliveries whose signed timestamp is outside the window (replay protection); pass 0 to skip the freshness check.

Delivery semantics

At-least-once. A delivery is retried (default 3 attempts, exponential backoff capped at 30s) until a 2xx or the ceiling, then marked failed and logged. Because the event id is stable across retries, a subscriber may see the same id more than once — make consumers idempotent and dedupe on id.

Emission is fire-and-forget: a slow or failing subscriber never delays or fails the originating request (an ingest still succeeds, a policy decision still returns) — delivery happens on a background task.

Management API

Header identity (X-Tenant-Id + X-Principal-Id, or Authorization: Bearer), same as the other header-identity routes.

Method + path Purpose
POST /v1/webhooks/subscriptions Create a subscription (secret returned once)
GET /v1/webhooks/subscriptions List subscriptions (secrets masked)
GET /v1/webhooks/subscriptions/{id} Describe one (secret masked)
PATCH /v1/webhooks/subscriptions/{id} Update url / event types / description / active toggle (secret stays masked)
DELETE /v1/webhooks/subscriptions/{id} Delete one
POST /v1/webhooks/subscriptions/{id}/test Send a synthetic test event
# create (returns the signing secret once)
curl -sX POST localhost:8000/v1/webhooks/subscriptions \
  -H 'x-tenant-id: acme' -H 'x-principal-id: svc' \
  -d '{"url":"https://example.test/hook","event_types":["ingest.completed"]}'

# fire a test delivery
curl -sX POST localhost:8000/v1/webhooks/subscriptions/whsub_…/test \
  -H 'x-tenant-id: acme' -H 'x-principal-id: svc'

event_types: [] means "all event types". The signing secret is generated server-side if you don't supply one, returned in the create response, and masked (whsec_••••) on every read thereafter.

Configuration (rag.yaml)

webhooks:
  enabled: true
  max_attempts: 3
  base_backoff_s: 0.5
  max_backoff_s: 30.0
  timeout_s: 10.0
  subscriptions:
    - tenant_id: acme
      url: https://example.test/hook
      event_types: [ingest.completed]
      secret: ${ACME_WEBHOOK_SECRET}

ragctl

ragctl webhooks demo                 # sign + deliver a sample event in-process
ragctl webhooks demo --fail 2        # show the retry path
ragctl webhooks demo --event audit.policy_violation

Prints the delivery result + the signed headers, so you can see the HMAC scheme without standing up a gateway.

Internals

  • Wire types + SPIs (rag-core): webhook_types.py (WebhookEvent / WebhookSubscription / WebhookDelivery / WebhookPublisher Protocol), spi/subscription_store.py, spi/webhook_sender.py, noop impls.
  • Delivery engine (rag-webhooks): signing.py (HMAC), retry.py (RetryPolicy), sender.py (HttpWebhookSender), dispatcher.py (WebhookDispatcher — the WebhookPublisher), events.py (payload builders).
  • Emitters: the gateway ingest route schedules ingest.completed; PolicyWriter emits audit.policy_violation on deny when a publisher is wired — both depend only on the WebhookPublisher Protocol.

Extension points

  • Add an event type — add a WebhookEventType value + an events.py builder; emitters call dispatcher.publish(ctx, event).
  • Swap the transport — implement WebhookSender (e.g. an SQS producer); inject it into the dispatcher.
  • Durable subscriptions — implement SubscriptionStore over Postgres; inject via build_app(subscription_store=…).

See also