Files
alkblobs/tasks/c2-token-custody-posture.md
T
glm-5.3-flash 61966b6f09 feat(ops): the token registry's admission cap — ADR-019 (review-002 C-2, wave 13)
OQ-16 resolved to ADR-019 (accepted 2026-10-17) — the (a)+(c) split,
the (b) non-fleet TTL form rejected on record:

- ADR-019: the POC-L attack shape on paper (the non-confirming upload
  loop: unbounded registry growth + never-expiring pin custody
  non-fleet); refusal-not-eviction ruled; the S-5 deferred-judgment
  flip reasoned; the bytes-between-sweeps boundary named
- the cap: registry-entry bound over both custody forms, checked in
  the mint's lock section; OpsConfig::token_registry_max_entries
  (default TokenRegistry::DEFAULT_CAP = 65_536, usize::MAX uncapped);
  TokenRegistry::with_cap/new, len/is_empty monitoring accessors
- the honest answer to the already-committed put: Error::RegistryFull
  (the error family's sixth member, ADR-012 §6.5's rule exercised) ->
  the declared MINT_REFUSED put wire error (committed digest in the
  details; the CAS-idempotent re-put the recovery; the refused
  custodial guard drops — no custody taken); operational mint
  failures keep the internal mapping
- the non-fleet custody posture documented embedder-facing (ops
  module doc, ops-surface.md hand-over contract, registry/OpsConfig
  docs; the S-5 'deferred' language retired); the fleet TTL named as
  the safe posture for doors that cannot prove confirm; store-api.md
  §Error model gains the sixth member; open-questions.md OQ-16
  resolved; README ADR index + AGENTS.md range note updated
- tests: the non-confirming loop reproduced against mem at both
  altitudes — registry level (refusal at cap, no custody taken,
  nothing evicted, both mint forms under the one cap, confirm frees
  headroom) and wire level (MINT_REFUSED + committed-digest details
  over a committed put, the recovery re-put); error.rs variants to six

Gates: cargo test (default) green; cargo test --all-features green
against the live pg harness (330/125/4/3); clippy -D warnings clean
on both feature variants; cargo fmt --check clean; cargo doc
--no-deps warning-free
- task: c2-token-custody-posture flipped to completed
2026-10-17 18:28:53 +00:00

11 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
c2-token-custody-posture C-2 — the token registry's admission posture: OQ-16 resolved to ADR-019, the cap or the custody posture landed completed
alkcall-010-bump
moderate medium component implementation
wave-13
review-002
ops
security
tokens

Description

Review-002 C-2 (docs/reviews/002-first-consumer-review.md). Review-001's S-5 fixed the wire-schema part (the token field's maxLength/pattern, 437ab7f) and deferred the admission cap on the token registry as "a mint-contract policy decision, not security-round material" — recorded as known future hardening (the retention posture doc, src/ops/tokens.rs:182-191). The deferral's missing input now exists: POC-L supplies a named, client-reachable attack shape.

The shape (POC-L, on record — deliberately not patched in the POC): mint_custodial(namespace, key, guard) binds a put's guard into the token; the confirm act is left to the caller. On the fleet shape a TTL bounds the held pin. On a non-fleet store an unconfirmed token holds its pin indefinitely — custody that never expires, registered per mint, with no cap on mints. The LFS deployment shape makes the loop realistic: a hostile client POSTs uploads whose content never matches the claimed oid (or abandons the confirm step after each mint) — each minted token is a held pin and a registry entry, so the loop is unbounded pin-holding plus unbounded registry growth on any embedder whose door mints tokens per request without confirming.

