Skip to content

Commit 6b73114

Browse files
committed
spec: review pass-1 + pass-2 fixes (14 findings closed)
5-agent review of today's spec ship (manifest schema, error catalog port, conformance YAMLs, §8.6 provider self-service) produced findings across two review passes. Pass 1 (10 findings) + pass 2 (4 follow-up regressions) all closed in this commit + the immediate predecessors. This commit handles pass 3 — the documentation gaps the pass-2 review surfaced. ## Pass 3 fixes (this commit) ### NEW-EC-1: §8.6 referenced 3 undefined error codes + 1 wrong code `spec/04-manifest.md §8.6` cited `DNS_VERIFICATION_FAILED`, `ROTATION_24H_CAP`, `ROTATION_RACE` as error codes — none existed in the canonical error spec. Also cited `AGENT_NOT_FOUND` where the correct Provider-side code is `PROVIDER_NOT_FOUND`. Resolution: added the 3 missing codes to `spec/03-errors.md` as a new §3.9 "Provider Self-Service Endpoints (Stage 3)". Each carries HTTP status, retry-safety, `details` shape, and recovery prose consistent with the rest of the catalog. §8.6 now deep-links to the canonical §3.9 anchors instead of dangling at `COVERAGE-PLAN.md`. `AGENT_NOT_FOUND` replaced with `PROVIDER_NOT_FOUND` throughout §8.6. ### NEW-C-1: conformance YAML cited non-existent error codes `conformance/v1.0/JECP-PROVIDER-MUST-AUTH-REQUIRED.yaml` description block listed `INVALID_PROVIDER_KEY`, `MISSING_AUTH`, `UNAUTHORIZED` as example auth-failure codes. None are in `spec/03-errors.md`. Replaced with canonical `AUTH_REQUIRED` and `INVALID_API_KEY`. ### NEW-X-1: internal project nomenclature in public spec `§8.6.2` and `§8.6.3` carried JobDoneBot-internal references like "S1 / TIER A.1 fix", "Sprint 6 TIER A.4 fix", and the Rust function name `rotate_provider_api_key_atomic`. Third-party implementers reading the spec can't look these up. Stripped them; the MUST/SHOULD prose stands on its own. ### NEW-X-2: stale "JSON Schema below" comment `spec/04-manifest.md` line 51 still had the YAML comment "JSON Schema below" — the inline JSON Schema was replaced with a pointer in pass 1's S-4 fix. Updated the comment to point at the canonical schema file. ## Coverage plan updated `spec/error-catalog/COVERAGE-PLAN.md` now lists the 3 new codes (DNS_VERIFICATION_FAILED, ROTATION_24H_CAP, ROTATION_RACE) plus PROVIDER_NOT_FOUND for v1.0.3 catalog backfill. The catalog pages themselves are deferred to v1.0.3 per the existing coverage plan. ## Out of scope (intentional) `conformance/v1.0/JECP-PROVIDER-MUST-AUTH-REQUIRED.yaml` notes block (line 68) still mentions "Sprint 6 review" — non-normative author commentary, not a public-facing claim. Will sweep in a future commit if the broader policy is "no internal nomenclature in any spec-repo file." ## Validation ``` YAML: 44 OK / 0 BAD schema: valid against Draft 2020-12 metaschema fixture: validates against canonical manifest schema ``` All 44 conformance YAML files parse, the JSON Schema validates as a Draft 2020-12 document, and the minimal-valid fixture validates against the canonical schema. No regressions from the prior pass-1 fixes.
1 parent e5c018b commit 6b73114

12 files changed

Lines changed: 354 additions & 181 deletions

