Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/current/SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -1267,6 +1267,53 @@ Returns the platform-level EEP capabilities document conforming to `schemas/v0.1
}
```

### 12.3.2 Event catalog (normative)

The manifest tells an agent **that** a publisher speaks EEP and **where** its
endpoints are. Until now it did not tell the agent **what the publisher will
send** — so an agent had to read human documentation, or subscribe to a
wildcard and discover the event types empirically.

That undercuts the autonomous-agent story directly. §9's "standard event
catalog" is a table in this document: not fetchable, not versioned per
publisher, and not machine-readable. `capabilities_query_url` (§12.4) covers
gated *capabilities*, not event types. The envelope's `eep_known_event_types`
puts a discovery concern on the per-event hot path, which is the wrong layer.

Publishers SHOULD declare their event types in the manifest:

```json
{
"event_types": [
{
"type": "com.example.entity.updated",
"description": "A field on the entity profile changed.",
"dataschema": "https://example.com/schemas/entity.updated/v2.json",
"since": "0.1"
},
{
"type": "com.example.trust.changed",
"description": "The entity's trust score was recalculated.",
"deprecated": true
}
]
}
```

- `type` is required and follows the reverse-DNS convention of §8.
- `dataschema` SHOULD be set, and SHOULD match the `dataschema` attribute the
publisher sets on events of that type (§7.1), so the payload contract is
discoverable *and* travels with each event.
- A publisher whose catalog is too large or too volatile to inline SHOULD
expose `event_catalog_url` instead, paginated like §12.4. When both are
present, `event_types` is a subset and the URL is authoritative.
- Declaring an event type is not a promise to emit it, and omitting one is not
a promise never to. Subscribers MUST tolerate an undeclared event type
rather than rejecting the delivery — a catalog is discovery metadata, not a
wire constraint.
- Publishers SHOULD mark a type `deprecated` and keep emitting it for a
documented period rather than removing it silently.

### 12.4 Dynamic Capability Discovery

For entities with large data catalogs or frequently changing capabilities, static manifests become stale. Publishers MAY expose a paginated capability query endpoint:
Expand Down
43 changes: 43 additions & 0 deletions packages/@eep-dev/middleware/src/core/eep-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,49 @@ describe("EEPServer", () => {
});
});

// SPECIFICATION.md §12.3.2 — the manifest said where the endpoints are but
// not what the publisher emits, so an agent had to subscribe to a wildcard
// and discover the event types empirically.
describe("manifest event catalog (§12.3.2)", () => {
const manifest = async (eventTypes?: string[]) => {
const server = new EEPServer({
baseUrl: "https://api.example.com",
did: "did:web:example.com",
...(eventTypes ? { eventTypes } : {})
});
const res = await server.getManifestHandler()({
method: "GET",
path: "/.well-known/eep.json",
headers: {}
});
return res.body as { event_types?: Array<{ type: string }> };
};

it("declares the publisher's configured event types", async () => {
const body = await manifest(["com.example.entity.updated", "com.example.trust.changed"]);
expect(body.event_types).toEqual([
{ type: "com.example.entity.updated" },
{ type: "com.example.trust.changed" }
]);
});

it("declares the default event type when none is configured", async () => {
const body = await manifest();
expect(body.event_types).toEqual([{ type: "entity.updated" }]);
});

// The catalog is only useful if it is valid: an event type that does not
// match §8's reverse-DNS form would make the whole manifest fail schema
// validation, which is a worse outcome than having no catalog.
it("emits types matching the §8 reverse-DNS pattern", async () => {
const pattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$/;
const body = await manifest(["com.example.entity.updated", "com.example.agent.task.completed"]);
for (const entry of body.event_types ?? []) {
expect(entry.type).toMatch(pattern);
}
});
});