This task opens OQ-16 in open-questions.md and resolves it by writing ADR-019, then lands the decision. The decision space (re-derive; the review's recommended shape is input, not adoption):

  • (a) The admission cap at the mint boundary — registry entry count and/or held-pin count; the cap's engine a registry-level knob. Must answer the already-verified put honestly (review-001's counter-consideration, which still stands): the put handler mints after the commit, so a refused mint answers a committed stream — what the putter is told and what happens to the hand-over is exactly the mint-contract policy decision (s5-token-field-bounds's Notes recorded why this is not trivially expressible; that record is the starting point, and the concrete attack shape is the new input that may flip it).
  • (b) The fleet-form TTL posture made available (and loud in the docs) for non-fleet embedded deployments whose door cannot prove confirm — a custody-expiry shape the non-fleet registry currently lacks by decided design ("nothing expires non-fleet").
  • (c) The documented non-fleet custody posture — the attack shape on paper as an explicit embedder responsibility (the door's mint policy is the embedder's; the registry's growth/custody behavior documented as contract, with the fleet TTL named as the safe posture).

The review's bar: not silence. The current state — deferred with the retention posture recorded — predates the concrete attack shape; the posture must now be decided with it on record. What should not happen is publish with the shape undocumented.

Landing: whatever ADR-019 decides — cap code + tests (a), the custody-expiry shape + docs (b), the posture docs (c), or a split (e.g. (c) now with (a) named as the hardening path). The reproduction/verification test per the review's re-verify note: the loop against a mem store — asserting the cap's refusal, the expiry's bound, or the documented behavior's shape, per the decision.

Acceptance Criteria

  • OQ-16 opened and resolved in open-questions.md; ADR-019 written and accepted — the decision made and the attack shape on paper in the ADR either way
  • The decision landed per the ADR (cap code + tests, or the posture docs, or the split) — the tokens.rs retention-posture doc updated to point at the decision (no stale "deferred" language if the cap lands)
  • The reproduction/verification test per the decision (the non-confirming loop against a mem store)
  • Gates green: cargo test --all-features (the ops-bearing runs; server-less the pg columns skip cleanly), clippy across the feature variants, cargo fmt --check

References

  • docs/reviews/002-first-consumer-review.md (C-2 — the attack shape, the decision framing, the "not silence" bar)
  • docs/reviews/001-storage-surface-general-review.md (S-5 — the original judgment and its counter-consideration)
  • tasks/s5-token-field-bounds.md (the deferred judgment + the refused-mint-answers-a-committed-put record)
  • src/ops/tokens.rs (the registry, mint_custodial, the custody split, the retention posture doc at :182-191)
  • src/ops/put.rs (the mint-after-commit ordering the cap must answer)
  • /workspace/@alkdev/alkgit/docs/research/pocl-findings.md (§ token hand-off — the attack path's record)
  • docs/architecture/decisions/017-ops-authority-model-cas-global-pool.md; ADR-012 §2 (the token contract); ops-surface.md

Notes

The decision: (a) and (c) as a split; (b) rejected (ADR-019, accepted 2026-10-17 — OQ-16 resolved in open-questions.md the same sitting).

  • The S-5 judgment flips. The deferral waited for a concrete input because the cap looked like an open eviction-policy question (which entry dies; what a refused mint answers a committed put). POC-L's non-confirming loop — the conforming-content variant that does reach the registry (the content-mismatch variant rejects the put with no mint, guard dropped) — is a client-reachable unbounded loop, which converts the question from "which entry dies" (answered: none die) to "does admission bound itself at all" (answered: yes). No eviction: a retiree's renewal would answer UnknownToken and its custody would drop beneath a live token — inventing loss in a contract whose loss model is otherwise exact.
  • The cap's shape (registry-level, per the task/ADR's "registry-entry count" arm): a live-entry bound over both custody forms, checked in the same lock section as the insert; OpsConfig::token_registry_max_entries (default TokenRegistry::DEFAULT_CAP = 65_536 — generous above honest concurrently-unconfirmed traffic while bounding the leak; each entry is ~hundreds of bytes, worst case tens of MB); usize::MAX documented as the uncapped form (an embedder that has bounded the loop's reach above the store); TokenRegistry::new() carries the default, with_cap is the explicit constructor; len/is_empty added as the monitoring window.
  • The honest answer to the already-verified put: a sixth error-family member, Error::RegistryFull (refusal-shaped like GcRefuse, ops-layer only — ADR-012 §6.5's "new failure families are new ADRs" rule exercised by ADR-019 itself), mapped by the put handler (both mint forms) onto the new declared put wire error MINT_REFUSED — stable domain text saying exactly what happened (committed and verified; no token; no custody taken — non-fleet the entry is sweep-eligible, fleet the pin row expires by TTL) with the committed digest in the details (the CAS re-put anchor). The guard dropped at the refused custodial mint is the no-custody-taken guarantee (asserted in both tests). Operational mint failures (entropy, engine) keep the internal mapping — only the cap refusal is the typed contract answer. Wire changes are additive (one declared code; request/response schemas unchanged) and pre-publish, exactly the cheap window.
  • Why (b) rejected (the ADR's three reasons, abbreviated): the fleet TTL is the pin-row machinery of the fleet core (renewal, never-resurrect, sweep) — the non-fleet guard has precisely no durable row and "nothing expires non-fleet" was the decided design; a non-fleet TTL is a second liveness semantic needing new clock/reaper machinery; expiry breaks the confirm verb (a confirm past expiry would register liveness for possibly-swept content — the mirrored never-resurrect hazard, worse than the leak); and the honest alternatives already exist (the fleet form for doors that cannot prove confirm; door-level admission policy for the loop's reach). Re-opening is a new ADR.
  • The boundary named so the cap is not misread: it bounds the registry's own resources (process-lifetime memory, never-expiring custody), not the pool's bytes — an authenticated writer still fills the pool between sweeps (the ACL write grant's story plus the GC cycle, unchanged).
  • Docs landed: the ops module doc (the retention-posture paragraph rewritten to the ADR-019 posture — no stale "deferred" language — plus the custody warning extended, the put section's hand-over refusal arm, the response contract note), TokenRegistry/ OpsConfig/mint-verb docs, ops-surface.md (the put op's declared refusal families + the hand-over contract's The admission posture), store-api.md §Error model (the sixth member + the altitude split), the architecture README's ADR index, AGENTS.md's ADR-range note (001-019, 018 reserved for the in-flight c1 task). The docs/plans/implementation.md wave-10 record is historical and untouched.
  • Test home: the registry-level loop in src/ops/tests.rs's sibling tokens.rs tests (the_non_confirming_loop_hits_the_ admission_cap_and_refusal_takes_no_custody — cap 3; the refusal's no-custody guarantee; both mint forms under the one cap; nothing evicted; confirm-frees-headroom), and the wire-level reproduction in src/ops/tests.rs (put_mint_refused_at_the_registry_cap_answers_committed_with_no_ token — MINT_REFUSED with the committed digest in the details, committed-but-no-custody asserted, one-entry cap, the recovery re-put answering {token, digest}). error.rs's variant tests updated to the six-member family. (Review-002's re-verify note: the loop reproduced against mem store — done at both altitudes.)

Summary

Landed the (a)+(c) split per ADR-019: the token registry's admission cap (registry-entry bound, refusal-not-eviction, default 65_536 / usize::MAX uncapped, knob OpsConfig::token_registry_max_entries, TokenRegistry::with_cap/DEFAULT_CAP/len), the refusal typed as the error family's sixth member Error::RegistryFull and answered by blobs/put through the new declared MINT_REFUSED wire error (the committed digest in the details; the CAS-idempotent re-put the recovery); the non-fleet custody posture documented embedder-facing with the POC-L attack shape (module doc, ops-surface.md, registry docs — the S-5 "deferred" language retired), the fleet TTL named as the safe posture for doors that cannot prove confirm, and the non-fleet TTL form rejected on ADR-019's record. OQ-16 resolved in open-questions.md. Tests: the non-confirming loop reproduced against mem at both altitudes (registry-level refusal/no-custody/no-eviction/ resume; wire-level MINT_REFUSED + committed-digest details + recovery re-put), the six-variant error tests. Gates: cargo test (default) green, cargo test --all-features green against the live pg harness (330/125/4/3), clippy -D warnings clean on both feature variants, cargo fmt --check clean, cargo doc --no-deps warning-free. ChangeLOG untouched (the wave-13 review gate owns the release-doc pass).