From bc9b317ad287f9471a3761835bab606b7f502b96 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 5 Jul 2026 18:45:20 +0100 Subject: [PATCH 1/2] feat(registry): type the change-feed event payload as a discriminated union The `GET /api/registry/feed` (and `/feed/stream`) events previously exposed `event_type: string` and `payload: object` (opaque), forcing every consumer to hand-cast `payload` to reach `publisher_domain` / `agent_url`. Type them at the Zod source (`RegistryFeedPageSchema`): `event_type` is now an enum of the known types across the agent, brand, property, collection, publisher, and authorization families, and each event is a discriminated-union arm tying its `event_type` literal to a named family payload (`AgentEventPayload`, `BrandEventPayload`, `PropertyEventPayload`, `CollectionEventPayload`, `AuthorizationEventPayload`, `PublisherEventPayload`). Payload fields that appear on only some members of a family (e.g. `*.merged` carries `alias_rid`/`canonical_rid` in place of `identifiers`) are optional. Enum coverage is grounded in the actual contract: the crawler's emit sites, the server's own registry-sync reference consumer, and the SDK's RegistrySync switch (which also handles the brand.* family and agent profile/compliance events). The generated `oneOf` uses per-arm enum discriminators (matching the repo's existing union style); openapi-typescript renders it as a proper discriminated union so consumers narrow `payload` by switching on `event_type`. Verified: server typecheck, OpenAPI freshness, oneof-discriminator audit, and a full @adcp/sdk regen (types generate, no loose-oneof arms, lib typecheck clean). Co-Authored-By: Claude Opus 4.8 (1M context) --- server/src/routes/registry-api.ts | 146 +++++- static/openapi/registry.yaml | 823 ++++++++++++++++++++++++++++-- 2 files changed, 934 insertions(+), 35 deletions(-) diff --git a/server/src/routes/registry-api.ts b/server/src/routes/registry-api.ts index dacc533c7f..bde0a81d0e 100644 --- a/server/src/routes/registry-api.ts +++ b/server/src/routes/registry-api.ts @@ -1967,16 +1967,150 @@ const RegistryFeedFreshnessSchema = z.object({ }), }); -const RegistryFeedPageSchema = z.object({ - events: z.array(z.object({ +// --- Feed event payloads, typed per event family --------------------------- +// The change feed carries one `payload` shape per event family. Typing them +// (rather than an opaque object) lets consumers route on `publisher_domain` / +// `agent_url` without hand-casting. Fields that appear on only some members of +// a family (e.g. `*.merged` carries `alias_rid`/`canonical_rid` in place of +// `identifiers`) are optional on the family schema. + +const AgentEventPayloadSchema = z + .object({ + agent_url: z.string().openapi({ description: "Canonical agent URL; the routing key for agent.* events (agents span many publishers)." }), + name: z.string().optional(), + type: z.string().optional(), + channels: z.array(z.string()).optional(), + property_types: z.array(z.string()).optional(), + markets: z.array(z.string()).optional(), + categories: z.array(z.string()).optional(), + tags: z.array(z.string()).optional(), + delivery_types: z.array(z.string()).optional(), + property_count: z.number().int().optional(), + publisher_count: z.number().int().optional(), + has_tmp: z.boolean().optional(), + inventory_profile: z.record(z.string(), z.unknown()).optional().openapi({ description: "On agent.profile_updated: the agent's refreshed inventory profile." }), + compliance_summary: z.record(z.string(), z.unknown()).optional(), + previous_status: z.string().optional().openapi({ description: "On agent.compliance_changed: prior compliance/verification status." }), + current_status: z.string().optional().openapi({ description: "On agent.compliance_changed: new compliance/verification status." }), + }) + .openapi("AgentEventPayload"); + +const BrandEventPayloadSchema = z + .object({ + domain: z.string().optional().openapi({ description: "Brand domain; brand.* events are identified by entity_id (the brand) and carry hierarchy context here." }), + chain: z.array(z.record(z.string(), z.unknown())).optional().openapi({ description: "On brand.resolved/hierarchy_updated: the resolved brand chain (root → leaf)." }), + ancestor_domains: z.array(z.string()).optional(), + domains: z.array(z.string()).optional(), + }) + .openapi("BrandEventPayload"); + +const PropertyEventPayloadSchema = z + .object({ + property_rid: z.string().optional(), + publisher_domain: z.string().optional().openapi({ description: "Publisher domain that owns the property; the routing key for property.* events." }), + identifiers: z.array(PropertyIdentifierSchema).optional(), + classification: z.string().optional(), + alias_rid: z.string().optional().openapi({ description: "On property.merged: the RID merged away." }), + canonical_rid: z.string().optional().openapi({ description: "On property.merged: the surviving RID." }), + evidence: z.string().optional(), + }) + .openapi("PropertyEventPayload"); + +const CollectionEventPayloadSchema = z + .object({ + collection_rid: z.string().optional(), + publisher_domain: z.string().optional().openapi({ description: "Publisher domain that owns the collection; the routing key for collection.* events." }), + collection_id: z.string().nullable().optional(), + name: z.string().nullable().optional(), + kind: z.string().nullable().optional(), + source: z.string().optional(), + status: z.string().optional(), + identifiers: z + .array(z.object({ publisher_domain: z.string(), type: z.string(), value: z.string() })) + .optional() + .openapi({ description: "Distribution identifiers; the per-identifier publisher_domain (e.g. youtube.com) is the distribution surface, distinct from the owning publisher_domain above." }), + collection: z.record(z.string(), z.unknown()).optional(), + alias_rid: z.string().optional().openapi({ description: "On collection.merged: the RID merged away." }), + canonical_rid: z.string().optional().openapi({ description: "On collection.merged: the surviving RID." }), + evidence: z.string().optional(), + }) + .openapi("CollectionEventPayload"); + +const AuthorizationEventPayloadSchema = z + .object({ + agent_url: z.string(), + publisher_domain: z.string().openapi({ description: "Publisher domain the authorization applies to; the routing key for authorization.* events." }), + authorization_type: z.string().optional().openapi({ description: "Present on authorization.granted; authorization.revoked carries only agent_url + publisher_domain." }), + authorized_for: z.string().optional(), + property_ids: z.array(z.string()).optional(), + property_tags: z.array(z.string()).optional(), + placement_ids: z.array(z.string()).optional(), + placement_tags: z.array(z.string()).optional(), + collections: z + .array(z.object({ publisher_domain: z.string(), collection_id: z.string() })) + .optional(), + countries: z.array(z.string()).optional(), + delegation_type: z.string().optional(), + exclusive: z.boolean().optional(), + effective_from: z.string().optional(), + effective_until: z.string().optional(), + signing_keys: z.array(z.record(z.string(), z.unknown())).optional(), + }) + .openapi("AuthorizationEventPayload"); + +const PublisherEventPayloadSchema = z + .object({ + publisher_domain: z.string().openapi({ description: "Publisher domain whose adagents.json was discovered/changed; the routing key for publisher.* events." }), + agent_count: z.number().int().optional(), + property_count: z.number().int().optional(), + collection_count: z.number().int().optional(), + discovery_method: z.string().optional(), + manager_domain: z.string().nullable().optional(), + source: z.string().optional(), + }) + .openapi("PublisherEventPayload"); + +// One arm per event_type, discriminated on `event_type`. Each arm ties the +// literal type to its family payload so consumers narrow `payload` by switching +// on `event_type`. +const feedEventArm = (eventType: T, payload: z.ZodTypeAny) => + z.object({ event_id: z.string().uuid(), - event_type: z.string().openapi({ example: "property.created" }), - entity_type: z.string().openapi({ example: "property" }), + event_type: z.literal(eventType), + entity_type: z.string(), entity_id: z.string(), - payload: z.record(z.string(), z.unknown()), + payload, actor: z.string(), created_at: z.string().datetime(), - })), + }); + +const RegistryFeedEventSchema = z + .discriminatedUnion("event_type", [ + feedEventArm("agent.discovered", AgentEventPayloadSchema), + feedEventArm("agent.removed", AgentEventPayloadSchema), + feedEventArm("agent.profile_updated", AgentEventPayloadSchema), + feedEventArm("agent.compliance_changed", AgentEventPayloadSchema), + feedEventArm("brand.resolved", BrandEventPayloadSchema), + feedEventArm("brand.updated", BrandEventPayloadSchema), + feedEventArm("brand.removed", BrandEventPayloadSchema), + feedEventArm("brand.deleted", BrandEventPayloadSchema), + feedEventArm("brand.hierarchy_updated", BrandEventPayloadSchema), + feedEventArm("property.created", PropertyEventPayloadSchema), + feedEventArm("property.updated", PropertyEventPayloadSchema), + feedEventArm("property.merged", PropertyEventPayloadSchema), + feedEventArm("collection.created", CollectionEventPayloadSchema), + feedEventArm("collection.updated", CollectionEventPayloadSchema), + feedEventArm("collection.merged", CollectionEventPayloadSchema), + feedEventArm("collection.removed", CollectionEventPayloadSchema), + feedEventArm("authorization.granted", AuthorizationEventPayloadSchema), + feedEventArm("authorization.revoked", AuthorizationEventPayloadSchema), + feedEventArm("publisher.adagents_changed", PublisherEventPayloadSchema), + feedEventArm("publisher.adagents_discovered", PublisherEventPayloadSchema), + ]) + .openapi("RegistryFeedEvent"); + +const RegistryFeedPageSchema = z.object({ + events: z.array(RegistryFeedEventSchema), cursor: z.string().uuid().nullable().openapi({ description: "Pass as cursor in the next request to continue polling" }), has_more: z.boolean(), freshness: RegistryFeedFreshnessSchema, diff --git a/static/openapi/registry.yaml b/static/openapi/registry.yaml index 83038a48f8..6da4d506cc 100644 --- a/static/openapi/registry.yaml +++ b/static/openapi/registry.yaml @@ -2170,6 +2170,799 @@ components: - policy_id - total - revisions + RegistryFeedEvent: + oneOf: + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - agent.discovered + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/AgentEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - agent.removed + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/AgentEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - agent.profile_updated + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/AgentEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - agent.compliance_changed + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/AgentEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - brand.resolved + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/BrandEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - brand.updated + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/BrandEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - brand.removed + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/BrandEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - brand.deleted + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/BrandEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - brand.hierarchy_updated + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/BrandEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - property.created + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/PropertyEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - property.updated + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/PropertyEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - property.merged + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/PropertyEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - collection.created + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/CollectionEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - collection.updated + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/CollectionEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - collection.merged + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/CollectionEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - collection.removed + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/CollectionEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - authorization.granted + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/AuthorizationEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - authorization.revoked + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/AuthorizationEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - publisher.adagents_changed + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/PublisherEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + - type: object + properties: + event_id: + type: string + format: uuid + event_type: + type: string + enum: + - publisher.adagents_discovered + entity_type: + type: string + entity_id: + type: string + payload: + $ref: "#/components/schemas/PublisherEventPayload" + actor: + type: string + created_at: + type: string + format: date-time + required: + - event_id + - event_type + - entity_type + - entity_id + - payload + - actor + - created_at + AgentEventPayload: + type: object + properties: + agent_url: + type: string + description: Canonical agent URL; the routing key for agent.* events (agents span many publishers). + name: + type: string + type: + type: string + channels: + type: array + items: + type: string + property_types: + type: array + items: + type: string + markets: + type: array + items: + type: string + categories: + type: array + items: + type: string + tags: + type: array + items: + type: string + delivery_types: + type: array + items: + type: string + property_count: + type: integer + publisher_count: + type: integer + has_tmp: + type: boolean + inventory_profile: + type: object + additionalProperties: {} + description: "On agent.profile_updated: the agent's refreshed inventory profile." + compliance_summary: + type: object + additionalProperties: {} + previous_status: + type: string + description: "On agent.compliance_changed: prior compliance/verification status." + current_status: + type: string + description: "On agent.compliance_changed: new compliance/verification status." + required: + - agent_url + BrandEventPayload: + type: object + properties: + domain: + type: string + description: Brand domain; brand.* events are identified by entity_id (the brand) and carry hierarchy context here. + chain: + type: array + items: + type: object + additionalProperties: {} + description: "On brand.resolved/hierarchy_updated: the resolved brand chain (root → leaf)." + ancestor_domains: + type: array + items: + type: string + domains: + type: array + items: + type: string + PropertyEventPayload: + type: object + properties: + property_rid: + type: string + publisher_domain: + type: string + description: Publisher domain that owns the property; the routing key for property.* events. + identifiers: + type: array + items: + $ref: "#/components/schemas/PropertyIdentifier" + classification: + type: string + alias_rid: + type: string + description: "On property.merged: the RID merged away." + canonical_rid: + type: string + description: "On property.merged: the surviving RID." + evidence: + type: string + CollectionEventPayload: + type: object + properties: + collection_rid: + type: string + publisher_domain: + type: string + description: Publisher domain that owns the collection; the routing key for collection.* events. + collection_id: + type: + - string + - "null" + name: + type: + - string + - "null" + kind: + type: + - string + - "null" + source: + type: string + status: + type: string + identifiers: + type: array + items: + type: object + properties: + publisher_domain: + type: string + type: + type: string + value: + type: string + required: + - publisher_domain + - type + - value + description: Distribution identifiers; the per-identifier publisher_domain (e.g. youtube.com) is the distribution surface, distinct from the owning publisher_domain above. + collection: + type: object + additionalProperties: {} + alias_rid: + type: string + description: "On collection.merged: the RID merged away." + canonical_rid: + type: string + description: "On collection.merged: the surviving RID." + evidence: + type: string + AuthorizationEventPayload: + type: object + properties: + agent_url: + type: string + publisher_domain: + type: string + description: Publisher domain the authorization applies to; the routing key for authorization.* events. + authorization_type: + type: string + description: Present on authorization.granted; authorization.revoked carries only agent_url + publisher_domain. + authorized_for: + type: string + property_ids: + type: array + items: + type: string + property_tags: + type: array + items: + type: string + placement_ids: + type: array + items: + type: string + placement_tags: + type: array + items: + type: string + collections: + type: array + items: + type: object + properties: + publisher_domain: + type: string + collection_id: + type: string + required: + - publisher_domain + - collection_id + countries: + type: array + items: + type: string + delegation_type: + type: string + exclusive: + type: boolean + effective_from: + type: string + effective_until: + type: string + signing_keys: + type: array + items: + type: object + additionalProperties: {} + required: + - agent_url + - publisher_domain + PublisherEventPayload: + type: object + properties: + publisher_domain: + type: string + description: Publisher domain whose adagents.json was discovered/changed; the routing key for publisher.* events. + agent_count: + type: integer + property_count: + type: integer + collection_count: + type: integer + discovery_method: + type: string + manager_domain: + type: + - string + - "null" + source: + type: string + required: + - publisher_domain AgentComplianceDetail: type: object properties: @@ -6913,35 +7706,7 @@ paths: events: type: array items: - type: object - properties: - event_id: - type: string - format: uuid - event_type: - type: string - example: property.created - entity_type: - type: string - example: property - entity_id: - type: string - payload: - type: object - additionalProperties: {} - actor: - type: string - created_at: - type: string - format: date-time - required: - - event_id - - event_type - - entity_type - - entity_id - - payload - - actor - - created_at + $ref: "#/components/schemas/RegistryFeedEvent" cursor: type: - string From eb0e3007466df6c3a51577964ad72658f5d3129b Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sun, 5 Jul 2026 22:44:55 +0100 Subject: [PATCH 2/2] fix(registry): align feed OpenAPI event schema --- server/src/routes/registry-api.ts | 82 +++++++-- static/openapi/registry.yaml | 265 +++++++++++++++++++++++++----- 2 files changed, 288 insertions(+), 59 deletions(-) diff --git a/server/src/routes/registry-api.ts b/server/src/routes/registry-api.ts index bde0a81d0e..66e077490b 100644 --- a/server/src/routes/registry-api.ts +++ b/server/src/routes/registry-api.ts @@ -1974,6 +1974,19 @@ const RegistryFeedFreshnessSchema = z.object({ // a family (e.g. `*.merged` carries `alias_rid`/`canonical_rid` in place of // `identifiers`) are optional on the family schema. +const ComplianceTrackStatusSchema = z.enum(["pass", "fail", "partial", "skip", "silent", "warning", "unknown", "skipped"]); + +const ComplianceStoryboardStatusSchema = z + .object({ + storyboard_id: z.string(), + status: z.string(), + steps_passed: z.number().int().nonnegative().optional(), + steps_total: z.number().int().nonnegative().optional(), + }) + .passthrough(); + +const ChangedFieldsSchema = z.array(z.string()).min(1); + const AgentEventPayloadSchema = z .object({ agent_url: z.string().openapi({ description: "Canonical agent URL; the routing key for agent.* events (agents span many publishers)." }), @@ -1983,37 +1996,49 @@ const AgentEventPayloadSchema = z property_types: z.array(z.string()).optional(), markets: z.array(z.string()).optional(), categories: z.array(z.string()).optional(), + category_taxonomy: z.string().nullable().optional(), tags: z.array(z.string()).optional(), delivery_types: z.array(z.string()).optional(), + format_ids: z.array(z.string()).optional(), property_count: z.number().int().optional(), publisher_count: z.number().int().optional(), has_tmp: z.boolean().optional(), + updated_at: z.string().optional(), + changed_fields: ChangedFieldsSchema.optional(), inventory_profile: z.record(z.string(), z.unknown()).optional().openapi({ description: "On agent.profile_updated: the agent's refreshed inventory profile." }), compliance_summary: z.record(z.string(), z.unknown()).optional(), previous_status: z.string().optional().openapi({ description: "On agent.compliance_changed: prior compliance/verification status." }), current_status: z.string().optional().openapi({ description: "On agent.compliance_changed: new compliance/verification status." }), + headline: z.string().nullable().optional().openapi({ description: "On agent.compliance_changed: human-readable summary of the compliance transition." }), + tracks: z.record(z.string(), ComplianceTrackStatusSchema).optional().openapi({ description: "On agent.compliance_changed: map of compliance track id to track status." }), + storyboards_passing: z.number().int().nonnegative().optional(), + storyboards_total: z.number().int().nonnegative().optional(), + storyboards: z.array(ComplianceStoryboardStatusSchema).optional(), + role: z.string().optional().openapi({ description: "On agent.verification_earned/lost: verified role affected by the badge transition." }), + verified_specialisms: z.array(z.string()).optional().openapi({ description: "On agent.verification_earned: specialisms covered by the earned badge." }), + reason: z.string().optional().openapi({ description: "On agent.verification_lost: reason the badge was revoked." }), + adcp_version: z.string().optional().openapi({ description: "On agent.verification_earned/lost: AdCP version the badge applies to, when known." }), }) + .passthrough() .openapi("AgentEventPayload"); -const BrandEventPayloadSchema = z - .object({ - domain: z.string().optional().openapi({ description: "Brand domain; brand.* events are identified by entity_id (the brand) and carry hierarchy context here." }), - chain: z.array(z.record(z.string(), z.unknown())).optional().openapi({ description: "On brand.resolved/hierarchy_updated: the resolved brand chain (root → leaf)." }), - ancestor_domains: z.array(z.string()).optional(), - domains: z.array(z.string()).optional(), - }) - .openapi("BrandEventPayload"); - const PropertyEventPayloadSchema = z .object({ property_rid: z.string().optional(), publisher_domain: z.string().optional().openapi({ description: "Publisher domain that owns the property; the routing key for property.* events." }), identifiers: z.array(PropertyIdentifierSchema).optional(), classification: z.string().optional(), + source: z.enum(["authoritative", "enriched", "contributed"]).optional(), + property: z.record(z.string(), z.unknown()).optional().openapi({ description: "Optional full post-change property object when available." }), + changed_fields: ChangedFieldsSchema.optional(), + last_resolved_at: z.string().optional().openapi({ description: "On property.stale: last successful resolution timestamp." }), + reactivated_at: z.string().optional().openapi({ description: "On property.reactivated: reactivation timestamp when available." }), + reason: z.string().optional().openapi({ description: "On property.stale: reason the property aged out of active resolution." }), alias_rid: z.string().optional().openapi({ description: "On property.merged: the RID merged away." }), canonical_rid: z.string().optional().openapi({ description: "On property.merged: the surviving RID." }), evidence: z.string().optional(), }) + .passthrough() .openapi("PropertyEventPayload"); const CollectionEventPayloadSchema = z @@ -2030,24 +2055,32 @@ const CollectionEventPayloadSchema = z .optional() .openapi({ description: "Distribution identifiers; the per-identifier publisher_domain (e.g. youtube.com) is the distribution surface, distinct from the owning publisher_domain above." }), collection: z.record(z.string(), z.unknown()).optional(), + changed_fields: ChangedFieldsSchema.optional(), alias_rid: z.string().optional().openapi({ description: "On collection.merged: the RID merged away." }), canonical_rid: z.string().optional().openapi({ description: "On collection.merged: the surviving RID." }), evidence: z.string().optional(), }) + .passthrough() .openapi("CollectionEventPayload"); const AuthorizationEventPayloadSchema = z .object({ + id: z.string().uuid().optional().openapi({ description: "Registry authorization row id when the event is backed by a materialized effective authorization row." }), agent_url: z.string(), + agent_url_canonical: z.string().optional().openapi({ description: "Registry-canonicalized form of agent_url for equality checks." }), publisher_domain: z.string().openapi({ description: "Publisher domain the authorization applies to; the routing key for authorization.* events." }), authorization_type: z.string().optional().openapi({ description: "Present on authorization.granted; authorization.revoked carries only agent_url + publisher_domain." }), - authorized_for: z.string().optional(), + authorized_for: z.string().nullable().optional(), property_ids: z.array(z.string()).optional(), property_tags: z.array(z.string()).optional(), + properties: z.array(z.record(z.string(), z.unknown())).optional(), + publisher_properties: z.array(z.record(z.string(), z.unknown())).optional(), + property_rid: z.string().nullable().optional().openapi({ description: "Catalog property_rid for materialized per-property authorization rows. Null for publisher-wide rows." }), + property_id_slug: z.string().nullable().optional().openapi({ description: "Publisher-local property id for materialized per-property authorization rows." }), placement_ids: z.array(z.string()).optional(), placement_tags: z.array(z.string()).optional(), collections: z - .array(z.object({ publisher_domain: z.string(), collection_id: z.string() })) + .array(z.object({ publisher_domain: z.string(), collection_ids: z.array(z.string()).min(1) }).passthrough()) .optional(), countries: z.array(z.string()).optional(), delegation_type: z.string().optional(), @@ -2055,12 +2088,26 @@ const AuthorizationEventPayloadSchema = z effective_from: z.string().optional(), effective_until: z.string().optional(), signing_keys: z.array(z.record(z.string(), z.unknown())).optional(), + evidence: z.string().optional(), + disputed: z.boolean().optional(), + created_by: z.string().nullable().optional(), + expires_at: z.string().nullable().optional(), + created_at: z.string().nullable().optional(), + updated_at: z.string().nullable().optional(), + override_applied: z.boolean().optional(), + override_reason: z.string().nullable().optional(), }) + .passthrough() .openapi("AuthorizationEventPayload"); const PublisherEventPayloadSchema = z .object({ - publisher_domain: z.string().openapi({ description: "Publisher domain whose adagents.json was discovered/changed; the routing key for publisher.* events." }), + publisher_domain: z.string().optional().openapi({ description: "Publisher domain whose adagents.json was discovered/changed; the routing key for publisher.* events." }), + domain: z.string().optional().openapi({ description: "Legacy alias for publisher_domain retained for early feed examples." }), + properties_added: z.number().int().nonnegative().optional(), + properties_removed: z.number().int().nonnegative().optional(), + agents_added: z.array(z.string()).optional(), + agents_removed: z.array(z.string()).optional(), agent_count: z.number().int().optional(), property_count: z.number().int().optional(), collection_count: z.number().int().optional(), @@ -2068,6 +2115,7 @@ const PublisherEventPayloadSchema = z manager_domain: z.string().nullable().optional(), source: z.string().optional(), }) + .passthrough() .openapi("PublisherEventPayload"); // One arm per event_type, discriminated on `event_type`. Each arm ties the @@ -2090,20 +2138,20 @@ const RegistryFeedEventSchema = z feedEventArm("agent.removed", AgentEventPayloadSchema), feedEventArm("agent.profile_updated", AgentEventPayloadSchema), feedEventArm("agent.compliance_changed", AgentEventPayloadSchema), - feedEventArm("brand.resolved", BrandEventPayloadSchema), - feedEventArm("brand.updated", BrandEventPayloadSchema), - feedEventArm("brand.removed", BrandEventPayloadSchema), - feedEventArm("brand.deleted", BrandEventPayloadSchema), - feedEventArm("brand.hierarchy_updated", BrandEventPayloadSchema), + feedEventArm("agent.verification_earned", AgentEventPayloadSchema), + feedEventArm("agent.verification_lost", AgentEventPayloadSchema), feedEventArm("property.created", PropertyEventPayloadSchema), feedEventArm("property.updated", PropertyEventPayloadSchema), feedEventArm("property.merged", PropertyEventPayloadSchema), + feedEventArm("property.stale", PropertyEventPayloadSchema), + feedEventArm("property.reactivated", PropertyEventPayloadSchema), feedEventArm("collection.created", CollectionEventPayloadSchema), feedEventArm("collection.updated", CollectionEventPayloadSchema), feedEventArm("collection.merged", CollectionEventPayloadSchema), feedEventArm("collection.removed", CollectionEventPayloadSchema), feedEventArm("authorization.granted", AuthorizationEventPayloadSchema), feedEventArm("authorization.revoked", AuthorizationEventPayloadSchema), + feedEventArm("authorization.modified", AuthorizationEventPayloadSchema), feedEventArm("publisher.adagents_changed", PublisherEventPayloadSchema), feedEventArm("publisher.adagents_discovered", PublisherEventPayloadSchema), ]) diff --git a/static/openapi/registry.yaml b/static/openapi/registry.yaml index 6da4d506cc..119ffbb0eb 100644 --- a/static/openapi/registry.yaml +++ b/static/openapi/registry.yaml @@ -2292,13 +2292,13 @@ components: event_type: type: string enum: - - brand.resolved + - agent.verification_earned entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/BrandEventPayload" + $ref: "#/components/schemas/AgentEventPayload" actor: type: string created_at: @@ -2320,13 +2320,13 @@ components: event_type: type: string enum: - - brand.updated + - agent.verification_lost entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/BrandEventPayload" + $ref: "#/components/schemas/AgentEventPayload" actor: type: string created_at: @@ -2348,13 +2348,13 @@ components: event_type: type: string enum: - - brand.removed + - property.created entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/BrandEventPayload" + $ref: "#/components/schemas/PropertyEventPayload" actor: type: string created_at: @@ -2376,13 +2376,13 @@ components: event_type: type: string enum: - - brand.deleted + - property.updated entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/BrandEventPayload" + $ref: "#/components/schemas/PropertyEventPayload" actor: type: string created_at: @@ -2404,13 +2404,13 @@ components: event_type: type: string enum: - - brand.hierarchy_updated + - property.merged entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/BrandEventPayload" + $ref: "#/components/schemas/PropertyEventPayload" actor: type: string created_at: @@ -2432,7 +2432,7 @@ components: event_type: type: string enum: - - property.created + - property.stale entity_type: type: string entity_id: @@ -2460,7 +2460,7 @@ components: event_type: type: string enum: - - property.updated + - property.reactivated entity_type: type: string entity_id: @@ -2488,13 +2488,13 @@ components: event_type: type: string enum: - - property.merged + - collection.created entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/PropertyEventPayload" + $ref: "#/components/schemas/CollectionEventPayload" actor: type: string created_at: @@ -2516,7 +2516,7 @@ components: event_type: type: string enum: - - collection.created + - collection.updated entity_type: type: string entity_id: @@ -2544,7 +2544,7 @@ components: event_type: type: string enum: - - collection.updated + - collection.merged entity_type: type: string entity_id: @@ -2572,7 +2572,7 @@ components: event_type: type: string enum: - - collection.merged + - collection.removed entity_type: type: string entity_id: @@ -2600,13 +2600,13 @@ components: event_type: type: string enum: - - collection.removed + - authorization.granted entity_type: type: string entity_id: type: string payload: - $ref: "#/components/schemas/CollectionEventPayload" + $ref: "#/components/schemas/AuthorizationEventPayload" actor: type: string created_at: @@ -2628,7 +2628,7 @@ components: event_type: type: string enum: - - authorization.granted + - authorization.revoked entity_type: type: string entity_id: @@ -2656,7 +2656,7 @@ components: event_type: type: string enum: - - authorization.revoked + - authorization.modified entity_type: type: string entity_id: @@ -2758,6 +2758,10 @@ components: type: array items: type: string + category_taxonomy: + type: + - string + - "null" tags: type: array items: @@ -2766,12 +2770,23 @@ components: type: array items: type: string + format_ids: + type: array + items: + type: string property_count: type: integer publisher_count: type: integer has_tmp: type: boolean + updated_at: + type: string + changed_fields: + type: array + items: + type: string + minItems: 1 inventory_profile: type: object additionalProperties: {} @@ -2785,28 +2800,67 @@ components: current_status: type: string description: "On agent.compliance_changed: new compliance/verification status." - required: - - agent_url - BrandEventPayload: - type: object - properties: - domain: - type: string - description: Brand domain; brand.* events are identified by entity_id (the brand) and carry hierarchy context here. - chain: + headline: + type: + - string + - "null" + description: "On agent.compliance_changed: human-readable summary of the compliance transition." + tracks: + type: object + additionalProperties: + type: string + enum: + - pass + - fail + - partial + - skip + - silent + - warning + - unknown + - skipped + description: "On agent.compliance_changed: map of compliance track id to track status." + storyboards_passing: + type: integer + minimum: 0 + storyboards_total: + type: integer + minimum: 0 + storyboards: type: array items: type: object + properties: + storyboard_id: + type: string + status: + type: string + steps_passed: + type: integer + minimum: 0 + steps_total: + type: integer + minimum: 0 + required: + - storyboard_id + - status additionalProperties: {} - description: "On brand.resolved/hierarchy_updated: the resolved brand chain (root → leaf)." - ancestor_domains: - type: array - items: - type: string - domains: + role: + type: string + description: "On agent.verification_earned/lost: verified role affected by the badge transition." + verified_specialisms: type: array items: type: string + description: "On agent.verification_earned: specialisms covered by the earned badge." + reason: + type: string + description: "On agent.verification_lost: reason the badge was revoked." + adcp_version: + type: string + description: "On agent.verification_earned/lost: AdCP version the badge applies to, when known." + required: + - agent_url + additionalProperties: {} PropertyEventPayload: type: object properties: @@ -2821,6 +2875,30 @@ components: $ref: "#/components/schemas/PropertyIdentifier" classification: type: string + source: + type: string + enum: + - authoritative + - enriched + - contributed + property: + type: object + additionalProperties: {} + description: Optional full post-change property object when available. + changed_fields: + type: array + items: + type: string + minItems: 1 + last_resolved_at: + type: string + description: "On property.stale: last successful resolution timestamp." + reactivated_at: + type: string + description: "On property.reactivated: reactivation timestamp when available." + reason: + type: string + description: "On property.stale: reason the property aged out of active resolution." alias_rid: type: string description: "On property.merged: the RID merged away." @@ -2829,6 +2907,7 @@ components: description: "On property.merged: the surviving RID." evidence: type: string + additionalProperties: {} CollectionEventPayload: type: object properties: @@ -2872,6 +2951,11 @@ components: collection: type: object additionalProperties: {} + changed_fields: + type: array + items: + type: string + minItems: 1 alias_rid: type: string description: "On collection.merged: the RID merged away." @@ -2880,11 +2964,19 @@ components: description: "On collection.merged: the surviving RID." evidence: type: string + additionalProperties: {} AuthorizationEventPayload: type: object properties: + id: + type: string + format: uuid + description: Registry authorization row id when the event is backed by a materialized effective authorization row. agent_url: type: string + agent_url_canonical: + type: string + description: Registry-canonicalized form of agent_url for equality checks. publisher_domain: type: string description: Publisher domain the authorization applies to; the routing key for authorization.* events. @@ -2892,7 +2984,9 @@ components: type: string description: Present on authorization.granted; authorization.revoked carries only agent_url + publisher_domain. authorized_for: - type: string + type: + - string + - "null" property_ids: type: array items: @@ -2901,6 +2995,26 @@ components: type: array items: type: string + properties: + type: array + items: + type: object + additionalProperties: {} + publisher_properties: + type: array + items: + type: object + additionalProperties: {} + property_rid: + type: + - string + - "null" + description: Catalog property_rid for materialized per-property authorization rows. Null for publisher-wide rows. + property_id_slug: + type: + - string + - "null" + description: Publisher-local property id for materialized per-property authorization rows. placement_ids: type: array items: @@ -2916,11 +3030,15 @@ components: properties: publisher_domain: type: string - collection_id: - type: string + collection_ids: + type: array + items: + type: string + minItems: 1 required: - publisher_domain - - collection_id + - collection_ids + additionalProperties: {} countries: type: array items: @@ -2938,15 +3056,59 @@ components: items: type: object additionalProperties: {} + evidence: + type: string + disputed: + type: boolean + created_by: + type: + - string + - "null" + expires_at: + type: + - string + - "null" + created_at: + type: + - string + - "null" + updated_at: + type: + - string + - "null" + override_applied: + type: boolean + override_reason: + type: + - string + - "null" required: - agent_url - publisher_domain + additionalProperties: {} PublisherEventPayload: type: object properties: publisher_domain: type: string description: Publisher domain whose adagents.json was discovered/changed; the routing key for publisher.* events. + domain: + type: string + description: Legacy alias for publisher_domain retained for early feed examples. + properties_added: + type: integer + minimum: 0 + properties_removed: + type: integer + minimum: 0 + agents_added: + type: array + items: + type: string + agents_removed: + type: array + items: + type: string agent_count: type: integer property_count: @@ -2961,8 +3123,7 @@ components: - "null" source: type: string - required: - - publisher_domain + additionalProperties: {} AgentComplianceDetail: type: object properties: @@ -4313,6 +4474,26 @@ components: - created_at - updated_at - source_updated_at + BrandEventPayload: + type: object + properties: + domain: + type: string + description: Brand domain; brand.* events are identified by entity_id (the brand) and carry hierarchy context here. + chain: + type: array + items: + type: object + additionalProperties: {} + description: "On brand.resolved/hierarchy_updated: the resolved brand chain (root → leaf)." + ancestor_domains: + type: array + items: + type: string + domains: + type: array + items: + type: string AdagentsJson: type: object properties: