Context request webhooks

Context webhooks are an optional wake-up mechanism for an external agent's existing Context request inbox. They are separate from message webhooks and hosted drafts. Nothing subscribes automatically, and enabling hosted drafts takes precedence over that owner's external keys for new requests.

From Chats, choose Agent drafts → Set up webhooks. A direct human session must select one conversation, an owned active read/write key with Context requests already enabled, and a public HTTPS endpoint. The screen reviews that destination and requires Approve this receiver and endpoint before Save receiving agent. The review names the key, conversation, and exact URL. Agent keys, delegated connector tokens and embedded sessions cannot approve endpoints. Membership and key authorization are checked again on creation and before each delivery.

The server flag OPENCHAT_CONTEXT_WEBHOOKS_ENABLED=true enables this optional transport. Its deployment default is false. Polling works independently. This release does not create production subscriptions or send a real webhook as a test. The operator may enable availability independently; each owner must still explicitly approve their own destination in the product. During a rollout that prohibits sends, verify there are no existing active subscriptions or pending deliveries before enabling availability.

Receiver contract

A POST contains only:

{"id":"stable-event-id","event":"context.requested","requestId":"request-id"}

No shared post text, conversation title, participant identity, private notes or messages are in that payload. Fetch the pending request using the receiving key's existing /api/chat/context-agent/requests inbox. Its live ACL checks remain mandatory. A wake-up is not authorization to run tools, disclose private sources, contact someone or publish a reply without the owner's applicable approval.

Verify X-OpenChat-Signature: sha256=<hex> against HMAC-SHA256 using the signing secret and the exact bytes X-OpenChat-Timestamp + "." + rawRequestBody. The timestamp is Unix time in seconds. Check a short timestamp tolerance (for example five minutes), compare the signature in constant time, then deduplicate by body id / X-OpenChat-Event-Id. No raw secret is sent in an HTTP header. Return a 2xx response only after durably accepting the event. Retries preserve the event ID but receive a fresh timestamp/signature.

At-least-once delivery means a crash after receiver acceptance can cause a repeat. There are at most four attempts, with 30-second, two-minute and ten-minute backoff, a 30-second recoverable lease, and at most two simultaneous claimed deliveries. Requests expire after 24 hours. DNS is bounded to two seconds and HTTP to five; private/reserved addresses, credentials, fragments, non-HTTPS URLs and redirects are rejected. DNS answers are pinned for the request. The legacy local webhook exception never enables local Context destinations.

The outbox is created atomically with a new explicit Context request, only while transport availability is enabled, for subscriptions already present. There is no replay of old requests on opt-in. Source revisions, lifecycle closure, blocks, membership removal, request completion, key revocation/expiry/scope changes and opt-out cancel stale deliveries. Unsubscribe cancels pending deliveries and removes the secret. Delivery metadata expires seven days after request expiry; account deletion removes its subscriptions and events. Cleanup continues even while outbound delivery is disabled.

API

All setup routes are human-session-only and Cache-Control: no-store:

Creation is idempotent for the same request and destination, including secret recovery after a lost response. One active subscription per owner/conversation and ten active subscriptions per owner are allowed. A changed destination requires removal and new explicit approval. Existing ordinary message subscriptions are unaffected.

The saved receiving key is the external receiver for that owner in that chat, even if another opted-in key sorts first. Hosted drafts still take precedence when available and enabled. An invalid saved key suspends external routing; it never silently switches to another key. Without a saved enabled subscription, the existing stable key-ID selection remains available for polling. Multiple legacy enabled subscriptions for one owner/chat fail closed until the owner disables the conflicting entries. Disabling and approving a replacement affects only new source revisions: existing requests are never rebound or replayed. Deduplication is per owner and source revision across hosted and external receivers. Toggling preferences does not retry that revision; a new explicit source revision is required. When hosted drafts take precedence, a waiting external wake is cancelled before sending. Its request remains bound to the original key for polling; it is not transferred to the hosted worker.

Both GET and POST subscription objects include server-derived routingStatus:

Status Meaning
ready The configured external receiver is eligible.
hosted_precedence Hosted drafts currently receive new requests instead.
key_ineligible The saved key is missing, revoked, expired, missing scopes, or opted out.
conversation_unavailable The owner no longer belongs to this conversation.
conflict Multiple enabled legacy receivers need explicit resolution.
server_disabled Webhook transport is unavailable; no wake will be sent.
disabled The owner disabled this saved subscription.

enabled records consent, not current delivery readiness. Status is a current configuration snapshot; every Ask and delivery still rechecks live eligibility. Saved subscriptions survive key deletion so the missing receiver remains visible and cannot silently fall back to a different key. No private source is included in setup or wake payloads.

Validation

contextWebhooks.integration.test.ts uses isolated Neo4j records and an injected transport: it creates no production subscriptions and makes no external sends. It covers consent, deduplication, minimal payload, retries, leases, kill switch, expiry, unsubscribe, source changes, membership, key revocation and blocks. contextWebhookSetup.mobile.test.ts checks destination consent and invalidation.

Routing regressions cover multiple opted-in keys, conflicting concurrent setup, legacy ambiguity, suspended/deleted keys, hosted precedence and owner/revision deduplication across explicit receiver changes.

Agent access does not grant permission to approve private sharing, change intention lifecycle, or configure webhook destinations. Those actions require the owner's direct signed-in session.