conformance/v1.0/INDEX.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ id: JECP-<AREA>-<LEVEL>-<NUMBER> # e.g., JECP-WIRE-MUST-415
1111
level: MUST | SHOULD | MAY # RFC 2119 keyword
1212
spec_section: "<doc>.md §<n>" # source citation
1313
description: "<one-sentence assertion>"
14+
preconditions: # OPTIONAL (v1.0.3+)
15+
env_required: # list of required env vars
16+
- name: JECP_TEST_PROVIDER_KEY
17+
description: "<why this env var is needed>"
1418
request:
1519
method: GET | POST | ...
1620
path: /v1/<endpoint>
@@ -23,6 +27,12 @@ expected:
2327
- { path: "$.error.code", equals: "<code>" }
2428
```
2529
30+
### `preconditions` semantics (added v1.0.3)
31+
32+
When an assertion declares `preconditions.env_required`, the runner MUST check that every named env var is defined and non-empty BEFORE executing the request. If any required env var is missing, the runner reports the assertion as **SKIPPED** (distinct from PASSED and FAILED) in both the JUnit XML output (`<skipped/>` child element) and the Markdown report. A SKIPPED assertion does NOT contribute to the conformance pass-rate calculation.
33+
34+
This addition lets us tighten assertions whose validation path is gated by external state (e.g., a valid Provider key) without falling back to permissive `status_in` ranges that pass for the wrong reason.
35+
2636
The runner is `scripts/jecp-conformance.sh` (Phase 0 / c10) — a bash + python3 harness that walks this directory, executes each assertion against `${TARGET}` (default `https://jecp.dev`), and emits JUnit XML + Markdown report. The same logic ships as `npx @jecpdev/conformance` and `docker run ghcr.io/jecpdev/conformance:v1.x` in Phase 2.
2737

2838
## Status
@@ -49,7 +59,7 @@ NUMBER = zero-padded sequence within (AREA, LEVEL), e.g., 001, 002
4959
5060
Examples (for v1.0.2):
5161
- `JECP-WIRE-MUST-415` — Content-Type ≠ application/json → 415
52-
- `JECP-WIRE-MUST-409-DUP-SAME` — same id + same input replay → 409
62+
- `JECP-WIRE-MUST-409-DUP-IDEMPOTENT-NO-CONFLICT` — same id + same input replay → 409 idempotent no-conflict
5363
- `JECP-WIRE-MUST-409-DUP-DIFFERENT-INPUT` — same id + different input → 400 INVALID_REQUEST (negative case)
5464
- `JECP-WIRE-MUST-410-SUNSET` — sunset capability → 410 + Sunset header
5565
- `JECP-WIRE-MUST-429-RETRY-AFTER` — burst → 429 + Retry-After ∈ [1, 600]

conformance/v1.0/JECP-PROVIDER-MUST-AUTH-REQUIRED.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
id: JECP-PROVIDER-MUST-AUTH-REQUIRED
22
level: MUST
3-
spec_section: "Provider lifecycle prose: Bearer `<provider_api_key>` REQUIRED on /v1/providers/{me, verify-dns, me/rotate-key}, /v1/manifests"
3+
spec_section: "spec/04-manifest.md §8.6 Provider Self-Service Endpoints + §8.4 Manifest Submission — Bearer `<provider_api_key>` REQUIRED on /v1/providers/{me, verify-dns, me/rotate-key} and POST /v1/manifests"
44
description: >
55
Conformant Hubs MUST reject Provider-side admin requests that do not
66
carry a valid `Authorization: Bearer <provider_api_key>` header. The
77
response MUST be HTTP 401 with `error.code` indicating a recognizable
8-
auth failure (e.g. `INVALID_PROVIDER_KEY`, `MISSING_AUTH`, or
9-
`UNAUTHORIZED`). Hubs that respond 200 or 4xx-without-auth-context on
10-
these endpoints would expose Provider state to anonymous callers.
8+
auth failure (e.g. `AUTH_REQUIRED` or `INVALID_API_KEY`, per
9+
spec/03-errors.md §3.1). Hubs that respond 200 or 4xx-without-auth-context
10+
on these endpoints would expose Provider state to anonymous callers.
1111
1212
This assertion is run against ALL of the following request shapes; a
1313
Hub passes only if every one of them returns 401 (or 403). The runner

