Federation is the part of an open protocol that has to be earned. A spec on
GitHub is a claim. A second operator running compatible software against the
reference clients is a fact. Until v0.4.7, Oversight had a registry
federation document with the right vocabulary in it (canonicalization,
sidecar binding, transparency proofs) but no machinery that let anybody
verify the document and the reference implementation were saying the same
thing. v0.4.7 closes that gap. The interop spec at
docs/spec/registry-v1.md
is now hardened against the reference server, and a 32-check conformance
harness at tests/test_registry_conformance.py runs the same
checks against any operator URL.
I want to write down what changed about the spec, what the harness actually proves, and what it does not.
The spec went from a vocabulary to a contract
The old draft listed the endpoints and gestured at canonicalization. The new
version pins them. Every endpoint has a normative request shape, response
shape, and error contract. The endpoint table now includes the things a
federated verifier actually depends on and the original draft glossed over.
/.well-known/oversight-registry declares the registry's identity,
suite support, and transparency-log type. The beacon paths are normative,
not advisory: /p/{token_id}.png for image beacons,
/r/{token_id} for redirects, /v/{token_id} for
VRF-style probes. The /evidence/{file_id} bundle has a fixed
field set so a verifier can fetch the manifest, the issuer-signed sidecars,
the transparency-log inclusion proof, and the registration receipts in one
round trip and check them locally without trusting the operator. The
/tlog/head, /tlog/proof, and /tlog/range
endpoints are explicit so a verifier can pull tree heads and consistency
proofs without operator-specific knowledge.
One endpoint left the spec. The previous draft had a
/query/{file_id} route that nothing in the reference server
ever served. I deleted it. A spec that names endpoints the reference does
not implement is worse than a spec that omits them, because an independent
operator who reads the doc and implements faithfully will fail
interoperability for following the spec. Removing it now is cheaper than
removing it after a second implementation existed.
Canonicalization is a deliberate decision
Two registries agreeing on what a manifest is requires byte-level agreement
on JSON serialization. JSON has no canonical form by default. The spec now
names the exact serializer the reference server uses:
json.dumps(sort_keys=True, separators=(",", ":")) over UTF-8.
Sorted keys, no whitespace, ASCII or escaped Unicode. That choice is
compatible with most language standard libraries and avoids the rabbit hole
of full RFC 8785 JCS, which is more rigorous and more expensive to implement
correctly. The tradeoff is honest in the spec: this canonicalization works
for the manifest fields Oversight signs, and a second operator who implements
the same routine will produce the same bytes. If a future requirement forces
full JCS, that is a v1.x decision with a migration path.
Sidecar binding follows from canonicalization. Every registration ships a
signed manifest plus optional beacons and watermarks
sidecar arrays. The registry compares the canonical bytes of the submitted
sidecars against the canonical bytes of the corresponding fields inside the
signed manifest and rejects anything that does not match. That is what makes
the registry useless as an attack surface for forging beacon ownership: the
sidecar is only ever a search-friendly mirror of what the issuer already
signed, and a divergent sidecar is rejected before it touches the database.
One error envelope so callers can branch on a code
The reference server used to return errors in three or four different shapes
depending on which framework primitive raised them. v0.4.7 picks one shape.
Every error is an object with error (a stable string code),
message (human-readable, not machine-parseable), and an
optional details object for structured context. The code
vocabulary is closed: bad_request, unauthorized,
forbidden, not_found, conflict,
unprocessable, rate_limited,
internal_error. A federated client can branch on
error without parsing prose, and a second operator who returns
the same code for the same condition is wire-compatible without further
coordination.
The harness is in-process by default and live by environment variable
The conformance harness runs in two modes. Without
OVERSIGHT_REGISTRY_URL set, it stands the reference Python
registry up inside a FastAPI TestClient against a fresh SQLite
database in a temp directory, runs every check there, and tears the server
down. That path is the CI gate: every commit that touches the registry runs
the same conformance suite that an external operator would run, which means
the spec and the reference cannot drift without breaking the build.
With OVERSIGHT_REGISTRY_URL=https://registry.example.org, the
harness points an httpx.Client at that URL and runs the same
32 checks against the live deployment. An independent operator who passes
the harness is not claiming compatibility, they are demonstrating it, and
the run log is a compact conformance report any verifier can read.
What the 32 checks actually exercise
The checks cluster into seven groups. The first is identity and liveness:
the /healthz endpoint, the /.well-known document
with its declared suites and transparency-log type, and a HEAD probe that
proves the server speaks JSON over HTTPS at the URL the manifest declared.
The second is registration: a full sealed-manifest round trip that posts a
signed registration with sidecars, gets an evidence pointer back, and
confirms the registry stored what was signed. The third is signed-input
rejection: the harness deliberately ships an unsigned manifest and a
sidecar that does not match the manifest, and the registry must reject
both with the spec's unauthorized and conflict
codes respectively. The fourth is attribution: a token id from the
registration round-trips through /attribute and resolves to
the same recipient and file id, and a token id that does not exist
returns not_found rather than a generic 500.
The fifth group is evidence and transparency: /evidence/{file_id}
returns the bundle with the manifest, sidecars, signed registration receipt,
and inclusion proof, and the proof verifies against the tree head at
/tlog/head. The sixth is the beacon surface: each declared
beacon endpoint resolves with the spec-mandated content type and HTTP
verb, including the image beacon's PNG response and the redirect beacon's
302. The seventh is operational discipline: the DNS event endpoint
rejects callbacks without the shared OVERSIGHT_DNS_EVENT_SECRET
and accepts 503 as a valid fail-closed signal, the read-only endpoints
return CORS headers so the in-browser inspector can read them, and rate
limit and identifier-length limits on the spec edges return the expected
error codes.
What the harness does not prove
Two things. First, conformance is wire-format and contract conformance, not security conformance. A registry that returns the right JSON shape for every check can still be running broken signature verification on the inside. The harness covers what a federated client observes from outside; an operator running the conformance suite still owes their users the threat model the protocol asks for, and the public threat model at research/threat-model.html is unchanged.
Second, conformance is a snapshot. The harness pins the v1 surface in the repository at the moment of release. Operators who fork the spec without re-running the harness against a current reference can drift. The intended cadence is: harness ships in the same commit as any normative spec change, and a deployment claiming v1.x conformance is claiming the harness from that release passed against it.
Why this is the acceptance gate for v1.0
Federation is on the v1.0 roadmap because Oversight's value collapses if the only registry is mine. A protocol that requires a single hostname is a product, not infrastructure. The conformance harness is the gate I wanted before any second operator was on the line: a way to say "compatible with" without ambiguity, with a reproducible artifact behind the claim. Until v0.4.7, the answer to "is this implementation compatible" was "read the doc and ask Zion." After v0.4.7, the answer is "run the harness." The difference is the difference between a project and a protocol.
Run it against the reference:
git clone https://github.com/oversight-protocol/oversight
cd oversight
pip install ".[registry]"
python3 tests/test_registry_conformance.py
Or against a live operator URL:
OVERSIGHT_REGISTRY_URL=https://registry.example.org \
python3 tests/test_registry_conformance.py
v1.0 stays gated on this passing in both modes, against more than one registry, with the wire format declared stable. v0.4.7 is the work that makes that gate concrete.
Oversight is at
github.com/oversight-protocol/oversight.
The federation spec is at
docs/spec/registry-v1.md.
Issues and pull requests welcome from anyone who plans to stand up a
compatible registry.