The last development note ended with the Rust registry refusing to lie when storage, migration, and the local log went sideways. The work since then is the last stretch before the registry v1 wire surface can be called stable. The federated monitor endpoint stops silently dropping corrupted leaves, both registries speak one error envelope, and the spec now names a candidate-frozen contract operators can build against. None of that is a v1.0 release. The point of this note is to say what is frozen, what is not, and what has to close before the tag.
The monitor endpoint that used to lie by omission
/tlog/range is the route a federated monitor calls to read
a slice of the local transparency log. Before this stretch of work, the
Rust registry served it by parsing leaves.jsonl line by
line and silently skipping malformed JSON. That is the wrong default
for an evidence protocol. A monitor that reads a shortened range, with
the corrupted entries simply missing, gets a false picture: it looks
like a clean, smaller log, and the corruption is invisible. The Rust
oversight-tlog crate gained a validated
range_records(start, limit) API, and the registry route
now reads through it. Disk truncation or a corrupted record fails
closed with a parse or hash error instead of returning a quietly
shorter answer.
The Python reference registry had the same posture and got the same
fix. Recovered leaf records now validate JSON shape, contiguous index,
a 32-byte leaf hash, and the SHA-256(0x00 || leaf_bytes)
recomputation before they enter the in-memory tree. New appends write
leaf_data_hex, so the exact bytes that fed the leaf hash
survive recovery, while the older human-readable leaf_data
field stays for registry event display. The Python
/tlog/range route calls the same
range_records path, returns HTTP 500 when local
validation fails, and rejects a negative start with HTTP
400.
The part I want to be explicit about: the range reader and the
startup recovery reader share one validation path. There is no
permissive reader left anywhere that can hand a monitor or an auditor
a lossy slice of the log. A verifier can recompute the RFC 6962 leaf
hash from exact bytes instead of trusting a lossy UTF-8 display
field, and the registry v1 spec now says so for
/tlog/range.
One error envelope, on both sides
The registry v1 spec already defined a standard error shape, but the
spec was stricter than the implementations. FastAPI and Pydantic would
return their own validation error bodies, and the Rust registry's
IntoResponse returned its own shape. An operator
integrating against the registry had to handle framework-native errors
the spec never promised.
Both sides now emit the spec envelope:
{"error":{"code":"not_found","message":"unknown file_id"}}
The Python server carries FastAPI exception handlers for
HTTPException and request-validation failures. The Rust
registry's IntoResponse maps signature, sidecar, auth,
not-found, conflict, rate-limit, and server errors into the same
{code, message} pair. The code vocabulary is fixed:
missing_field, signature_invalid,
sidecar_mismatch, issuer_mismatch,
auth_required, rate_limited,
not_found, and server_error. An operator
writing a client writes one error parser, not two.
The conformance harness grew to cover it. The
/tlog/range shape check from the parity push and four
new envelope checks, signature_invalid,
sidecar_mismatch, missing_field, and
not_found, took the harness from 33 checks at the start
of May to 38. That count is now part of the contract.
The candidate stability note
The capstone is docs/REGISTRY_V1_STABILITY.md. It names the v1.0 candidate-frozen route surface:
GET /health
GET /.well-known/oversight-registry
POST /register
POST /attribute
POST /dns_event
GET /evidence/{file_id}
GET /tlog/head
GET /tlog/proof/{index}
GET /tlog/range
GET /p/{token_id}.png
GET /r/{token_id} and GET /ocsp/r/{token_id}
GET /v/{token_id} and GET /lic/v/{token_id}
GET /candidates/semantic
Beyond the routes, the manifest-signature rules, the sidecar equality rule, the identifier length ceiling, the DNS bridge authentication rule, the operator-token header contract, evidence-bundle core fields, local tlog leaf record fields, and the standard error envelope are also candidate-frozen.
The compatibility rules are deliberately one-directional. A compatible implementation MAY add optional fields that older clients can ignore. It MUST NOT remove or rename fields, change paths, change canonicalization, weaken authentication, or return framework-native errors instead of the registry envelope. The same note lists the changes that would force a new profile or a v2 draft: changing manifest canonicalization or signature-verification bytes, accepting unsigned sidecars as registry authority, renaming required evidence-bundle fields, changing tlog leaf hashing or inclusion-proof shape, allowing unauthenticated non-loopback DNS events, changing route paths, or returning non-envelope errors for registry failures.
The conformance harness is the executable version of that contract.
tests/test_registry_conformance.py runs the 38 checks
against either the in-process Python reference or a live operator URL
through OVERSIGHT_REGISTRY_URL. Passing it is what lets
an operator claim registry v1 candidate compatibility. The harness
checks identity, liveness, registration, manifest-signature rejection,
sidecar rejection, attribution, evidence bundles, tlog head and range
shape, browser CORS, beacon routes, DNS-event fail-closed behavior,
and the representative error-envelope codes.
What this is not
This is a candidate freeze, not a final standard, and the note says so explicitly. Before the first v1.0 tag, the project still needs longer burn-in against a migrated operator database, a final live conformance run against the public deployment target, a release note that names the exact conformance count, and the tag itself. The route surface and the error envelope are the compatibility target operators can build against today; the final standards claim waits for the burn-in data.
I am comfortable calling a candidate freeze now because the hardening that earned it is done in public and checked by a test, not asserted by a document. The monitor endpoint reads through one validated path. The error envelope is the same bytes in Python and Rust. The route surface is a list a downstream implementer can hold us to. When something on that list has to change, the right answer is a new profile name, not a quiet edit. That is the discipline the candidate note is meant to enforce.
Relevant public references: the registry v1 stability note, the registry v1 spec, the deployment guide, and the roadmap.