conformance/v1.0/JECP-PROVIDER-MUST-MANIFEST-VALIDATES-SCHEMA.yaml

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
id: JECP-PROVIDER-MUST-MANIFEST-VALIDATES-SCHEMA
22
level: MUST
3-
spec_section: "04-manifest.md §5 + schemas/v1/manifest.schema.json"
3+
spec_section: "04-manifest.md §5 + §6.1 + schemas/v1/manifest.schema.json"
44
description: >
55
Conformant Hubs MUST validate POST /v1/manifests bodies against the
66
canonical manifest schema at https://jecp.dev/schemas/v1/manifest.schema.json
@@ -11,17 +11,27 @@ description: >
1111
no actions, no endpoint) and break invocation routing.
1212
1313
This assertion sends a YAML manifest with `actions: []` (empty array
14-
violates the minItems: 1 constraint) and asserts the Hub rejects it.
14+
violates the minItems: 1 constraint) and asserts the Hub rejects it
15+
with HTTP 400. The assertion is meaningless without a valid Provider
16+
key — preconditions REQUIRE one — because without auth the Hub
17+
short-circuits to 401 before the schema validator runs and we cannot
18+
distinguish a Hub that validates schemas from one that does not.
19+
20+
preconditions:
21+
env_required:
22+
- name: JECP_TEST_PROVIDER_KEY
23+
description: >
24+
Bearer token for a Provider with verified DNS + Stripe Connect
25+
onboarding. Required to reach the schema validation path; without
26+
it the Hub returns 401 before the validator runs and the assertion
27+
cannot prove conformance. Runners that do not have this env var
28+
configured MUST report this assertion as SKIPPED, not PASSED.
1529
1630
request:
1731
method: POST
1832
path: /v1/manifests
1933
headers:
2034
Content-Type: application/x-yaml
21-
# Test uses a syntactically valid Bearer; auth check is NOT what we're
22-
# asserting here. If auth fails first (401) that's fine — re-run
23-
# against a registered Provider with a real key when authoring this
24-
# in CI. The skeleton below documents what the body should look like.
2535
Authorization: "Bearer ${JECP_TEST_PROVIDER_KEY}"
2636
body: |
2737
namespace: test-validation
@@ -33,20 +43,17 @@ request:
3343
actions: []
3444
3545
expected:
36-
# We accept 400 (validation failed) or 401 (auth failed first — the
37-
# runner can substitute a real key in CI; the assertion shape is
38-
# what matters for spec conformance).
39-
status_in: [400, 401]
46+
status_in: [400]
4047
body_jsonpath:
41-
- { path: "$.error.code", regex: "^(PARSE_ERROR|INVALID_MANIFEST|VALIDATION_FAILED|INVALID_API_KEY|UNAUTHORIZED)$" }
48+
- { path: "$.error.code", regex: "^(PARSE_ERROR|INVALID_MANIFEST|VALIDATION_FAILED)$" }
4249

