From 8731299dea60455545d8d1f11e69308cda179844 Mon Sep 17 00:00:00 2001 From: NarrativAI Agent Date: Wed, 12 Aug 2026 10:48:53 +0100 Subject: [PATCH 1/2] Define the click context: ctx_token issuance, carriage, discovery and resolution (#23, #28, #30) New section 7.4 replaces the v0.1 click manifest with the click context: - Token pattern ct_[A-Za-z0-9_-]{8,240}, opaque, bound to exactly one presentation at mint time; per-click minting for routed surfaces. - Reserved query parameters ctx_token and ctx_iss with SHOULD-level redirect propagation mirroring section 7.2. - Resolver discovery through the issuer manifest: telemetry.ctx_resolution. - Resolution returns the engagement, the clicked content's lineage cut by content identity across turns, and an optional count-based session summary; never the raw session_id, never other owners' events. - presentation_id becomes conditional: required for agent-reported engagements, restored from issuer state for destination reports that carry ctx_token; the URL-carried presentation UUID is withdrawn. - Consumer-custody trade-off recorded (#29). Schemas, six new/updated fixtures, migration note. 74/74 conformance checks, 9/9 worked examples. Co-Authored-By: Claude Fable 5 --- SPECIFICATION.md | 87 +++++++++++++++++-- manifest.json | 6 ++ telemetry-event-batch.json | 25 +++++- telemetry-event.json | 24 ++++- telemetry-session.json | 21 ++++- tests/invalid/ctx-token-bad-pattern.json | 12 +++ ...dalone-missing-presentation-and-token.json | 14 +++ .../event-standalone-engaged-ctx-token.json | 3 +- .../event-standalone-engaged-redirect.json | 14 +++ .../valid/manifest-agent-ctx-resolution.json | 12 +++ tests/valid/session-earlier-turn-click.json | 53 +++++++++++ .../session-repeated-link-engagement.json | 54 ++++++++++++ 12 files changed, 307 insertions(+), 18 deletions(-) create mode 100644 tests/invalid/ctx-token-bad-pattern.json create mode 100644 tests/invalid/engaged-standalone-missing-presentation-and-token.json create mode 100644 tests/valid/event-standalone-engaged-redirect.json create mode 100644 tests/valid/manifest-agent-ctx-resolution.json create mode 100644 tests/valid/session-earlier-turn-click.json create mode 100644 tests/valid/session-repeated-link-engagement.json diff --git a/SPECIFICATION.md b/SPECIFICATION.md index b3e960e..4121219 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -12,7 +12,7 @@ 4. [Concepts](#4-concepts) - roles, sessions, event lifecycle, source roles, content identification 5. [Schema](#5-schema) - session, event, event types, conversation turn, privacy, intent, conformance levels 6. [Data profiles](#6-data-profiles) - retrieval, edge enrichment, origin enrichment, grounding, citation, reproduction, presentation, engagement -7. [Transport](#7-transport) - delivery formats, Content-Telemetry-ID header +7. [Transport](#7-transport) - delivery formats, Content-Telemetry-ID header, routing, click context 8. [Manifest](#8-manifest) - discovery, schema, operator, keys, telemetry, domains 9. [Privacy](#9-privacy) - data minimisation, recommended levels, retention 10. [Attribution](#10-attribution) - counting semantics, grounding without citation @@ -213,7 +213,7 @@ Content moves through six stages during an agent interaction: Grounding and presentation record different boundary crossings: grounding records entry into a generation context, while presentation records a recipient-facing delivery occurrence. As agent experiences evolve beyond the chat window the two diverge - content can shape an answer whose source is never presented, and an agent can present content that never entered a generation context (see *Departures from the funnel model* below). -6. **Engaged** - The recipient or agent performed an observable action on a presentation: clicked a link, expanded a preview, copied text, shared the response, or directed the agent to act on the content. It does not imply attention beyond the reported action. `presentation_id` links the action to the exact presentation occurrence; a click-out can also carry a `ctx_token` that a destination resolves to the session's click manifest (section 7.1). +6. **Engaged** - The recipient or agent performed an observable action on a presentation: clicked a link, expanded a preview, copied text, shared the response, or directed the agent to act on the content. It does not imply attention beyond the reported action. `presentation_id` links the action to the exact presentation occurrence; a click-out can also carry a `ctx_token` that a destination resolves to the click context (section 7.4). ``` Retrieved (HTTP layer, cacheable) @@ -368,7 +368,8 @@ Format: the URL of a manifest served at `/.well-known/content-telemetry.json` un | `output_id` | string | For reproduced/cited/presented | Opaque output-artifact identifier joining construction to later delivery | | `output_element_id` | string | No | Opaque element within `output_id`, such as a passage, media track, caption, link, or card | | `citation_id` | UUID | No | On `content_presented` or `content_reproduced`, the `id` of the associated citation event; absent for uncited presentations and uncredited reproductions | -| `presentation_id` | UUID | For engaged | On `content_engaged`, the `id` of the exact presentation occurrence acted upon | +| `presentation_id` | UUID | For engaged | On agent-reported `content_engaged`, the `id` of the exact presentation occurrence acted upon. Destination-reported events carrying an envelope `ctx_token` omit it (section 7.4) | +| `ctx_token` | string | No | On agent-reported `content_engaged`, the click token minted for this engagement's presentation, recorded so destination reports join to it (section 7.4) | | `source_role` | SourceRole | No | Who is reporting: `origin`, `edge`, `index`, `agent` (see 4.4) | | `content_telemetry_id` | UUID | No | Correlation ID for cross-observer deduplication (see 7.2) | | `content_url` | string | No | Content URL as fetched or canonical URL | @@ -805,7 +806,7 @@ When a session includes `content_presented` events but no subsequent `content_en |-------|------|-------------| | `engagement_type` | string | Type of interaction (see below) | -The content URL is identified by the event-level `content_url` field (section 5.2), not duplicated in `data`. Every engagement MUST carry `presentation_id`, referencing the exact `content_presented.id` on which the action occurred. Matching on URL alone is insufficient because the same source reference can be presented more than once. +The content URL is identified by the event-level `content_url` field (section 5.2), not duplicated in `data`. Every agent-reported engagement MUST carry `presentation_id`, referencing the exact `content_presented.id` on which the action occurred. Matching on URL alone is insufficient because the same source reference can be presented more than once. A destination-reported engagement carries `ctx_token` on its envelope instead: the destination cannot know the presentation UUID, and the telemetry consumer restores the binding from the token at resolution (section 7.4). #### Engagement types @@ -823,7 +824,7 @@ These are the core values. Extensions MAY define additional engagement actions - `link_click` is the primary signal for clickthrough rate calculation. Telemetry consumers can derive per-content-owner and aggregate clickthrough rates from `link_click` engagements and link presentations, joining each engagement through `presentation_id` rather than URL alone. -A `link_click` or `agent_navigate` engagement reported from the landing page after a click-out crosses a trust boundary. Such events carry a `ctx_token` in place of `session_id`, which the telemetry consumer resolves to the originating session's click manifest (see section 7.1). +A `link_click` or `agent_navigate` engagement reported from the landing page after a click-out crosses a trust boundary. Such events carry a `ctx_token` in place of `session_id`, which the telemetry consumer resolves to the click context (see section 7.4). ## 7. Transport @@ -891,9 +892,7 @@ Session documents use `"document_type": "session"`. When `document_type` is abse For origin-side emitters at Retrieval conformance level, `session_id` MAY be omitted when the content owner has no session context. Telemetry consumers correlate these events with agent-reported sessions using the `content_telemetry_id` field. -For `content_engaged` events emitted from a landing page after a click-out (typically by a content marketplace, affiliate network, or destination site), `session_id` MAY be replaced by a `ctx_token` field that carries an opaque click-token issued by the originating agent. Telemetry consumers resolve the token to the owning session. This lets a downstream observer report a corroborating engagement event without sharing the session UUID across trust boundaries. An event MUST carry either `session_id` or `ctx_token` at Grounding conformance and above. - -**ctx_token resolution.** A telemetry consumer that supports `ctx_token` resolution exposes, for a resolved token, the **click manifest**: the set of `content_grounded`, `content_cited`, and `content_presented` events belonging to the resolved session, identifying every source that informed the response that produced the click. The manifest is gated by the resolved session's `privacy_level` and by consent. A consumer MUST return the manifest only when the issuing agent has opted in to sharing sessions via click tokens; when the agent opt-in is absent, the consumer MUST NOT disclose the manifest. Within a returned manifest, a source MUST appear only when its content owner has opted in to being visible in click-token lookups; the consumer MUST withhold the events of any content owner whose opt-in is absent while returning the remainder of the manifest. A resolution response MUST NOT include the resolved session's raw `session_id`: the token exists so that the session UUID never crosses the trust boundary, and a resolution response that returned it would undo that. The mechanism by which an agent and a content owner record these opt-ins is operator-defined; the consent gate is normative. +For `content_engaged` events emitted from a landing page after a click-out (typically by a content marketplace, affiliate network, or destination site), `session_id` MAY be replaced by a `ctx_token` field that carries an opaque click token issued by the originating agent. This lets a downstream observer report a corroborating engagement event without sharing the session UUID across trust boundaries. An event MUST carry either `session_id` or `ctx_token` at Grounding conformance and above. Token issuance, carriage, binding, resolver discovery, and the resolution response are defined in section 7.4. The primary schema (`telemetry-session.json`) validates session documents. A standalone event envelope schema (`telemetry-event.json`) validates the event delivery format, and a batch envelope schema (`telemetry-event-batch.json`) validates the event batch format. All three schemas share the `TelemetryEvent` definition. @@ -952,6 +951,66 @@ Any party may operate a consumer: an agent operator, a licensing intermediary, o **Cross-consumer correlation.** Origin-side emitters and agent-side emitters MAY use different telemetry consumers. A content owner's CDN sends retrieval events to one telemetry consumer; an agent sends sessions to another. The `content_telemetry_id` field (section 7.2) correlates the same retrieval across consumers - both sides share the same UUID from the HTTP request. This correlation operates at the retrieval level only. Grounding, reproduction, citation, presentation, and engagement events have no independent origin-side counterpart to correlate against. +### 7.4 Click context (`ctx_token`) + +A click-out is the moment content usage becomes traffic the destination can observe. The click token lets the destination corroborate that moment and learn what produced it, without receiving the session UUID or any other publisher's activity. + +#### 7.4.1 Token issuance + +A `ctx_token` is an opaque token minted by the originating agent. Its value MUST match `^ct_[A-Za-z0-9_-]{8,240}$` and MUST NOT encode content, session, or user identifiers recoverable without the issuer's state. + +A token MUST be bound to exactly one `content_presented` occurrence at mint time. The same URL presented twice receives two tokens; a token observed on two presentations is malformed issuance and consumers MUST NOT resolve it. Surfaces that route outbound navigation through the agent SHOULD mint per click, additionally binding the token to the resulting `content_engaged` event. Direct-link surfaces mint per presentation; repeated clicks on one presentation then share a token, and are distinguished at resolution by the destination's event timestamps. + +The token-to-presentation binding is issuer state. It never travels in the URL: destinations do not receive `presentation_id`, and the consumer restores the binding at resolution. The agent SHOULD record the minted token on its own `content_engaged` event (the event-level `ctx_token` field) so the consumer can join destination reports to it. + +#### 7.4.2 Carriage and redirects + +Agents that decorate outbound link URLs MUST use the reserved query parameters `ctx_token` (the token) and `ctx_iss` (the issuer locator, section 7.4.3), and MUST NOT place other telemetry data in the URL. + +Parties operating redirects SHOULD propagate both parameters through same-domain redirect hops, mirroring the `Content-Telemetry-ID` redirect guidance in section 7.2. Destinations relying on redirect-based routing SHOULD capture the parameters at the earliest point in the chain. This is a transport and correlation convention: it does not claim that every intermediary preserved the value, and it does not enforce downstream behaviour. + +#### 7.4.3 Resolver discovery + +`ctx_iss` carries the issuer manifest locator: a host, optionally with a path prefix, identifying a well-known manifest location (section 8.1). `ctx_iss=example.com/agents/search` resolves to `https://example.com/agents/search/.well-known/content-telemetry.json`. That manifest declares the resolution endpoint in `telemetry.ctx_resolution` (section 8.5). + +The token stays opaque and carries no routing; the locator travels alongside it. No central registry is required or defined. + +#### 7.4.4 Resolution response - the click context + +A telemetry consumer that supports resolution exposes, for a presented token, the **click context**: + +1. **The engagement.** The `content_engaged` occurrence(s) bound to the token, including the presentation record the token restores: `presentation_id`, `output_id`, `output_element_id` where present, and timestamps. +2. **The lineage of the clicked content, selected by content identity.** The resolved session's `content_retrieved`, `content_grounded`, `content_reproduced`, `content_cited`, and `content_presented` events whose `content_url` or `content_id` identify the same content as the clicked reference - across all turns. The cut is by content identity, not by turn or click timestamp: a click in turn 5 on content grounded in turn 2 resolves that content's full lineage. +3. **An optional privacy-bounded session summary.** Event counts by type and a distinct-source count. Counts, not events, and no content identifiers of other owners. + +A resolution response MUST NOT include the resolved session's raw `session_id`: the token exists so the session UUID never crosses the trust boundary. A resolution response MUST NOT include events for content other than the clicked content; cross-content session detail is a reporting concern, delivered through publisher-filtered views (section 7.3), not through per-click resolution. A consumer MUST resolve a token only when the issuing agent has opted in to click-token resolution, and the response is gated by the resolved session's `privacy_level`. The mechanism by which the opt-in is recorded is operator-defined; the gate is normative. + +A worked resolution response (informative): + +``` +{ + "engagement": { + "engagement_type": "link_click", + "timestamp": "2026-08-10T09:02:31Z", + "presentation_id": "880e8400-e29b-41d4-a716-446655440213", + "output_id": "response:2" + }, + "lineage": [ + { "type": "content_grounded", "timestamp": "2026-08-10T09:00:01Z", "turn_id": "1", "data": { "chars_ingested": 9400 } }, + { "type": "content_cited", "timestamp": "2026-08-10T09:00:04Z", "turn_id": "1", "data": { "citation_type": "paraphrase", "position": "primary" } }, + { "type": "content_presented", "timestamp": "2026-08-10T09:00:04Z", "turn_id": "1", "data": { "presentation_kind": "source_reference", "presentation_type": "link" } }, + { "type": "content_presented", "timestamp": "2026-08-10T09:02:10Z", "turn_id": "2", "data": { "presentation_kind": "source_reference", "presentation_type": "link" } } + ], + "session_summary": { "turns": 2, "distinct_sources": 3, "events_by_type": { "content_grounded": 4, "content_cited": 3, "content_presented": 5 } } +} +``` + +The response shape above is informative in v1; the constraints in this section are normative. A response schema can follow implementation evidence during the release-candidate window. + +#### 7.4.5 Recorded limit: consumer custody + +Resolution depends on the telemetry consumer the agent chose, because that consumer holds the session. Grounding and citation events precede the click and cannot carry its later token, and distributing tokens to every contributing content owner after the fact would weaken the privacy boundary this section maintains. Publisher-derived tokens would require a federation and key-management design; that belongs in a later attribution or evidence profile. Core v1 mitigates the dependency with resolver discoverability (7.4.3) and exact click binding (7.4.1). + ## 8. Manifest Content owners, agents, and platforms publish a manifest declaring their identity and telemetry endpoints. The `manifest_ref` field on session documents (5.1.2) and the routing logic for origin-side emitters (7.3) resolve to manifests defined in this section. @@ -1017,6 +1076,7 @@ Public keys used to sign telemetry events emitted by this participant. Per-event |-------|------|----------|-------------| | `endpoint` | string | Yes | HTTPS URL. For agents and platforms, the outbound submission endpoint. For content owners, the inbound destination for events about the content owner's content. | | `conformance_level` | string | No | Conformance level advertised by this participant's own emitter(s). One of `retrieval`, `grounding`, `citation` (see 5.7). | +| `ctx_resolution` | string | No | HTTPS URL of the click-token resolution endpoint operated by or for this participant (see 7.4). Valid on `agent` and `platform` manifests. | `conformance_level` is informational. It advertises the level of telemetry the manifest's participant emits. It does **not** constrain what an inbound `endpoint` accepts - an endpoint accepts whatever events it is configured to accept, regardless of any level declared here - and it places **no requirement** on other emitters. On a `content_owner` manifest it describes only the events the owner's own infrastructure emits (typically a CDN edge worker at `retrieval`); it says nothing about what agents or platforms report about the owner's content, which those parties advertise in their own manifests. A `content_owner` manifest SHOULD omit `conformance_level` unless the owner operates its own emitter. There is no field for a content owner to *request* a minimum level from agents; consumers tolerate events from any level (see 5.7), and the protocol does not give a manifest a way to demand more. @@ -1289,6 +1349,17 @@ NOT synthesize a reproduction event from `false` or incomplete historical data. A grounding event MAY retain `content_fingerprint.scheme`, `detected`, and a scheme-defined `value`. +V1 narrows `ctx_token` resolution. The v0.1 click manifest returned every +source that informed the resolved session, gated by per-owner opt-in; the v1 +click context (section 7.4) returns the engagement, the clicked content's +lineage by content identity, and at most a count-based session summary. +Consumers implementing v0.1 resolution narrow their response shape and MUST +NOT return other owners' events through per-click resolution. Token values +gain the `ct_` pattern; presentation binding moves from the URL-carried +`presentation_id` a destination could never legitimately know to issuer state +restored at resolution. + + Preview versions (0.x) use two-component version numbers. From 1.0.0 onward, versions follow [semantic versioning](https://semver.org/): - **Major** (1.0.0 → 2.0.0) - breaking changes to required fields diff --git a/manifest.json b/manifest.json index 435a5e2..ddaa8d2 100644 --- a/manifest.json +++ b/manifest.json @@ -82,6 +82,12 @@ "type": "string", "enum": ["retrieval", "grounding", "citation"], "description": "Conformance level advertised by this participant's own emitter(s). Informational. (Sections 8.5, 5.7)" + }, + "ctx_resolution": { + "type": "string", + "format": "uri", + "pattern": "^https://", + "description": "HTTPS URL of the click-token resolution endpoint operated by or for this participant. Valid on agent and platform manifests. Destinations resolve ctx_iss to this manifest and present the token here. (Section 7.4)" } }, "description": "Telemetry endpoint declaration. (Section 8.5)" diff --git a/telemetry-event-batch.json b/telemetry-event-batch.json index 588c95f..2c2d987 100644 --- a/telemetry-event-batch.json +++ b/telemetry-event-batch.json @@ -28,7 +28,8 @@ }, "ctx_token": { "type": "string", - "description": "Opaque click-token issued by the originating agent, carried in place of session_id on batches of content_engaged events emitted from a landing page after a click-out. Applies to every event in the batch and is resolved by the telemetry consumer to the owning session. See section 7.1. An event MUST carry either session_id or ctx_token at Grounding conformance and above." + "pattern": "^ct_[A-Za-z0-9_-]{8,240}$", + "description": "Opaque click token issued by the originating agent, carried in place of session_id on batches of content_engaged events emitted from a landing page after a click-out. Applies to every event in the batch and is resolved by the telemetry consumer to the click context (section 7.4). An event MUST carry either session_id or ctx_token at Grounding conformance and above." }, "agent_id": { "type": "string", @@ -47,5 +48,25 @@ }, "description": "The telemetry events in the batch" } - } + }, + "allOf": [ + { + "if": { + "not": { "required": ["ctx_token"] } + }, + "then": { + "properties": { + "events": { + "items": { + "if": { + "properties": { "type": { "const": "content_engaged" } }, + "required": ["type"] + }, + "then": { "required": ["presentation_id"] } + } + } + } + } + } + ] } diff --git a/telemetry-event.json b/telemetry-event.json index fb21ff7..6ec0b53 100644 --- a/telemetry-event.json +++ b/telemetry-event.json @@ -28,7 +28,8 @@ }, "ctx_token": { "type": "string", - "description": "Opaque click-token issued by the originating agent, carried on content_engaged events emitted from a landing page after a click-out in place of session_id. Resolved by the telemetry consumer to the owning session (the click manifest). See section 7.1. An event MUST carry either session_id or ctx_token at Grounding conformance and above." + "pattern": "^ct_[A-Za-z0-9_-]{8,240}$", + "description": "Opaque click token issued by the originating agent, carried on content_engaged events emitted from a landing page after a click-out in place of session_id. Resolved by the telemetry consumer to the click context (section 7.4). An event MUST carry either session_id or ctx_token at Grounding conformance and above." }, "agent_id": { "type": "string", @@ -43,5 +44,24 @@ "$ref": "telemetry-session.json#/$defs/TelemetryEvent", "description": "The telemetry event" } - } + }, + "allOf": [ + { + "if": { + "required": ["event"], + "properties": { + "event": { + "properties": { "type": { "const": "content_engaged" } }, + "required": ["type"] + } + }, + "not": { "required": ["ctx_token"] } + }, + "then": { + "properties": { + "event": { "required": ["presentation_id"] } + } + } + } + ] } diff --git a/telemetry-session.json b/telemetry-session.json index 41c29e3..2f833a6 100644 --- a/telemetry-session.json +++ b/telemetry-session.json @@ -57,9 +57,18 @@ "events": { "type": "array", "items": { - "$ref": "#/$defs/TelemetryEvent" + "allOf": [ + { "$ref": "#/$defs/TelemetryEvent" }, + { + "if": { + "properties": { "type": { "const": "content_engaged" } }, + "required": ["type"] + }, + "then": { "required": ["presentation_id"] } + } + ] }, - "description": "Ordered list of events in the session (chronological by timestamp)" + "description": "Ordered list of events in the session (chronological by timestamp). Session documents are agent-reported, so content_engaged events here always carry presentation_id; the ctx_token relaxation applies only to standalone/batch envelopes (section 7.4)." } }, "$defs": { @@ -103,7 +112,12 @@ "presentation_id": { "type": "string", "format": "uuid", - "description": "The id of the exact content_presented event on which the engagement occurred. REQUIRED on content_engaged events." + "description": "The id of the exact content_presented event on which the engagement occurred. REQUIRED on agent-reported content_engaged events. Destination-reported events carrying ctx_token omit it; the consumer restores the binding at resolution (section 7.4)." + }, + "ctx_token": { + "type": "string", + "pattern": "^ct_[A-Za-z0-9_-]{8,240}$", + "description": "The click token the agent minted for this engagement's presentation, recorded so the consumer can join destination-reported events to it. Valid only on content_engaged events. See section 7.4." }, "source_role": { "$ref": "#/$defs/SourceRole", @@ -240,7 +254,6 @@ "required": ["type"] }, "then": { - "required": ["presentation_id"], "properties": { "data": { "properties": { diff --git a/tests/invalid/ctx-token-bad-pattern.json b/tests/invalid/ctx-token-bad-pattern.json new file mode 100644 index 0000000..b286ce5 --- /dev/null +++ b/tests/invalid/ctx-token-bad-pattern.json @@ -0,0 +1,12 @@ +{ + "_test_description": "ctx_token failing the value pattern: tokens are opaque but MUST match ^ct_[A-Za-z0-9_-]{8,240}$ so they survive URL carriage and are recognisable in logs (section 7.4). This value has no ct_ prefix and contains reserved characters.", + "document_type": "event", + "schema_version": "0.1", + "ctx_token": "session=660e8400!", + "event": { + "type": "content_engaged", + "timestamp": "2026-08-10T11:20:00Z", + "content_url": "https://news.example.com/markets/rate-decision", + "data": { "engagement_type": "link_click" } + } +} diff --git a/tests/invalid/engaged-standalone-missing-presentation-and-token.json b/tests/invalid/engaged-standalone-missing-presentation-and-token.json new file mode 100644 index 0000000..fb7e85e --- /dev/null +++ b/tests/invalid/engaged-standalone-missing-presentation-and-token.json @@ -0,0 +1,14 @@ +{ + "_test_description": "Agent-reported standalone content_engaged with session_id but no presentation_id. The presentation_id relaxation applies only to envelopes carrying ctx_token; an emitter that knows the session knows the presentation and MUST bind to it (sections 6.8, 7.4).", + "document_type": "event", + "schema_version": "0.1", + "session_id": "660e8400-e29b-41d4-a716-446655440230", + "agent_id": "assistant-v2", + "started_at": "2026-08-10T12:00:00Z", + "event": { + "type": "content_engaged", + "timestamp": "2026-08-10T12:00:30Z", + "content_url": "https://news.example.com/markets/rate-decision", + "data": { "engagement_type": "link_click" } + } +} diff --git a/tests/valid/event-standalone-engaged-ctx-token.json b/tests/valid/event-standalone-engaged-ctx-token.json index f73d6af..ce90f25 100644 --- a/tests/valid/event-standalone-engaged-ctx-token.json +++ b/tests/valid/event-standalone-engaged-ctx-token.json @@ -1,12 +1,11 @@ { - "_test_description": "Standalone content_engaged (link_click) reported from a landing page after a click-out. Carries ctx_token in place of session_id (section 7.1): the destination corroborates the click without receiving the session UUID. The telemetry consumer resolves ctx_token to the originating session's click manifest.", + "_test_description": "Standalone content_engaged (link_click) reported from a landing page after a click-out. Carries ctx_token in place of session_id and no presentation_id: the destination cannot know the presentation UUID, and the consumer restores the token's presentation binding at resolution (section 7.4).", "document_type": "event", "schema_version": "0.1", "ctx_token": "ct_9f3a1c7e2b8d4a06", "event": { "type": "content_engaged", "timestamp": "2026-03-28T14:06:00Z", - "presentation_id": "990e8400-e29b-41d4-a716-446655440064", "content_url": "https://www.example-review.com/headphones/best-noise-cancelling", "data": { "engagement_type": "link_click" diff --git a/tests/valid/event-standalone-engaged-redirect.json b/tests/valid/event-standalone-engaged-redirect.json new file mode 100644 index 0000000..a7ba533 --- /dev/null +++ b/tests/valid/event-standalone-engaged-redirect.json @@ -0,0 +1,14 @@ +{ + "_test_description": "Destination-reported click after a redirect chain: the presented short link redirected to the canonical URL, and the ctx_token and ctx_iss query parameters were propagated through the same-domain hops (section 7.4). The destination reports the canonical URL it serves; correlation runs through the token, not the URL.", + "document_type": "event", + "schema_version": "0.1", + "ctx_token": "ct_77b41f0ac93e5d28", + "event": { + "type": "content_engaged", + "timestamp": "2026-08-10T11:15:42Z", + "content_url": "https://shop.example.com/products/anc-headphones-x9", + "data": { + "engagement_type": "link_click" + } + } +} diff --git a/tests/valid/manifest-agent-ctx-resolution.json b/tests/valid/manifest-agent-ctx-resolution.json new file mode 100644 index 0000000..096fc7c --- /dev/null +++ b/tests/valid/manifest-agent-ctx-resolution.json @@ -0,0 +1,12 @@ +{ + "_test_description": "Agent manifest declaring a click-token resolution endpoint in telemetry.ctx_resolution. A destination receiving ctx_iss=searchco.com/agents/web-search resolves this manifest and presents the token at the declared endpoint (sections 7.4, 8.5).", + "schema_version": "0.1", + "id": "https://searchco.com/agents/web-search/.well-known/content-telemetry.json", + "roles": ["agent"], + "operator": { "name": "SearchCo" }, + "telemetry": { + "endpoint": "https://telemetry.example.com/v1/events", + "conformance_level": "citation", + "ctx_resolution": "https://telemetry.example.com/v1/ctx/resolve" + } +} diff --git a/tests/valid/session-earlier-turn-click.json b/tests/valid/session-earlier-turn-click.json new file mode 100644 index 0000000..a6b3ee5 --- /dev/null +++ b/tests/valid/session-earlier-turn-click.json @@ -0,0 +1,53 @@ +{ + "_test_description": "A click in a later turn on a presentation from an earlier turn: the content_engaged event carries the turn of the click but binds by presentation_id to the turn-1 presentation. Resolution returns the clicked content's lineage across turns by content identity, not a turn or timestamp cut (section 7.4).", + "schema_version": "0.1", + "session_id": "660e8400-e29b-41d4-a716-446655440220", + "agent_id": "assistant-v2", + "started_at": "2026-08-10T10:00:00Z", + "events": [ + { + "type": "content_grounded", + "timestamp": "2026-08-10T10:00:01Z", + "turn_id": "1", + "content_id": "publisher:feature:512", + "data": { "scope": "session", "chars_ingested": 15200 } + }, + { + "id": "770e8400-e29b-41d4-a716-446655440221", + "type": "content_cited", + "timestamp": "2026-08-10T10:00:05Z", + "turn_id": "1", + "output_id": "response:1", + "content_id": "publisher:feature:512", + "data": { "citation_type": "reference", "position": "primary" } + }, + { + "id": "880e8400-e29b-41d4-a716-446655440222", + "type": "content_presented", + "timestamp": "2026-08-10T10:00:05Z", + "turn_id": "1", + "output_id": "response:1", + "citation_id": "770e8400-e29b-41d4-a716-446655440221", + "content_id": "publisher:feature:512", + "content_url": "https://publisher.example.com/features/512", + "data": { "presentation_kind": "source_reference", "presentation_type": "link" } + }, + { + "type": "content_grounded", + "timestamp": "2026-08-10T10:03:00Z", + "turn_id": "3", + "content_id": "otherpub:brief:77", + "data": { "scope": "turn", "chars_ingested": 2100 } + }, + { + "type": "content_engaged", + "timestamp": "2026-08-10T10:04:12Z", + "turn_id": "3", + "presentation_id": "880e8400-e29b-41d4-a716-446655440222", + "ctx_token": "ct_e5a0b6d92c17f480", + "content_id": "publisher:feature:512", + "content_url": "https://publisher.example.com/features/512", + "data": { "engagement_type": "link_click" } + } + ] +} diff --git a/tests/valid/session-repeated-link-engagement.json b/tests/valid/session-repeated-link-engagement.json new file mode 100644 index 0000000..19669c7 --- /dev/null +++ b/tests/valid/session-repeated-link-engagement.json @@ -0,0 +1,54 @@ +{ + "_test_description": "The same URL presented twice in one session: two content_presented events with distinct ids and distinct minted click tokens, and a content_engaged bound by presentation_id to the second occurrence. Matching on URL alone could not tell the two presentations apart (sections 6.8, 7.4).", + "schema_version": "0.1", + "session_id": "660e8400-e29b-41d4-a716-446655440210", + "agent_id": "assistant-v2", + "started_at": "2026-08-10T09:00:00Z", + "events": [ + { + "type": "content_grounded", + "timestamp": "2026-08-10T09:00:01Z", + "turn_id": "1", + "content_url": "https://news.example.com/markets/rate-decision", + "content_id": "newsex:article:8841", + "data": { "scope": "session", "chars_ingested": 9400 } + }, + { + "id": "770e8400-e29b-41d4-a716-446655440211", + "type": "content_cited", + "timestamp": "2026-08-10T09:00:04Z", + "turn_id": "1", + "output_id": "response:1", + "content_url": "https://news.example.com/markets/rate-decision", + "data": { "citation_type": "paraphrase", "position": "primary" } + }, + { + "id": "880e8400-e29b-41d4-a716-446655440212", + "type": "content_presented", + "timestamp": "2026-08-10T09:00:04Z", + "turn_id": "1", + "output_id": "response:1", + "citation_id": "770e8400-e29b-41d4-a716-446655440211", + "content_url": "https://news.example.com/markets/rate-decision", + "data": { "presentation_kind": "source_reference", "presentation_type": "link" } + }, + { + "id": "880e8400-e29b-41d4-a716-446655440213", + "type": "content_presented", + "timestamp": "2026-08-10T09:02:10Z", + "turn_id": "2", + "output_id": "response:2", + "content_url": "https://news.example.com/markets/rate-decision", + "data": { "presentation_kind": "source_reference", "presentation_type": "link" } + }, + { + "type": "content_engaged", + "timestamp": "2026-08-10T09:02:31Z", + "turn_id": "2", + "presentation_id": "880e8400-e29b-41d4-a716-446655440213", + "ctx_token": "ct_r2d1a9c44be07f31", + "content_url": "https://news.example.com/markets/rate-decision", + "data": { "engagement_type": "link_click" } + } + ] +} From af939c9d42eeaa53fdfbf7b1381482eb0bae3c47 Mon Sep 17 00:00:00 2001 From: NarrativAI Agent Date: Thu, 13 Aug 2026 21:01:37 +0100 Subject: [PATCH 2/2] Click context: gated contributing sources; owner-scoped engagement delivery (#28, PR #38 review) Two changes from Pedro Santos's review of #38, both from #28. Contributing sources (7.4.4). Lineage by content identity alone cannot support the commerce case: a click through to a destination contributed by another owner's content resolves nothing about the contributor. The click context regains the v0.1 click manifest's role as a consent-gated contributing-source set, narrowed to the engaged presentation's turn and gated per contributing owner's recorded opt-in. Owners without an opt-in remain visible only as counts. The migration note in 12.1 now records a scoping plus the retained gate, not a removal. Owner-scoped delivery (new 7.4.5). The agent-authored click content_engaged is delivered to the engaged content's owner through 7.3 filtered views on the same terms as grounding and citation events, session_id included. The 7.4.4 session_id prohibition binds token resolution (possession of a URL-carried value), not 7.3 delivery to a verified domain owner. This survives token loss in redirect chains, removes the resolver dependency for the destination's own join, and lets a party holding both a contributor's and a destination's owner-scoped streams match on session_id for attribution. Not token distribution: only the clicked content's owner receives the event. Recorded limit renumbered to 7.4.6. Conformance suite 74/74, worked examples 9/9. Co-Authored-By: Claude Fable 5 --- SPECIFICATION.md | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/SPECIFICATION.md b/SPECIFICATION.md index 4121219..fbf71a2 100644 --- a/SPECIFICATION.md +++ b/SPECIFICATION.md @@ -824,7 +824,7 @@ These are the core values. Extensions MAY define additional engagement actions - `link_click` is the primary signal for clickthrough rate calculation. Telemetry consumers can derive per-content-owner and aggregate clickthrough rates from `link_click` engagements and link presentations, joining each engagement through `presentation_id` rather than URL alone. -A `link_click` or `agent_navigate` engagement reported from the landing page after a click-out crosses a trust boundary. Such events carry a `ctx_token` in place of `session_id`, which the telemetry consumer resolves to the click context (see section 7.4). +A `link_click` or `agent_navigate` engagement reported from the landing page after a click-out crosses a trust boundary. Such events carry a `ctx_token` in place of `session_id`, which the telemetry consumer resolves to the click context (see section 7.4). The agent-authored engagement itself reaches the engaged content's owner through owner-scoped routing whether or not the token survived the redirect chain (section 7.4.5). ## 7. Transport @@ -981,9 +981,10 @@ A telemetry consumer that supports resolution exposes, for a presented token, th 1. **The engagement.** The `content_engaged` occurrence(s) bound to the token, including the presentation record the token restores: `presentation_id`, `output_id`, `output_element_id` where present, and timestamps. 2. **The lineage of the clicked content, selected by content identity.** The resolved session's `content_retrieved`, `content_grounded`, `content_reproduced`, `content_cited`, and `content_presented` events whose `content_url` or `content_id` identify the same content as the clicked reference - across all turns. The cut is by content identity, not by turn or click timestamp: a click in turn 5 on content grounded in turn 2 resolves that content's full lineage. -3. **An optional privacy-bounded session summary.** Event counts by type and a distinct-source count. Counts, not events, and no content identifiers of other owners. +3. **The contributing sources, gated per owner.** The sources that informed the response the click came from: `content_grounded` events in scope for the engaged presentation's turn (including session-scoped groundings) and that turn's `content_cited` and `content_presented` events, for content other than the clicked content. A contributing owner's events appear only when that owner has opted in to contributing-source disclosure with the resolving consumer; owners without a recorded opt-in are visible only through the counts in the session summary. This is the component that supports multi-citation attribution when the clicked content is not the contributing content - a click through to a commerce destination whose recommendation a publisher's review produced - and it restores the consent-gated role of the v0.1 click manifest (section 12.1), scoped to the click's provenance rather than the whole session. +4. **An optional privacy-bounded session summary.** Event counts by type and a distinct-source count. Counts, not events, and no content identifiers of owners not disclosed above. -A resolution response MUST NOT include the resolved session's raw `session_id`: the token exists so the session UUID never crosses the trust boundary. A resolution response MUST NOT include events for content other than the clicked content; cross-content session detail is a reporting concern, delivered through publisher-filtered views (section 7.3), not through per-click resolution. A consumer MUST resolve a token only when the issuing agent has opted in to click-token resolution, and the response is gated by the resolved session's `privacy_level`. The mechanism by which the opt-in is recorded is operator-defined; the gate is normative. +A resolution response MUST NOT include the resolved session's raw `session_id`: the token exists so the session UUID never crosses the trust boundary. Outside the contributing-source component, a resolution response MUST NOT include events for content other than the clicked content; within it, a response MUST NOT include events for an owner without a recorded contributing-source opt-in. Whole-session cross-content detail remains a reporting concern, delivered through publisher-filtered views (section 7.3), not through per-click resolution. A consumer MUST resolve a token only when the issuing agent has opted in to click-token resolution, and the response is gated by the resolved session's `privacy_level`. The mechanisms by which the issuer and contributing-owner opt-ins are recorded are operator-defined; both gates are normative. A worked resolution response (informative): @@ -1001,13 +1002,28 @@ A worked resolution response (informative): { "type": "content_presented", "timestamp": "2026-08-10T09:00:04Z", "turn_id": "1", "data": { "presentation_kind": "source_reference", "presentation_type": "link" } }, { "type": "content_presented", "timestamp": "2026-08-10T09:02:10Z", "turn_id": "2", "data": { "presentation_kind": "source_reference", "presentation_type": "link" } } ], + "contributing_sources": [ + { + "content_url": "https://publisher-a.example/heaters/space-heater-review", + "events": [ + { "type": "content_grounded", "timestamp": "2026-08-10T09:02:08Z", "turn_id": "2", "data": { "chars_ingested": 7200 } }, + { "type": "content_cited", "timestamp": "2026-08-10T09:02:10Z", "turn_id": "2", "data": { "citation_type": "paraphrase", "position": "primary" } } + ] + } + ], "session_summary": { "turns": 2, "distinct_sources": 3, "events_by_type": { "content_grounded": 4, "content_cited": 3, "content_presented": 5 } } } ``` The response shape above is informative in v1; the constraints in this section are normative. A response schema can follow implementation evidence during the release-candidate window. -#### 7.4.5 Recorded limit: consumer custody +#### 7.4.5 Owner-scoped delivery of the engagement + +The agent-authored click `content_engaged` is a session event like any other: it reaches content owners through routing and aggregation (section 7.3), independent of whether the token in the URL survived the redirect chain. A telemetry consumer that provides owner-filtered views MUST include the agent-authored `content_engaged` event in the filtered view of the engaged content's owner, on the same terms as `content_grounded` and `content_cited` events - including the session identifier that owner-scoped delivery carries. The `session_id` prohibition in section 7.4.4 binds token resolution, where the requesting party is authenticated by nothing more than possession of a URL-carried value; it does not bind section 7.3 delivery to an owner whose domain registration the consumer has verified. + +This delivery is deliberately redundant with the token path. It notifies the destination owner of the click even when `ctx_token` was stripped in transit; it lets that owner join the click to their own grounded and cited events on `session_id` without calling a resolver; and it lets a party processing owner-scoped streams for both a contributing publisher and a click destination match its clients' events on `session_id` for attribution. The owner's filtered view SHOULD carry the event-level `ctx_token`, so a destination that captured the query parameters at landing can join the URL-channel observation to the server-side event directly. This is not token distribution to contributing owners (the limit recorded in section 7.4.6): only the owner of the clicked content receives the event, and that owner already saw the token in the URL. + +#### 7.4.6 Recorded limit: consumer custody Resolution depends on the telemetry consumer the agent chose, because that consumer holds the session. Grounding and citation events precede the click and cannot carry its later token, and distributing tokens to every contributing content owner after the fact would weaken the privacy boundary this section maintains. Publisher-derived tokens would require a federation and key-management design; that belongs in a later attribution or evidence profile. Core v1 mitigates the dependency with resolver discoverability (7.4.3) and exact click binding (7.4.1). @@ -1352,14 +1368,15 @@ scheme-defined `value`. V1 narrows `ctx_token` resolution. The v0.1 click manifest returned every source that informed the resolved session, gated by per-owner opt-in; the v1 click context (section 7.4) returns the engagement, the clicked content's -lineage by content identity, and at most a count-based session summary. -Consumers implementing v0.1 resolution narrow their response shape and MUST -NOT return other owners' events through per-click resolution. Token values +lineage by content identity, a contributing-source set under the same +per-owner opt-in gate - scoped to the turn the click came from rather than +the whole session - and at most a count-based session summary. Consumers +implementing v0.1 resolution narrow the contributing-source scope accordingly +and MUST NOT return events for owners without a recorded opt-in. Token values gain the `ct_` pattern; presentation binding moves from the URL-carried `presentation_id` a destination could never legitimately know to issuer state restored at resolution. - Preview versions (0.x) use two-component version numbers. From 1.0.0 onward, versions follow [semantic versioning](https://semver.org/): - **Major** (1.0.0 → 2.0.0) - breaking changes to required fields