// SPECIFICATION.md §5.1.2 — webhooks had no catch-up mechanism at all,
// which bites hardest right after §10 pauses a subscription: the endpoint
// was down, it missed the most, and resuming produced a silent hole.
Expand Down
3 changes: 3 additions & 0 deletions packages/@eep-dev/middleware/src/core/eep-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ export class EEPServer {
layer3_ws: `${this.baseUrl.replace(/^http/, "ws")}/eep/pulse`
},
supported_content_types: ["application/json", "text/markdown"],
// §12.3.2 — declare what this publisher emits so an agent does not
// have to subscribe to a wildcard and find out empirically.
event_types: this.eventTypes.map((type) => ({ type })),
gates_url: `${this.baseUrl}/eep/gates`,
services_url: `${this.baseUrl}/eep/services`,
pqc_ready: false,
Expand Down
50 changes: 50 additions & 0 deletions schemas/v0.1/eep-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,56 @@
]
}
},
"event_types": {
"type": "array",
"description": "Event types this publisher emits, with the payload contract for each. Without this an agent can discover THAT a publisher speaks EEP and WHERE its endpoints are, but not WHAT it will send \u2014 it must read human documentation or subscribe to a wildcard and find out empirically. Publishers with large or frequently-changing catalogs SHOULD use `event_catalog_url` instead.",
"maxItems": 500,
"items": {
"type": "object",
"required": [
"type"
],
"additionalProperties": false,
"properties": {
"type": {
"type": "string",
"description": "The event type, in the reverse-DNS form of SPECIFICATION.md \u00a78.",
"pattern": "^[a-z][a-z0-9]*(\\.[a-z][a-z0-9_]*)+$",
"examples": [
"com.example.entity.updated"
]
},
"description": {
"type": "string",
"description": "Human-readable summary of when this event is emitted.",
"maxLength": 512
},
"dataschema": {
"type": "string",
"description": "Absolute URI of a schema describing this event type's `data`. Publishers SHOULD set the matching `dataschema` attribute on the events themselves (\u00a77.1) so the contract also travels with each event.",
"format": "uri"
},
"since": {
"type": "string",
"description": "EEP or publisher version in which this event type first appeared.",
"maxLength": 32
},
"deprecated": {
"type": "boolean",
"description": "Whether this event type is deprecated. Publishers SHOULD keep emitting a deprecated type for a documented period rather than removing it silently.",
"default": false
}
}
}
},
"event_catalog_url": {
"type": "string",
"description": "Absolute https URL of a paginated event catalog, for publishers whose event list is too large or too volatile to inline. Follows the same pagination shape as `capabilities_query_url` (\u00a712.4). When both are present, `event_types` is a subset and the URL is authoritative.",
"format": "uri",
"examples": [
"https://api.example.com/eep/event-types"
]
},
"gates_url": {
"type": "string",
"format": "uri",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"valid": false,
"reason": "event_types[].type must match the reverse-DNS pattern of SPECIFICATION.md \u00a78"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"did": "did:web:test.eep.dev:o:acme",
"eep_version": "0.1",
"eep_versions": [
"0.1"
],
"preferred_version": "0.1",
"layers": {
"layer1": "https://test.eep.dev/eep",
"layer2_sse": "https://test.eep.dev/eep/stream",
"layer2_webhook": "https://test.eep.dev/eep/subscribe",
"layer3_ws": "wss://test.eep.dev/eep/pulse"
},
"supported_content_types": [
"application/json",
"text/markdown"
],
"pqc_ready": false,
"x402_enabled": false,
"event_types": [
{
"type": "Com.Example.Entity.Updated"
}
]
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"did": "did:web:test.eep.dev:o:acme",
"eep_version": "0.1",
"eep_versions": ["0.1"],
"eep_versions": [
"0.1"
],
"preferred_version": "0.1",
"layers": {
"layer1": "https://test.eep.dev/eep",
Expand All @@ -14,5 +16,17 @@
"text/markdown"
],
"pqc_ready": false,
"x402_enabled": false
"x402_enabled": false,
"event_types": [
{
"type": "com.example.entity.updated",
"description": "A field on the entity profile changed.",
"dataschema": "https://example.com/schemas/entity.updated/v2.json",
"since": "0.1"
},
{
"type": "com.example.trust.changed",
"deprecated": true
}
]
}
11 changes: 11 additions & 0 deletions tests/conformance-fixtures/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,17 @@
"expected": "discovery/crosswalk-host/expected.json",
"shape": "bundle",
"asserts_valid": true
},
{
"id": "discovery-event-catalog-bad-type",
"category": "discovery",
"tier": "Standard",
"spec_section": "\u00a712.3.2 Event catalog",
"schema": "schemas/v0.1/eep-manifest.json",
"input": "discovery/event-catalog-bad-type.input.json",
"expected": "discovery/event-catalog-bad-type.expected.json",
"shape": "json-pair",
"asserts_valid": false
}
]
}
31 changes: 31 additions & 0 deletions tests/types/eep-schemas.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -898,6 +898,37 @@ export interface EEPManifest {
* @minItems 1
*/
supported_content_types: [string, ...string[]];
/**
* Event types this publisher emits, with the payload contract for each. Without this an agent can discover THAT a publisher speaks EEP and WHERE its endpoints are, but not WHAT it will send — it must read human documentation or subscribe to a wildcard and find out empirically. Publishers with large or frequently-changing catalogs SHOULD use `event_catalog_url` instead.
*
* @maxItems 500
*/
event_types?: {
/**
* The event type, in the reverse-DNS form of SPECIFICATION.md §8.
*/
type: string;
/**
* Human-readable summary of when this event is emitted.
*/
description?: string;
/**
* Absolute URI of a schema describing this event type's `data`. Publishers SHOULD set the matching `dataschema` attribute on the events themselves (§7.1) so the contract also travels with each event.
*/
dataschema?: string;
/**
* EEP or publisher version in which this event type first appeared.
*/
since?: string;
/**
* Whether this event type is deprecated. Publishers SHOULD keep emitting a deprecated type for a documented period rather than removing it silently.
*/
deprecated?: boolean;
}[];
/**
* Absolute https URL of a paginated event catalog, for publishers whose event list is too large or too volatile to inline. Follows the same pagination shape as `capabilities_query_url` (§12.4). When both are present, `event_types` is a subset and the URL is authoritative.
*/
event_catalog_url?: string;
/**
* Gate configuration endpoint URL
*/
Expand Down