From 2bf79f3f6ab4d5baf5820fef09e2b11ddbf1e802 Mon Sep 17 00:00:00 2001 From: Ugur Cekmez Date: Wed, 26 Aug 2026 21:49:39 +0300 Subject: [PATCH] fix(standards): make the IETF draft describe the protocol that ships `docs/standards/draft-eep-protocol-core-00.md` and `schemas/v0.1/eep-manifest.json` had drifted until not one field name matched except `eep_version`: draft requires publisher_did, endpoints.discovery, endpoints.subscribe, endpoints.stream, supported_layers, delivery_methods, conformance_level schema requires did, eep_version, layers, supported_content_types, pqc_ready, x402_enabled A publisher conformant to the shipped schema fails every MUST in the Internet-Draft, and vice versa. Nothing in CI noticed, because nothing compared them. ROADMAP.md targets IETF/W3C submission at v0.2, so this is the document reviewers would read first. Direction: the draft is rewritten to match the schema, not the reverse. The schema is implemented by both middleware packages, both reference implementations and the compliance CLI; the draft has no implementations at all. Changing the schema instead would be a breaking change to a published v0.1 artifact for no protocol benefit. Changes: - Layer 1 field table restated against `eep-manifest.json`, with the `layers` object documented and the rule that at least one Layer 2 endpoint must be populated. - `endpoints.stream` / `endpoints.subscribe` references replaced with `layers.layer2_sse` / `layers.layer2_webhook`. - Envelope section documents `eep_version` (what implementations emit) instead of `eepversion` / `eepdelivery` (which nothing emits). - Editor's note recording that the shipped `eep_`-prefixed names violate the CloudEvents v1.0.2 attribute-naming rule, which excludes the underscore. This is a real problem that becomes load-bearing in binary content mode, where attributes become `ce-`-prefixed headers. Recorded as an open question rather than silently renamed: no implementation emits the compliant spelling today. - `scripts/check-draft-schema-parity.mjs` plus a CI job, so this class of drift is a build failure. The gate parses the draft's delimited field table and diffs names and required-ness against the schema; verified by injecting drift and confirming a non-zero exit. No schema changed, so the types drift gate is a no-op here. Refs: EEP audit 2026-08 finding A1 Signed-off-by: Ugur Cekmez --- .github/workflows/test.yml | 14 +++ docs/standards/draft-eep-protocol-core-00.md | 75 +++++++++++--- scripts/check-draft-schema-parity.mjs | 102 +++++++++++++++++++ 3 files changed, 175 insertions(+), 16 deletions(-) create mode 100644 scripts/check-draft-schema-parity.mjs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e851e8d..687ec9d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -439,6 +439,20 @@ jobs: # ──────────────────────────────────────────────────────────────────── # Schema → types drift gate # ──────────────────────────────────────────────────────────────────── + # The Internet-Draft is the document this project intends to submit for + # standardisation. It described a different manifest than the one that + # ships — different field names, different required set — and nothing + # compared them. This gate makes that class of drift a build failure. + draft-schema-parity: + name: IETF draft ↔ schema parity + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 # renovate: pin + - uses: actions/setup-node@v7 # renovate: pin + with: + node-version: '22' + - run: node scripts/check-draft-schema-parity.mjs + schema-types-drift: name: Schema types drift gate runs-on: ubuntu-latest diff --git a/docs/standards/draft-eep-protocol-core-00.md b/docs/standards/draft-eep-protocol-core-00.md index 07d1849..959114c 100644 --- a/docs/standards/draft-eep-protocol-core-00.md +++ b/docs/standards/draft-eep-protocol-core-00.md @@ -133,35 +133,59 @@ Pulse channel: A conforming publisher MUST expose a discovery document at `/.well-known/eep.json` per {{RFC9110}}. The document MUST be a JSON -object that includes: +object conforming to the `eep-manifest.json` schema in {{EEP-SPEC}}. +The fields relevant to the Core tier are: + | Field | Type | Required | Description | |---|---|---|---| +| `did` | string | yes | An absolute DID URI per {{W3C.DID}} identifying the publisher. | | `eep_version` | string | yes | The EEP version supported, e.g. `"0.1"`. | -| `publisher_did` | string | yes | An absolute DID URI per {{W3C.DID}}. | -| `endpoints.discovery` | string (URI) | yes | Absolute https URL of this document. | -| `endpoints.subscribe` | string (URI) | yes | Absolute https URL for subscribe / unsubscribe. | -| `endpoints.stream` | string (URI) | yes | Absolute https URL for SSE stream. | -| `endpoints.pulse` | string (URI) | no | Absolute wss URL for pulse channel, when supported. | -| `supported_layers` | array | yes | Subset of `{state_resolution, signal_stream, network_pulse}`. | -| `delivery_methods` | array | yes | Subset of `{sse, webhook}`. | -| `conformance_level` | string | yes | One of `Core`, `Standard`, `Full`. | +| `layers` | object | yes | Endpoint URLs per layer; see below. | +| `supported_content_types` | array | yes | Media types the Layer 1 entity endpoint can serve, e.g. `["application/json", "text/markdown"]`. | +| `pqc_ready` | boolean | yes | Whether the publisher can verify post-quantum signature algorithms. | +| `x402_enabled` | boolean | yes | Whether the publisher supports HTTP 402 payment gating. | +| `gates_url` | string (URI) | no | Absolute https URL of the gate configuration document. | +| `services_url` | string (URI) | no | Absolute https URL of the service catalog. | +| `updated_at` | string | no | RFC 3339 timestamp of the last manifest change. | + + +The `layers` object carries the endpoint URLs: + +| Member | Type | Required | Description | +|---|---|---|---| +| `layer1` | string (URI) | yes | Absolute https URL of the Layer 1 entity resolution endpoint. | +| `layer2_sse` | string (URI) | no | Absolute https URL of the SSE stream. | +| `layer2_webhook` | string (URI) | no | Absolute https URL for subscription creation. | +| `layer3_ws` | string (URI) | no | Absolute wss URL of the pulse channel, when supported. | + +A publisher MUST populate at least one of `layers.layer2_sse` or +`layers.layer2_webhook`, because {#layer-2} is the only mandatory +transport. Subscribers MUST be able to fetch this document with a single HTTPS GET. Publishers MUST serve it over TLS. +The full manifest surface — including `signing_algorithms`, +`conformance_credential`, `reputation`, `data_residency` and the +discovery hints — is specified in {{EEP-SPEC}} and constrained by +`eep-manifest.json`. This document restates only what a Core-tier +implementation must produce; it does not redefine the schema, and +`scripts/check-draft-schema-parity.mjs` in the EEP repository fails the +build if the table above drifts from it. + # Layer 2: Signal Stream {#layer-2} The signal stream is the only mandatory transport. A Core-conformant publisher MUST implement at least one of: -- Server-Sent Events ({{W3C.SSE}}) at `endpoints.stream`, OR +- Server-Sent Events ({{W3C.SSE}}) at `layers.layer2_sse`, OR - Outbound HTTPS Webhooks signed per {#signing}. ## Subscription A subscriber MAY register for events by POSTing to -`endpoints.subscribe` a JSON body conforming to the +`layers.layer2_webhook` a JSON body conforming to the `subscription.request.json` schema in {{EEP-SPEC}}. Required fields are `source_did`, `event_types`, and `delivery_method`; webhook subscriptions also require `delivery_url`. @@ -173,13 +197,32 @@ resolves to a private, link-local, loopback, or cloud-metadata address ## Event envelope {#envelope} -EEP events use the CloudEvents v1.0.2 envelope {{CLOUDEVENTS}}. In -addition to the core CloudEvents fields, EEP defines two extension -attributes: +EEP events use the CloudEvents v1.0.2 envelope {{CLOUDEVENTS}}, +constrained by the `event.envelope.json` schema in {{EEP-SPEC}}. The +core attributes `specversion`, `id`, `source`, `type`, `time` and +`datacontenttype` are all REQUIRED. -- `eepversion` (string, REQUIRED): the spec version the publisher used, +In addition, EEP defines the following extension attribute for the Core +tier: + +- `eep_version` (string, REQUIRED): the spec version the publisher used, matching the `EEP-Version` response header. -- `eepdelivery` (string, OPTIONAL): one of `webhook`, `sse`, `pulse`. + +Further `eep_`-prefixed extension attributes (`eep_subscription_id`, +`eep_trust_score`, `eep_actor_type`, `eep_tier` and others) are defined +in {{EEP-SPEC}} for the Standard and Full tiers. + +> **Editor's note (to be resolved before submission).** CloudEvents +> v1.0.2 restricts context attribute names to lowercase ASCII letters +> and digits, which excludes the underscore. The attribute names above +> are the ones shipped and deployed today, but they do not satisfy that +> rule, and the divergence becomes load-bearing in CloudEvents *binary* +> content mode, where attributes are carried as `ce-`-prefixed HTTP +> headers. Options are (a) rename to `eepversion` and friends with a +> deprecation window, or (b) carry the underscore names only in +> structured mode and define a binary-mode mapping. This draft +> deliberately documents what exists rather than a name no +> implementation emits. EEP event types MUST follow reverse-DNS naming, e.g. `com.example.entity.updated`. Event types MAY use a trailing wildcard diff --git a/scripts/check-draft-schema-parity.mjs b/scripts/check-draft-schema-parity.mjs new file mode 100644 index 0000000..eb7c321 --- /dev/null +++ b/scripts/check-draft-schema-parity.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * Verify that the IETF Internet-Draft's normative field tables agree with + * the shipped JSON Schemas. + * + * Why this exists: `docs/standards/draft-eep-protocol-core-00.md` and + * `schemas/v0.1/eep-manifest.json` had drifted so far apart that not one + * field name matched except `eep_version`. The draft required + * `publisher_did`, `endpoints.*`, `supported_layers`, `delivery_methods` + * and `conformance_level`; the schema requires `did`, `layers`, + * `supported_content_types`, `pqc_ready` and `x402_enabled`. A publisher + * conformant to one failed every MUST in the other, and nothing in CI + * noticed — because nothing compared them. + * + * The draft is the document this project intends to submit for + * standardisation. It has to describe the protocol that actually ships. + * + * Usage: + * node scripts/check-draft-schema-parity.mjs + * + * Exits non-zero, with a readable diff, when the two disagree. + */ +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const DRAFT = resolve(REPO_ROOT, 'docs/standards/draft-eep-protocol-core-00.md'); +const MANIFEST_SCHEMA = resolve(REPO_ROOT, 'schemas/v0.1/eep-manifest.json'); + +/** Marker comments in the draft that delimit the manifest field table. */ +const TABLE_START = ''; +const TABLE_END = ''; + +function fail(lines) { + console.error('\n✗ IETF draft and eep-manifest.json disagree:\n'); + for (const line of lines) console.error(` ${line}`); + console.error( + '\n The draft must describe the protocol that ships. Update\n' + + ' docs/standards/draft-eep-protocol-core-00.md, or change the schema\n' + + ' and regenerate types — but do not let them diverge silently.\n' + ); + process.exit(1); +} + +const draft = readFileSync(DRAFT, 'utf8'); +const startIdx = draft.indexOf(TABLE_START); +const endIdx = draft.indexOf(TABLE_END); +if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { + fail([ + 'Could not locate the delimited manifest field table in the draft.', + `Expected the markers:\n ${TABLE_START}\n ${TABLE_END}`, + ]); +} + +const table = draft.slice(startIdx, endIdx); + +// Rows look like: | `field` | type | yes/no | description | +const draftFields = new Map(); +for (const line of table.split('\n')) { + const m = /^\|\s*`([^`]+)`\s*\|[^|]*\|\s*(yes|no)\s*\|/.exec(line.trim()); + if (m) draftFields.set(m[1], m[2] === 'yes'); +} + +if (draftFields.size === 0) { + fail(['The manifest field table is present but no rows parsed.']); +} + +const schema = JSON.parse(readFileSync(MANIFEST_SCHEMA, 'utf8')); +const schemaProps = new Set(Object.keys(schema.properties ?? {})); +const schemaRequired = new Set(schema.required ?? []); + +const problems = []; + +for (const [field, draftRequired] of draftFields) { + if (!schemaProps.has(field)) { + problems.push(`draft documents \`${field}\`, which eep-manifest.json does not define`); + continue; + } + const schemaSaysRequired = schemaRequired.has(field); + if (draftRequired !== schemaSaysRequired) { + problems.push( + `\`${field}\`: draft says required=${draftRequired}, schema says required=${schemaSaysRequired}` + ); + } +} + +// Every schema-required property must appear in the draft. Optional ones may +// be omitted: the draft scopes itself to the Core tier and deliberately does +// not restate the full manifest surface. +for (const field of schemaRequired) { + if (!draftFields.has(field)) { + problems.push(`eep-manifest.json requires \`${field}\`, which the draft does not document`); + } +} + +if (problems.length > 0) fail(problems); + +console.error( + `✓ IETF draft matches eep-manifest.json ` + + `(${draftFields.size} documented fields, ${schemaRequired.size} required)` +);