Files
alkblobs/tasks/wire-key-hex-ingress.md
T
glm-5.3-flash 3e0f04a7f9 feat(ops): the wire speaks keys — tag-prefixed key hex ingress + schemas (ADR-020 §Decision 2, wave 14)
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).
2026-10-18 17:50:15 +00:00

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
wave-14
review-003
adr-020
ops
wire
one-way-door

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, then Key::from_bytes (src/keys.rs:98 — the tag validated, the digest length checked per tag, never inferred). The Digest::from_git_oid_hex route 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_schema inherit — 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_DIGEST error 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 via Digest::from_git_oid_hex switch to the ingress or Key::from_bytes over the physical encoding.

Design points to record in-task:

  • The ingress's exact decode path (hex decode then Key::from_bytes vs 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_DIGEST details field name stays digest (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_ingress decodes tag-prefixed key hex via the physical encoding's decoder (tag validated, per-tag digest length checked, never inferred); Digest::from_git_oid_hex has 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 over blobs/stat/ blobs/have/blobs/delete/blobs/fetch resolves to the entry the pool holds — the read-side gap closed, pinned by test
  • INVALID_DIGEST texts 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.