4350
notes: |
44-
When the runner has `JECP_TEST_PROVIDER_KEY` available (registered
45-
Provider with verified DNS + Stripe), this assertion exercises the
46-
schema validation path. Without that key the Hub fails auth first
47-
and we accept that outcome as a non-violation — the conformance
48-
intent is "this manifest never gets persisted", which 401 also
49-
satisfies.
51+
Tightened in v1.0.3 (per spec-side review fix #4) to drop the previous
52+
401 soft-pass. The earlier behaviour accepted "auth failed first" as a
53+
conformance pass, which masked Hubs that never wired up schema
54+
validation at all. The new contract: SKIP the assertion when
55+
JECP_TEST_PROVIDER_KEY is absent; PASS only on a true 400+validation
56+
rejection.
5057
5158
Future expansion: a multi-variant version of this test would cover
5259
each constraint in manifest.schema.json (missing namespace, invalid

conformance/v1.1/INDEX.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Each assertion is a single YAML file at `conformance/v1.1/<id>.yaml` with a self
88

99
Fixture references inside YAMLs:
1010
- `${fixtures.x402.<name>}` — placeholder consumed by the conformance harness, resolved from the JSON files in this directory.
11-
- `conformance/v1.1/fixtures/<file>` — direct relative path (used only by the rc3 keeper assertions for `idempotency-pull-event.json` and `facilitator-fleet-eoas.txt`). These fixture files are not yet committed; they will land alongside the keeper-rc3 fixture batch.
11+
- `conformance/v1.1/fixtures/<file>` — direct relative path (used by the rc3 keeper assertions for `idempotency-pull-event.json` and `facilitator-fleet-eoas.txt`). Both fixtures are committed as PLACEHOLDER (2026-05-16) — `idempotency-pull-event.json` uses a deterministic `tx_hash` and a `${TEST_AGENT_EOA}` substitution placeholder for `payer`; `facilitator-fleet-eoas.txt` lists known operators behind `#`-prefixed comment lines pending BaseScan verification (see `feedback_verify_facilitator_on_chain.md` in the JobDoneBot release-prep notes). The harness MAY run the YAMLs that reference them, but the keeper batch is only declared "runnable for GA gate" once the comment-only EOA lines are uncommented after on-chain verification.
1212

1313
## Assertion list
1414

@@ -63,10 +63,10 @@ JSON fixtures resolved via `${fixtures.x402.<name>}`:
6363
- `x402-happy-path-pure.json`, `x402-happy-path-wallet-fallback.json`
6464
- `x402-error-facilitator-unreachable.json`, `x402-error-not-accepted.json`, `x402-error-payment-invalid.json`, `x402-error-settlement-reused.json`, `x402-error-settlement-timeout.json`
6565

66-
Fixture files referenced by path (rc3 keeper batch — to be committed alongside fixture rollout):
66+
Fixture files referenced by path (rc3 keeper batch — committed 2026-05-16, PLACEHOLDER pending verification):
6767

68-
- `conformance/v1.1/fixtures/idempotency-pull-event.json` — deterministic pull-event fixture for replay test
69-
- `conformance/v1.1/fixtures/facilitator-fleet-eoas.txt` — observed facilitator EOA list (Coinbase x402 Facilitator 1-8, Canza, Daydreams, X402rs, plus newly-observed entries)
68+
- `conformance/v1.1/fixtures/idempotency-pull-event.json` — deterministic pull-event fixture for replay test (PLACEHOLDER `payer` uses `${TEST_AGENT_EOA}` substitution)
69+
- `conformance/v1.1/fixtures/facilitator-fleet-eoas.txt` — observed facilitator EOA list (Coinbase x402 Facilitator 1-8, Canza, Daydreams, X402rs, plus newly-observed entries). All entries currently `#`-commented; uncomment after BaseScan verification per `feedback_verify_facilitator_on_chain.md`.
7070

7171
## Coverage policy
7272

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# x402.org facilitator EOA fleet (auto-updated from BaseScan)
2+
#
3+
# Last updated: 2026-05-16 (PLACEHOLDER pending BaseScan verification —
4+
# the entries below are non-authoritative until they are re-pulled from
5+
# the canonical on-chain source. See feedback_verify_facilitator_on_chain.md
6+
# in the JobDoneBot release-prep set for the verification procedure.)
7+
#
8+
# Format: one EOA address per line (lowercase, 0x-prefixed, 40 hex chars).
9+
# Comment lines start with `#` and are ignored by the harness.
10+
# Blank lines are also ignored.
11+
#
12+
# Provenance: each entry MUST be reconfirmed against on-chain activity
13+
# attributable to the named operator before being relied on for
14+
# X402_FACILITATOR_NOT_TRUSTED_AS_SETTLER (rc3 keeper batch). The list is
15+
# intentionally permissive at the conformance layer — the assertion checks
16+
# that NONE of the listed EOAs can call recordSettlement on the Splitter
17+
# contract, so an over-inclusive list is safer than an under-inclusive one.
18+
#
19+
# Known operators (PLACEHOLDER — DO NOT trust until verified):
20+
#
21+
# Coinbase x402 Facilitator 1
22+
# 0x0000000000000000000000000000000000000001
23+
#
24+
# Coinbase x402 Facilitator 2
25+
# 0x0000000000000000000000000000000000000002
26+
#
27+
# Coinbase x402 Facilitator 3-8
28+
# 0x0000000000000000000000000000000000000003
29+
# 0x0000000000000000000000000000000000000004
30+
# 0x0000000000000000000000000000000000000005
31+
# 0x0000000000000000000000000000000000000006
32+
# 0x0000000000000000000000000000000000000007
33+
# 0x0000000000000000000000000000000000000008
34+
#
35+
# Canza
36+
# 0x0000000000000000000000000000000000000009
37+
#
38+
# Daydreams
39+
# 0x000000000000000000000000000000000000000a
40+
#
41+
# X402rs
42+
# 0x000000000000000000000000000000000000000b
43+
#
44+
# Newly observed (placeholder slot — populate from BaseScan)
45+
# 0x000000000000000000000000000000000000000c
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"_comment_status": "PLACEHOLDER fixture. The chain_id targets Base Sepolia (84532) for safety. The tx_hash + payer + amount values are deterministic stand-ins so the conformance harness can produce reproducible runs. Replace `payer` with the actual test agent's EOA at runtime, OR rely on the harness to fill it from `${TEST_AGENT_EOA}`. Authored 2026-05-16 for the rc3 keeper batch (ADR-0003 Am-7).",
4+
"_comment_usage": "Referenced by conformance/v1.1/X402_RECORD_SETTLEMENT_IDEMPOTENT.yaml via the `x402_pull_fixture: idempotency-pull-event` directive. The harness loads this file, substitutes ${TEST_AGENT_EOA} into `payer`, and submits the resulting event TWICE to assert the keeper rejects the duplicate on (chain_id, tx_hash, log_index).",
5+
6+
"chain_id": 84532,
7+
"tx_hash": "0xidempotency_fixture_pull_event_v1",
8+
"log_index": 0,
9+
"payer": "${TEST_AGENT_EOA}",
10+
"amount_usdc_micro": 5000
11+
}

schemas/v1/manifest.schema.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@
225225
},
226226
"action": {
227227
"type": "string",
228-
"pattern": "^[a-z][a-z0-9-]*$",
229-
"description": "Action id within the target capability."
228+
"pattern": "^[a-z][a-z0-9-]{2,63}$",
229+
"description": "Action id within the target capability. MUST match the Action.id pattern (3-64 chars, [a-z][a-z0-9-])."
230230
},
231231
"input": {
232232
"type": "object",
@@ -279,12 +279,13 @@
279279

280280
"Authentication": {
281281
"type": "object",
282+
"description": "Auth scheme for inbound Hub-forwarded calls. v1.0 supports `api_key` only (HMAC-signed header). v1.1+ will add `mtls` and `oauth2` with per-type subschemas.",
282283
"required": ["type"],
283284
"additionalProperties": false,
284285
"properties": {
285286
"type": {
286-
"enum": ["api_key", "mtls", "oauth2"],
287-
"description": "Auth scheme the Provider expects on inbound Hub-forwarded calls. v1.0 reference Hub implements only api_key (HMAC-signed header)."
287+
"enum": ["api_key"],
288+
"description": "Auth scheme the Provider expects on inbound Hub-forwarded calls. v1.0 supports `api_key` only. `mtls` and `oauth2` are reserved for v1.1+ where per-type subschemas will land."
288289
},
289290
"header_name": {
290291
"type": "string",
@@ -379,6 +380,15 @@
379380
"pattern": "^\\d+\\.\\d+\\.\\d+$",
380381
"description": "Semver of the replacement version. Surfaced as details.successor in CAPABILITY_DEPRECATED responses."
381382
}
383+
},
384+
"if": {
385+
"properties": {
386+
"status": { "enum": ["deprecated", "sunset"] }
387+
},
388+
"required": ["status"]
389+
},
390+
"then": {
391+
"required": ["sunset_at"]
382392
}
383393
}
384394
}

