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
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 |
|
moderate | medium | component | implementation |
|
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
UnknownTokenand 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(defaultTokenRegistry::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::MAXdocumented as the uncapped form (an embedder that has bounded the loop's reach above the store);TokenRegistry::new()carries the default,with_capis the explicit constructor;len/is_emptyadded as the monitoring window. - The honest answer to the already-verified put: a sixth
error-family member,
Error::RegistryFull(refusal-shaped likeGcRefuse, 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 errorMINT_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 theinternalmapping — 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). Thedocs/plans/implementation.mdwave-10 record is historical and untouched. - Test home: the registry-level loop in
src/ops/tests.rs's siblingtokens.rstests (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 insrc/ops/tests.rs(put_mint_refused_at_the_registry_cap_answers_committed_with_no_ token—MINT_REFUSEDwith 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).