SPEC-0012: Outbound Webhooks
Graph Edges
- Implements: ADR-0017 — in-process async fan-out of
artifact.createdevents - Related: SPEC-0002 — artifact envelope, provenance, and expiry
- Related: SPEC-0005 — inbound webhook capture; naming neighbor, not a dependency
(Rendered edges are derived from front-matter; body links to repo paths would break on the generated site page.)
Overview
When an artifact (single-body or bundle) is created through any surface — REST API,
web upload, CLI, or MCP — Cairn emits an artifact.created event over HTTP to each
configured outbound webhook target. The initial consumer is Switchboard, whose
generic-trust ingest URLs turn the event into a todo on a scoped queue, ringing a
doorbell on a live agent session. See ADR-0017 for the decision record.
The event also carries the artifact's client-asserted tags, so a consumer can route it
without reading the body. The main case is an agent handoff: a work order tagged
handoff that a Switchboard rule sends to a worker lane (ADR-0018).
Requirements
Requirement: Event Emission on Artifact Creation
The system SHALL emit an a client POSTs an an agent calls the the same a bundle is created via the multipart endpoint an a target is unreachable or the emitter errors the artifact creation response is unaffected and the error is loggedartifact.created event after a single-body artifact or a
bundle is durably created, regardless of the creating surface (REST, web, CLI, MCP).
Emission MUST NOT block or fail the creation request: an emitter failure MUST NOT
change the API response.Scenario: REST creation emits
/v1/artifacts and receives 201artifact.created event for the new artifact is delivered to every
configured targetScenario: MCP creation emits
artifact_create MCP tool successfullyartifact.created event shape is emitted as for REST creationScenario: Bundle creation emits
artifact.created event with share_type: "bundle" is emittedScenario: Emitter failure is isolated
Requirement: Event Payload
The event body SHALL be a JSON object with at minimum:
source("cairn");kind("artifact.created");event_id(unique per event);created_at(RFC 3339);- a
dataobject carrying the artifact'sid,share_type,title, weburl,channel,model(when present),actor_id(when present),expires_at(when present),on_behalf_of(when present), andtags(when the artifact has any).
on_behalf_of is the connected MCP client's initialize name/version. The server
records it from the session, never from a request field, but the client reports it
about itself, and it is empty for REST/CLI creates (SPEC-0007). tags is the
artifact's normalized tag list, in stored order (SPEC-0002 REQ "Artifact Tags"). The
url MUST be the public web URL of the artifact.
Payload changes MUST be additive. An existing field's name, meaning, and encoding MUST NOT change. A field that does not apply to an artifact MUST be omitted rather than emitted empty, so the event for an artifact without the newer fields is byte-identical to the event from before those fields were introduced.
any event is delivered the body parses as JSON and contains the fields above with an agent creates an artifact or a bundle over MCP with tags an artifact with no tags and no on-behalf-of is created the body MUST contain neither a a tagged event is delivered with tags are client-asserted. A consumer MUST NOT base a trust or authorization
decision on data.tags. data.actor_id (the authenticated principal) and
data.channel are the server-derived identity fields; data.on_behalf_of is
self-reported harness context.Scenario: Payload shape
source equal
to cairn and kind equal to artifact.createdScenario: Tags and on-behalf-of carried
data.tags MUST equal the stored tags and data.on_behalf_of MUST equal
the connected MCP client's initialize name/versionScenario: Untagged payload unchanged
tags nor an on_behalf_of key, and MUST
be byte-identical to the pre-tags payload for the same field valuesScenario: Signature covers tags
CAIRN_OUTBOUND_WEBHOOK_SECRET setX-Cairn-Signature MUST verify over the raw body, data.tags included
Requirement: Delivery Targets from Configuration
Targets SHALL be configured via the comma-separated environment variable
artifact creation behaves exactly as before and no outbound HTTP is made two URLs are configured each receives its own copy of the eventCAIRN_OUTBOUND_WEBHOOK_URLS. When the variable is unset or empty, the system MUST
NOT emit anything (feature inert by default). Target URLs MUST NOT be logged.Scenario: Inert by default
CAIRN_OUTBOUND_WEBHOOK_URLS is emptyScenario: Multiple targets
Requirement: Signed Delivery
Each delivery SHALL carry the secret is configured and an event is delivered the no secret is configured deliveries carry no X-Cairn-Event: artifact.created, X-Cairn-Event-Id
(matching event_id), and, when CAIRN_OUTBOUND_WEBHOOK_SECRET is set,
X-Cairn-Signature: sha256=<hex> — the HMAC-SHA256 of the raw request body. The
signature comparison at consumers MUST be constant-time. When the secret is unset,
the signature header MUST be omitted.Scenario: Signature present with secret
X-Cairn-Signature header verifies against the raw body with the
configured secretScenario: No secret, no header
X-Cairn-Signature header
Requirement: Bounded Async Delivery with Retry
Delivery SHALL be asynchronous to the creation request, through a bounded in-memory
queue. The system MUST attempt each delivery up to 3 times with short backoff before
dropping the event and logging the failure. When the queue is full, the event MUST
be dropped and logged, never block the caller. a target fails twice then succeeds the event is delivered exactly once to that target and not dropped a target fails 3 times the event is dropped for that target and a failure is loggedScenario: Retry then succeed
Scenario: Exhausted retries
Requirement: Graceful Lifecycle
The emitter worker SHALL start with the server and shut down gracefully on context
cancellation, draining or abandoning in-flight attempts within a bounded shutdown
window. Restart behavior follows ADR-0017: queued events are not persisted. the server receives SIGTERM mid-delivery the process exits within the shutdown window without deadlockingScenario: Shutdown
Security Requirements
- Authentication: Emission is server-initiated to operator-configured targets; no new unauthenticated inbound endpoint is introduced. Target URLs act as bearer capabilities and MUST NOT be logged or echoed in errors.
- Rate limiting: Outbound fan-out is bounded by the in-memory queue cap and one worker; no inbound rate surface changes. Consumers apply their own limits.
- Security headers: Deliveries are POSTs with
Content-Type: application/json; no HTML rendering is involved. TLS is required for non-localhost targets. - Request body size limits: Event bodies are server-generated and bounded by the artifact metadata they carry (never artifact content). Tags add at most 32 × 64 bytes plus JSON punctuation. No inbound body limit changes.
- Untrusted fields:
data.tagsis client-asserted and MUST NOT drive a consumer's trust or authorization decision (ADR-0018). A receiver of a handoff treats it as semi-trusted and guards against prompt injection in the artifact body. - CSRF protection: Not applicable — no browser-facing state change; targets are machine consumers with token/HMAC verification.
- Redirect validation: The delivery client MUST NOT follow HTTP redirects to avoid leaking signed bodies or capability URLs to unconfigured hosts.
Accessibility Requirements
Not applicable — no UI is introduced by this capability.