spec/03-errors.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,35 @@ The `details.documentation_url` field, when present, is a deep-link of the form
498498

499499
A Hub MUST NOT emit `details.subcause` until after it has authenticated the agent (via `X-API-Key` or equivalent). This prevents the subcause registry from acting as an enumeration oracle for unauthenticated callers — same rule as `PROVENANCE_MISMATCH` (§3.1).
500500

501+
### 3.9 Provider Self-Service Endpoints (Stage 3)
502+
503+
These error codes fire only on the Provider-admin endpoints defined in 04-manifest.md §8.6 (`POST /v1/providers/verify-dns`, `POST /v1/providers/me/rotate-key`). They are never emitted on agent-facing wire calls (`POST /v1/invoke`, etc.). Hubs that do not yet implement Stage 3 (third-party Provider acceptance) never emit these codes.
504+
505+
#### `DNS_VERIFICATION_FAILED`
506+
507+
- **HTTP**: 422
508+
- **Cause**: The Provider invoked `POST /v1/providers/verify-dns` (04-manifest.md §8.6.2) but the Hub could not find a matching `_jecp.<domain>` TXT record carrying `jecp-verify=<token>` against the domain extracted from `provider.endpoint_url`. Either the record is absent, the token mismatches, or DNS propagation has not completed.
509+
- **Retry-safe**: Yes (after the Provider publishes / corrects the TXT record and DNS propagates)
510+
- **`details`**: `{ "domain": "<host>", "expected_token_prefix": "<first 8 chars>", "reason": "txt_record_missing" | "txt_record_mismatch" | "nxdomain" }`
511+
- **Recovery**: Publish the TXT record per 04-manifest.md §8.2, wait for propagation (typically < 5 minutes for low-TTL zones), then re-call `POST /v1/providers/verify-dns`.
512+
513+
#### `ROTATION_24H_CAP`
514+
515+
- **HTTP**: 429
516+
- **Cause**: The Provider invoked `POST /v1/providers/me/rotate-key` (04-manifest.md §8.6.3) but has already rotated the maximum number of times allowed in a sliding 24-hour window (default 3). The request has NO effect on the existing key.
517+
- **Retry-safe**: Yes (after the oldest rotation in the window ages out)
518+
- **Headers (SHOULD)**: `Retry-After: <integer-seconds>` indicating when the next rotation slot opens.
519+
- **`details`**: `{ "limit_per_24h": <int>, "rotations_in_last_24h": <int>, "next_slot_at": "<RFC 3339>" }`
520+
- **Recovery**: Wait until `next_slot_at`. Operators MAY adjust the cap via Hub configuration; agents/Providers cannot.
521+
522+
#### `ROTATION_RACE`
523+
524+
- **HTTP**: 409
525+
- **Cause**: The Provider invoked `POST /v1/providers/me/rotate-key` but the Provider record was modified or deleted by a concurrent administrative action mid-transaction (e.g., Provider deleted, or another rotation racing with row-level lock contention). No new key is issued and the existing key is unchanged.
526+
- **Retry-safe**: Yes (re-issue the call once the contending operation completes)
527+
- **`details`**: `{ "reason": "row_locked" | "provider_disappeared" }`
528+
- **Recovery**: Retry the call after a brief backoff (100-500 ms). If `provider_disappeared`, the Provider record has been removed and the Provider must re-register.
529+
501530
## 4. `next_action` Object
502531

503532
Errors that have a clear recovery path SHOULD include a `next_action` object with machine-readable guidance.

0 commit comments

Comments
 (0)