SPEC-0016: Annotation and Trace Lifecycle Events
Graph Edges
- Implements: ADR-0022: annotation and trace lifecycle events with a server-derived actor kind.
- Requires: SPEC-0012: the outbound webhook envelope, signing and delivery this spec extends.
- Requires: SPEC-0006: the reactions and comments whose lifecycle is announced.
- Related: SPEC-0004 (run lifecycle), SPEC-0009 (actor provenance), SPEC-0007 (MCP tool surface), SPEC-0017 (redaction runs before persist, so an event never carries an unredacted body), SPEC-0023 (owned subscriptions that deliver these kinds).
Overview
Cairn announces artifact.created today (SPEC-0012) and nothing else. This
capability adds comment.created, reaction.added, reaction.removed and
run.closed on the same envelope, signer and queue. Every event also gains a
server-derived actor_kind (human or agent) and auth method.
A server-computed approval bit makes a human 👍 a fact that the human's own
agents cannot forge. It is backed by per-kind reaction storage, so an agent can
neither occupy nor withdraw a human's reaction. See ADR-0022.
Requirement IDs (EV-n) are stable. Plans and code cite them as
SPEC-0016 EV-n.
Requirements
Requirement: EV-1 Event Kind Registry
The system SHALL emit exactly these kinds, each named <noun>.<past-tense verb>:
artifact.created(SPEC-0012, unchanged trigger);comment.created;reaction.added;reaction.removed;run.closed;artifact.retained,artifact.releasedandartifact.deleted, registered by SPEC-0020 REQ-14 (opt-in permanent retention) and emitted only once that capability ships.
a contributor adds an event for a comment edit it MUST use the kind the emitter is asked to encode a kind absent from the registry it MUST refuse to encode it, log an error, and deliver nothingcomment.edited and comment.deleted are RESERVED names. They MUST NOT be
emitted for any other meaning. The comment edit and delete routes (#158) SHALL
emit them when those routes exist. A new kind MUST be added to this registry
before it is emitted.Scenario: Reserved kinds are not reused
comment.edited and follow EV-3's payload rulesScenario: Unknown kind never emitted
Requirement: EV-2 Emission Points
Each event MUST be emitted only after its transaction commits, from the service layer that owns the change, so every surface (REST, web, CLI, MCP) triggers it through one choke point:
comment.createdwhenAddCommentcommits;reaction.addedwhenReactinserts a row (an idempotent duplicate MUST NOT emit);reaction.removedwhenUnreactorUnreactByIDdeletes a row (removing an absent reaction MUST NOT emit);run.closedwhenCloseRuncommits, and when a batch run is created already closed. The batch case is emitted after that run'sartifact.created.
Emission MUST NOT block, fail or alter the response of the request that caused
it (SPEC-0012 REQ "Event Emission on Artifact Creation"). an agent calls a a caller reacts with an emoji it already reacted with, under the same
actor kind the response is 200 with the existing row and no event is emitted a client POSTs a complete batch run the event queue receives a comment insert fails after validation and its transaction rolls back no Scenario: MCP reaction emits
artifact_react and a new row is insertedreaction.added event is emitted for the subject artifactScenario: Duplicate reaction is silent
Scenario: Batch run emits closed after created
artifact.created and then run.closed for
that runScenario: Rolled-back write emits nothing
comment.created event is emitted
Requirement: EV-3 Payload Shape
Every event SHALL use the SPEC-0012 envelope {source, kind, event_id, created_at, data} unchanged. Within data:
id,url,share_type,title,tags,expires_atMUST describe the subject artifact, with the meanings SPEC-0012 REQ "Event Payload" defines.actor_id,channel,on_behalf_ofMUST describe the principal that caused the event.actor_kindandauthMUST be present on every kind (EV-4).- Kind-specific fields MUST nest under exactly one object:
commentcarriesid,parent_id,anchor_type,anchor_key,bodyandbody_truncated;reactioncarriesid,anchor_type,anchor_key,emoji,approval_classandapproval;runcarriesstatus,span_count,started_at,ended_atandduration_ms.
comment.bodyMUST be at most 4096 bytes, cut on a UTF-8 boundary, withbody_truncated: truewhen it was cut.
New fields MUST be appended after existing ones, and a field that does not apply
MUST be omitted rather than emitted empty. The exception is a consumer reads they identify the artifact reacted to, exactly as they would on its
a 10 KiB comment is created the event carries the first 4096 bytes (at most) with
an untagged REST artifact is created after this change its body equals the SPEC-0012 golden body with approval_class and
approval, which are always present on reaction events, because false is
meaningful there.Scenario: Subject fields resolve the artifact for every kind
data.id and data.url from a reaction.added eventartifact.createdScenario: Long comment is truncated in the event only
body_truncated: true, and the stored comment is unchangedScenario: artifact.created gains only appended keys
actor_kind and auth
appended to data, and no other byte differs
Requirement: EV-4 Server-Derived Actor Kind
actor_kind MUST be derived from how the principal authenticated, never from
request content:
humanif and only if the principal authenticated with an ambient browser session (Principal.Ambient: Pocket ID, GitHub, or the development login);agentfor every bearer credential: MCP OAuth access tokens, personal access tokens regardless of theiris_agentflag, andCAIRN_API_TOKENSentries.
a human reacts from the CLI with a personal access token minted with
the event carries a signed-in human reacts from the web viewer the event carries a REST comment body includes the stored comment and its event carry auth MUST be one of session, oauth, pat, api_token. The development
bearer shortcut (CAIRN_DEV_INSECURE_BEARER_AUTH) MUST report api_token. No
request header, body field, tool argument or on_behalf_of value may change
either field.Scenario: Human PAT is an agent
is_agent=falseactor_kind: "agent" and auth: "pat"Scenario: Browser session is human
actor_kind: "human" and auth: "session"Scenario: Client cannot assert the kind
"actor_kind": "human" or an
on_behalf_of value, sent with a bearer tokenactor_kind: "agent", and the
request field is ignored
Requirement: EV-5 Approval Class and the Approval Bit
The system SHALL define an approval class of emoji, configured by
CAIRN_APPROVAL_REACTIONS (comma-separated). The default is 👍 (U+1F44D),
✅ (U+2705) and ✔️ (U+2714). Membership MUST be tested after stripping skin-tone
modifiers (U+1F3FB..U+1F3FF) and variation selector-16 (U+FE0F).
On reaction.added and reaction.removed:
reaction.approval_classMUST be true exactly when the emoji is in the class;reaction.approvalMUST be true exactly whenapproval_classis true andactor_kindishuman.
Approval-class reactions from agent credentials MUST be accepted and stored. They
MUST NOT be refused. an agent reacts 👍 over MCP the reaction is stored, and its event carries a human in a browser session reacts 👍🏽 the event carries that human removes the 👍🏽 a Scenario: Agent thumbs-up is not an approval
approval_class: true
and approval: falseScenario: Human thumbs-up with skin tone is an approval
approval_class: true and approval: trueScenario: Withdrawn approval is announced
reaction.removed event carries approval: true, so a consumer can
revoke the approval it acted on
Requirement: EV-6 Per-Kind Reaction and Comment Ownership
reactions and comments MUST store actor_kind. Reaction idempotency MUST be
keyed on (artifact_id, anchor_type, anchor_key, emoji, actor_id, actor_kind).
Every path that removes a reaction MUST match the caller's actor_kind as well as
its actor_id. Every path that edits or deletes a comment (#158) MUST do the same.
reactions MUST also store on_behalf_of, populated exactly as for comments
(#159).
Rows written before this change carry an empty an agent for alice reacts 👍, and then alice reacts 👍 in her browser two reaction rows exist, and the second insert emits alice's agent calls un-react for 👍 after alice reacted 👍 in her
browser only the agent's row (if any) is removed, and alice's row and
approval stand an agent credential calls the response is 403 and nothing is deleted a reaction row created before this change is removed by its actor its actor_kind. They MUST be treated
as not human: they never produce approval: true, and only a caller whose
actor_id matches may remove them.Scenario: Agent cannot occupy the human's row
reaction.added
with approval: trueScenario: Agent cannot withdraw the human's approval
Scenario: Agent cannot remove by id
DELETE /v1/artifacts/{id}/reactions/{rid}
for a row with actor_kind = humanScenario: Legacy row is never an approval
reaction.removed event carries approval: false
Requirement: EV-7 Recipient Selection and Tenancy
Every internal event MUST carry the subject artifact's owner, so that owner and team subscriptions (Cairn ADR-0029) receive only events about artifacts their owner or team owns. The owner MUST NOT appear on the wire.
The kinds this capability adds MUST be delivered only to owned subscriptions
(Cairn ADR-0029) of the workspace that owns the subject artifact, and only to
those whose event-type filter admits the kind. They MUST NOT be sent to the
instance env targets ( a build where a user reacts 👍 to an artifact no request carrying alice's subscription filters to it receives creations and reactions on her artifacts, and no comments
or run closures a subscription is created with the event type the request is refused with a validation error naming bob comments on alice's artifact, and bob has his own subscription bob's subscription receives nothing for that comment. Alice's
subscriptions (once ADR-0029 lands) receive CAIRN_OUTBOUND_WEBHOOK_URLS), which ADR-0029 removes in
the change that ships subscriptions; there is no kind allowlist for env targets
and no interim path to them. Until subscriptions exist, the new kinds MUST be
emitted and counted, and MUST NOT be delivered. A subscription filter naming a
kind that is not in the EV-1 registry MUST be refused with a validation error
naming it. Events MUST NOT be routed to subscriptions of the event's actor
unless that actor's workspace owns the subject artifact.Scenario: No env target receives the new kinds
CAIRN_OUTBOUND_WEBHOOK_URLS is still readreaction.added is made to any env targetScenario: Subscription filter selects kinds
artifact.created,reaction.addedScenario: Misspelt kind in a filter is refused
reaction.addreaction.add
as an unknown kindScenario: Another owner's subscription is not notified
comment.created.
Requirement: EV-8 Signed, Bounded Delivery with Creation Priority
Delivery MUST reuse SPEC-0012's queue, three-attempt retry, redirect refusal and
a the queue is more than half full of pending reaction events and an
artifact is created new reaction events are dropped and counted, and the
X-Cairn-Signature. X-Cairn-Event MUST equal the body's kind. When the
bounded queue is more than half full, the emitter MUST drop new non-creation
events before enqueueing, logging and counting each drop. artifact.created
MUST still be enqueued while capacity remains.Scenario: Header matches kind
run.closed event is deliveredX-Cairn-Event is run.closed and the signature verifies over the
raw bodyScenario: Reaction burst does not starve creation
artifact.created event is enqueued
Requirement: Error Handling Standards
All error-producing operations MUST follow structured error handling:
- Errors MUST be wrapped with contextual information at each layer boundary.
- Sentinel errors MUST be defined for failure modes callers distinguish (an unknown event kind, and an un-react forbidden by actor kind).
- Silent error swallowing MUST NOT occur. An emit failure MUST be logged with the event id and kind, never the target URL.
- Structured logging MUST be used for error reporting.
Scenario: Emit failure is logged, not surfaced
encoding an event fails
the originating request succeeds and a structured log line names the kind and event id
Requirement: Concurrency Safety
- Context propagation MUST be used for cancellation across the emitter worker.
- The worker MUST keep SPEC-0012's clean startup and graceful shutdown.
- Queue-priority accounting MUST be race-free.
- Emitter tests MUST run under the race detector in CI (
make ci).
Scenario: Concurrent reactions
50 concurrent reactions insert rows while an artifact is created
every insert emits exactly one event, and the race detector reports nothing
Requirement: Database Operation Standards
- The
actor_kindcolumns and the new uniqueness index MUST ship in one migration. The index MUST be built without blocking writes. - Reaction and comment writes and their counters MUST stay in one transaction (SPEC-0006 REQ "Count Aggregation").
- Queries MUST be parameterized.
Scenario: Migration over existing rows
the migration runs over a database that already holds reactions
existing rows gain actor_kind = '' and remain unique under the new
key
Security Requirements
- Authentication: No new inbound endpoint is added. Every annotation write
keeps its existing
requireAuth+annotations:write+ CSRF chain.actor_kindandauthcome from the authenticated principal only. - Authorization: An agent credential MUST NOT remove, edit or delete an
annotation whose
actor_kindishuman(EV-6).approvalis computed server-side (EV-5). - Rate limiting: Unchanged inbound. Outbound fan-out is bounded by the queue, with creation priority (EV-8).
- Security headers: Deliveries are JSON POSTs, and no HTML is rendered.
- Request body size limits: Event bodies are server-generated. Comment bodies in events are capped at 4096 bytes (EV-3). Inbound annotation limits are unchanged (SPEC-0006).
- CSRF protection: Unchanged. Cookie-session annotation writes stay
CSRF-guarded, which is what makes a session-derived
humanunforgeable cross-site. - Redirect validation: The delivery client MUST NOT follow redirects (SPEC-0012).
- Untrusted fields:
tagsandon_behalf_ofremain asserted (ADR-0018, SPEC-0012). Consumers MUST gate trust onactor_kind,authandapproval, never onon_behalf_ofor tags. - Privacy: No event kind reaches a target the operator chose. The only recipients are subscriptions owned by the subject artifact's user or team (EV-7, Cairn ADR-0029).
Accessibility Requirements
The viewer SHOULD visually distinguish agent reactions from human reactions in approval-class tallies. When it does:
- WCAG 2.1 AA is the target.
- ARIA landmarks: the viewer's existing landmarks are unchanged.
- Icon-only controls: an agent marker MUST carry an accessible name (for example "reacted by an agent").
- Dynamic content: tally updates keep the viewer's existing
aria-live="polite"behaviour. - Keyboard navigation: reaction controls stay keyboard-operable (SPEC-0006).
- Focus management: not applicable; no modal is introduced.