The digest wire form becomes the physical key encoding verbatim:
[registry byte] + digest bytes, hex-encoded (42 chars for the
21-byte keys 0x02/0x03, 66 for the 33-byte keys 0x01/0x04), across
blobs/stat, blobs/have, blobs/delete, blobs/fetch, and the put offer.
Bare hex dies — no implicit default-family reading survives.
- digest_ingress (src/ops/wire.rs): lowercase-strict charset check,
hex decode, then Key::from_bytes — the persistence boundary's
rejects-never-guesses decoder; the tag validated, the digest
length checked per tag, never inferred. The
Digest::from_git_oid_hex route (and its blob-family pinning) is
off the ops surface — zero callers in src/ops/.
- DIGEST_WIRE_PATTERN: ^[0-9a-f]{42}$|^[0-9a-f]{66}$ — gate and
ingress change together (S-5); the shared schema fns carry every
digest field through the new gate.
- INVALID_DIGEST texts (specs.rs stat/have/delete; wire.rs const +
message) state the tag-prefixed contract; the ops module doc's
wire-form paragraph restated, the blob-family scoping dropped.
- Tests: every wire-form site in src/ops/tests.rs switched to the
wire key hex (fleet columns included); the put response's bare
digest echo split out (response_hex_of) for the still-canonical
put handler until its task; three new pins — per-member wire hex
round-trips to the pool's held entries, the S-5 gate/ingress
agreement battery (bare hex → gate reject at INVALID_INPUT;
unknown tag / tag-width mismatch → ingress reject at
INVALID_DIGEST; ingress acceptance ⊆ gate, exact-key decode), and
the end-to-end family read-through (facade-published git-object
entry stat-able, have-servable, fetchable byte-exact in its own
0x04 key hex; a wire-offered family put rejects honestly at the
still-canonical cross-check with zero residue).
Verification: cargo test (default) and cargo test --all-features
against the live pg harness (341 + 125 passed), both clippy variants
(-D warnings), cargo fmt --check, cargo doc --no-deps, and the fuzz
corpora replay gate — all green.
Task: tasks/wire-key-hex-ingress.md (status: completed).
9.9 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wire-key-hex-ingress | The wire speaks keys — tag-prefixed key hex ingress + schemas across the ops surface | completed | narrow | medium | component | implementation |
|
Description
ADR-020 §Decision 2's wire form, landed across the ops surface (the
first seam of §Decision 9's order — it lands first because the facade
task deletes Digest::from_git_oid_hex, which this task takes the ops
module off). The digest wire form becomes the physical key encoding
verbatim: [registry byte] + digest bytes, hex-encoded — 42 chars for
the 21-byte keys (0x02/0x03), 66 for the 33-byte keys
(0x01/0x04) — parsed by the same rejects-never-guesses decoder the
persistence boundary already trusts. Bare hex dies: no implicit
default-family reading survives (zero wire consumers; decided on
coherence, ADR-020 §Decision 2).
Changes, all in src/ops/:
digest_ingress(src/ops/wire.rs:75): decode the hex to bytes, thenKey::from_bytes(src/keys.rs:98 — the tag validated, the digest length checked per tag, never inferred). TheDigest::from_git_oid_hexroute and its blob-family pinning go. The typed [INVALID_DIGEST] rejection keeps its mechanical shape (code + the offending string in details); the message text describes the family-explicit contract (tag-prefixed key hex, 42 or 66 chars), not the blob family's hex lengths.DIGEST_WIRE_PATTERN(src/ops/wire.rs:142):^[0-9a-f]{42}$|^[0-9a-f]{66}$.digest_wire_schema/digests_wire_schemainherit —blobs/stat,blobs/have,blobs/delete,blobs/fetch, and the put offer's digest field all gate on the new pattern through the shared schema fns.- The declared
INVALID_DIGESTerror texts (specs.rs stat/have/ delete; fetch.rs): the descriptions state the tag-prefixed contract. - The module doc's digest-wire-form paragraph (src/ops/mod.rs :328-341): the wire form restated; the blob-family-scoping posture dropped (the full docs sweep is record-corrections' — here the module doc must not contradict the landed behavior).
src/ops/tests.rs: every wire-form site updates — the stat/have/delete/fetch tests' digests become tag-prefixed key hex; the malformed-digest batteries gain the new reject shapes (bare 40/64-hex now rejected; wrong tag; tag/width mismatch — e.g. a 42-char string whose tag byte declares a 33-byte key) while keeping the old rejects that stay rejects (non-hex, wrong length outright). Sites constructing keys viaDigest::from_git_oid_hexswitch to the ingress orKey::from_bytesover the physical encoding.
Design points to record in-task:
- The ingress's exact decode path (hex decode then
Key::from_bytesvs a hex-taking key constructor if one exists) — either is fine; the contract is: the wire couples only to the frozen physical encoding, and every gate-passing string decodes identically at the ingress (S-5: the gate and the ingress disagree with no one). - The
INVALID_DIGESTdetails field name staysdigest(the offending string as presented) — renaming wire error details is not this task's call.
Intermediate-state note (expected, not a defect): after this task the
read ops serve family entries end to end — the store already holds
and serves family keys; only the ingress was the wall. The put
handler still hardcodes the canonical blob derivation (src/ops/put.rs
:333) until the put-handler task, so a wire-offered family put
rejects honestly at its cross-check (DIGEST_MISMATCH) — no wrong
landing is possible. The put offer's field is still optional and
still named digest at this stage; the mandatory-claim rename is the
put-handler task's.
Acceptance Criteria
digest_ingressdecodes tag-prefixed key hex via the physical encoding's decoder (tag validated, per-tag digest length checked, never inferred);Digest::from_git_oid_hexhas zero callers in src/ops/- The schema gate is
^[0-9a-f]{42}$|^[0-9a-f]{66}$and the gate and the ingress agree on every input (nothing the gate passes decodes differently below it; nothing the ingress accepts the gate rejects) — pinned by test - Bare 40/64-hex digest strings are rejected at the gate and at the ingress (the implicit default-family reading is dead) — pinned by test
- A family key (tag
0x03/0x04) offered overblobs/stat/blobs/have/blobs/delete/blobs/fetchresolves to the entry the pool holds — the read-side gap closed, pinned by test INVALID_DIGESTtexts describe the tag-prefixed contract; the module doc's wire-form paragraph matches the landed behavior- Gates green:
cargo test(default),cargo test --all-features(server-less; the pg columns skip cleanly — this task touches no pg code), both clippy variants,cargo fmt --check,cargo doc --no-deps
References
- docs/architecture/decisions/020-family-explicit-wire-payloads.md (§Decision 2 — this task's contract; §Decision 9 — the seam order)
- docs/reviews/003-family-wire-gap-review.md (F-1/F-3; D-1)
- docs/architecture/ops-surface.md (the wire form's WHAT — record corrections restates it; the behavior lands here)
- src/ops/wire.rs (digest_ingress :75, DIGEST_WIRE_PATTERN :142); src/ops/specs.rs; src/ops/fetch.rs; src/ops/mod.rs :328-341; src/ops/tests.rs; src/keys.rs :98 (Key::from_bytes)
Notes
The ingress's exact decode path (the design point, decided):
digest_ingress checks the string's charset itself (lowercase
0-9a-f only — hex::decode is case-insensitive, so an explicit
lowercase gate keeps the S-5 agreement with the [0-9a-f] schema
pattern: nothing the ingress accepts the gate rejects), then
hex::decode, then Key::from_bytes — the persistence boundary's
rejects-never-guesses decoder: the tag byte validated against the
registry, the digest length checked per tag, never inferred. No new
hex-taking key constructor was added; the frozen encoding's existing
decoder is the only route.
Gate/ingress layering, pinned honestly (the S-5 test's recorded
shape, refined from the acceptance's literal phrasing): the schema
gate's pattern is the widths + charset only (42/66), so the
gate-passing set is strictly larger than the ingress's acceptance set
— the regex cannot validate the tag byte's semantic. What is pinned:
(1) bare 40/64-hex dies at the gate (INVALID_INPUT; the old
implicit default-family readings are gone before any handler); (2)
the shapes the loose gate cannot see — an unknown tag byte, and a
tag/width mismatch (a 42-char string whose tag declares a 33-byte
key, and its inverse) — reject at the ingress (INVALID_DIGEST,
wire and handler-direct agreeing); (3) everything the ingress accepts
the gate passes, and the ingress's decode reproduces the constructed
key exactly, per registry member (ops::tests::bare_hex_dies_... and
ops::tests::the_wire_key_hex_decodes_...).
The put response's digest echo stays bare at this stage:
put.rs's {token, digest} response schema/minting is the
put-handler task's scope (ADR-020 §Decision 4's {token, key} rename
rides there), so the tests split the two forms — wire_hex_of (the
tag-prefixed offer/probe form) vs response_hex_of (the bare
canonical digest the response still echoes). No wrong anchoring is
possible: the MINT_REFUSED/CAS assertions against the echo use the
bare form exactly as put.rs mints it.
The INVALID_DIGEST details field name stays digest (the task's
recorded design point) — the offending string as presented, wire
error details untouched.
The stat semantic rode through untouched: the family read-through
test pins ADR-018 §Decision 3's stored-preimage length (stat's
len = the loose-object form's length, not the content's) for a
wire-borne family key — the store layer took zero changes, per ADR-020
§Decision 9.
Test support added in src/ops/tests.rs: wire_hex_of /
wire_hex_of_seed (the wire key hex forms), response_hex_of (the
bare echo form), and seeded_entry now returns the wire form; every
payload/construction site in the e2e tree (stat/have/delete/fetch/
put, the fleet columns included) switched to the wire key hex, with
the put-response echo assertions split out as above.
fetch_establish_whole-driven direct establisher tests present gate-
passing shapes only, so the post-schema "unreachable" guards stay
exercisable through bare-hex battery members driven handler-direct.
Summary
The wire now speaks keys: every digest-bearing field on the ops
surface (blobs/stat, blobs/have, blobs/delete, blobs/fetch,
and the blobs/put offer) carries the physical key encoding verbatim
— [registry byte] + digest bytes, hex-encoded, 42/66 chars —
gated by ^[0-9a-f]{42}$|^[0-9a-f]{66}$ and parsed by
digest_ingress → Key::from_bytes over the decoded bytes; bare
hex dies. Digest::from_git_oid_hex has zero callers in src/ops/
(the hashing.rs route itself is deleted by the facade task). The
declared INVALID_DIGEST texts and the ops module doc state the
tag-prefixed contract; the blob-family scoping posture is dropped.
Three pinned tests carry the wave: the per-member wire hex decodes to
the pool's held entries (stat/have/delete, all four registry
members), the S-5 gate/ingress agreement battery (bare hex → gate
reject; unknown tag / tag-width mismatch → ingress reject; acceptance
subset + exact-key decode), and the end-to-end family read-through
(facade-published git-object entry stat-able, have-servable,
fetchable byte-exact in its own tag 0x04 key hex, with the honest
DIGEST_MISMATCH on a wire-offered family put until the put-handler
task). Verification: cargo test and cargo test --all-features
against the live pg harness (all columns ran: 341 + 125 passed), both
clippy variants, cargo fmt --check, cargo doc --no-deps, and the
fuzz corpora replay gate — all green.