From 0e5928273bad088accccf6a96f6c644fa2e78ba4 Mon Sep 17 00:00:00 2001 From: "david.yapfest.club" Date: Tue, 15 Sep 2026 14:11:24 -0400 Subject: [PATCH] =?UTF-8?q?D-012=20rev14=20correction=20(reviewer:d012-rev?= =?UTF-8?q?13-verifier=20HIGH):=20the=20rev13=20guard=20(authority=20exist?= =?UTF-8?q?s=20+=20live=20ALLOCATED=20attempt)=20was=20satisfiable=20by=20?= =?UTF-8?q?any=20ordinary=20allocation,=20so=20a=20caller=20could=20alloca?= =?UTF-8?q?te=20once=20and=20then=20directly=20reserve=20arbitrary=20globa?= =?UTF-8?q?l=20digests=20via=20register=5Fbinding=20=E2=80=94=20denying=20?= =?UTF-8?q?later=20legitimate=20consumption=20(replay=20poison)=20or=20lea?= =?UTF-8?q?ving=20orphan=20reservations=20after=20pre-consumption=20recove?= =?UTF-8?q?ry.=20Per=20the=20verifier's=20correction=20direction,=20replay?= =?UTF-8?q?-index=20insertion=20is=20no=20longer=20an=20independently=20ca?= =?UTF-8?q?llable=20operation:=20the=20public=20register=5Fbinding=20metho?= =?UTF-8?q?d=20is=20REMOVED,=20and=20the=20global=20replay=20check=20+=20b?= =?UTF-8?q?inding=20insertion=20+=20CONSUMED=20transition=20are=20ONE=20at?= =?UTF-8?q?omic=20unit=20inside=20consume=20(both=20locks=20held,=20final-?= =?UTF-8?q?point=20authorization=20already=20passed).=20The=20lock-release?= =?UTF-8?q?=20invariant=20is=20now=20bidirectional=20by=20construction:=20?= =?UTF-8?q?every=20=5Fconsumed=5Fbindings=20entry=20corresponds=20to=20an?= =?UTF-8?q?=20attempt=20durably=20in=20CONSUMED=20with=20that=20digest,=20?= =?UTF-8?q?and=20no=20public=20call=20can=20create=20one=20side=20alone.?= =?UTF-8?q?=20Standalone-API-absence=20probe=20added=20(live=20allocation?= =?UTF-8?q?=20reserves=20nothing;=20only=20consume=20creates=20a=20binding?= =?UTF-8?q?;=20no=20orphan=20via=20recovery;=20global=20replay=20still=20e?= =?UTF-8?q?nforced)=20and=20mutation-checked=20against=20a=20restored=20re?= =?UTF-8?q?v13-style=20register=5Fbinding.=2066/66=20tests.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decisions/d012/m_admission_replay.py | 62 ++++++++++------------ decisions/d012/test_d012_models.py | 78 +++++++++++++--------------- 2 files changed, 63 insertions(+), 77 deletions(-) diff --git a/decisions/d012/m_admission_replay.py b/decisions/d012/m_admission_replay.py index b8806a9e03..363378490e 100644 --- a/decisions/d012/m_admission_replay.py +++ b/decisions/d012/m_admission_replay.py @@ -6,10 +6,11 @@ temporal re-validation; rev9: the registered expiry is an immutable ceiling and the generation pair is materialized once into an owned snapshot; rev12: closed identifier/digest/outcome grammar, fixed rejection details, trusted-string decision checks, and no -exception-string formatting on the intake path; rev13: replay -registration is independently safe — all durable fields validated and -bound to an existing authority's live ALLOCATED attempt, fixed replay -detail). +exception-string formatting on the intake path; rev13/rev14: replay +registration is not an independently callable operation — the global +binding check-and-insert is the sole writer inside consume's atomic +transition, so every _consumed_bindings entry corresponds to an attempt +durably in CONSUMED with that digest). Corrections: - COMPOSITION IS MANDATORY: authority creation binds an M-A rev4 registry and @@ -34,9 +35,11 @@ SCHEMA = "D012-MODEL-ADMISSION-REPLAY-4" # is a REQUIRED protocol (rev6), currentness callbacks are followed by a # FULL post-callback revalidation including the temporal domain (rev7/rev8), # time values live in a closed nonnegative-int domain with a per-authority -# monotonic guard (rev7), and identifiers/digests/outcomes are closed +# monotonic guard (rev7), identifiers/digests/outcomes are closed # exact-string grammar with fixed rejection details and no exception-string -# formatting anywhere on the intake path (rev12). +# formatting anywhere on the intake path (rev12), and replay-index insertion +# is the sole writer inside consume's atomic transition — no standalone +# registration operation exists (rev14). from m_policy_generations import GenerationError @@ -161,33 +164,13 @@ class AdmissionRegistry: expires_at, currentness) self._authorities[authority_id] = auth return auth - - def register_binding(self, binding_digest, authority_id, attempt_id): - """Global replay check (atomic under the registry lock): a consumed - binding digest is unique across authorities, namespaces, attempts, - and generations. The method is independently safe against direct - calls (rev12-verifier Finding): every durable field is validated as - an exact immutable string before any hashing or membership, the - digest can only be reserved against an EXISTING authority's live - ALLOCATED attempt (a replay reservation cannot be forged without an - in-flight consumption), and the replay rejection detail is fixed — - no retained value is ever formatted.""" - binding_digest = _validated_token(binding_digest, "binding_digest") - authority_id = _validated_token(authority_id, "authority_id") - attempt_id = _validated_token(attempt_id, "attempt_id") - with self._lock: - if binding_digest in self._consumed_bindings: - raise AdmissionError(REJECT, "replay: binding digest already consumed") - authority = self._authorities.get(authority_id) - if authority is None: - raise AdmissionError(FAIL_CLOSED, - "binding requires a registered authority") - rec = authority.attempts.get(attempt_id) - if (rec is None or rec["state"] != ALLOCATED - or authority.live_attempt != attempt_id): - raise AdmissionError(FAIL_CLOSED, - "binding requires the authority's live ALLOCATED attempt") - self._consumed_bindings[binding_digest] = (authority_id, attempt_id) + # rev13-verifier HIGH: replay-index insertion is deliberately NOT an + # independently callable operation. The sole writer of + # _consumed_bindings is inside consume's atomic transition (both locks + # held, final-point authorization already passed), so the invariant at + # lock release is bidirectional by construction: every global binding + # entry corresponds to an attempt durably in CONSUMED with that digest, + # and no public call can create one side alone. class PromotionAuthority: @@ -327,19 +310,28 @@ class PromotionAuthority: """Final-point commit under both registry locks (M-B -> M-A): a generation revoke() is linearized entirely before the authorization decision (typed REJECT) or entirely after the commit; it can never - land between admit() returning ADMIT and the binding/state write.""" + land between admit() returning ADMIT and the binding/state write. + The global replay check, the binding insertion, and the CONSUMED + transition are ONE atomic unit — the sole writer of + _consumed_bindings (rev13-verifier HIGH): a live ALLOCATED attempt + cannot reserve any digest except through this transition, and every + global binding entry corresponds to an attempt durably in CONSUMED + with that digest.""" with self.registry._lock, _generation_lock(self.generation_registry): attempt_id = _validated_token(attempt_id, "attempt_id") + binding_digest = _validated_token(binding_digest, "binding_digest") now = _validated_instant(now, "now") self._enforce_monotonic(now) rec = self.attempts.get(attempt_id) if rec is None or rec["state"] != ALLOCATED or self.live_attempt != attempt_id: raise AdmissionError(REJECT, "consume requires the live ALLOCATED attempt") self._authorization_checks(now, attempt_id=attempt_id, expected_state=ALLOCATED) - self.registry.register_binding(binding_digest, self.id, attempt_id) + if binding_digest in self.registry._consumed_bindings: + raise AdmissionError(REJECT, "replay: binding digest already consumed") fresh = self.attempts[attempt_id] # validated post-callback snapshot rec = dict(fresh); rec["state"] = CONSUMED; rec["binding"] = binding_digest self.attempts[attempt_id] = rec + self.registry._consumed_bindings[binding_digest] = (self.id, attempt_id) self._last_now = now return CONSUMED diff --git a/decisions/d012/test_d012_models.py b/decisions/d012/test_d012_models.py index 2c26b331be..f722446ea6 100644 --- a/decisions/d012/test_d012_models.py +++ b/decisions/d012/test_d012_models.py @@ -13,9 +13,10 @@ pair intake (rev9); no-copy-protocol intake with cycle/depth guards (rev10); class-identity-only intake decisions with no equality dispatch or attribute access on rejected objects (rev11); M-B closed identifier/digest/outcome grammar, fixed time-rejection details, and no -exception-string formatting (rev12); independently safe replay -registration bound to a live ALLOCATED attempt (rev13). Schema constants -remain grammar revision 4. +exception-string formatting (rev12); replay-index insertion removed as a +standalone operation — the global binding check-and-insert lives only +inside consume's atomic transition (rev14). Schema constants remain +grammar revision 4. """ import sys, unittest, pathlib @@ -1026,49 +1027,42 @@ class AdmissionReplayTests(unittest.TestCase): self.assertEqual(calls["str"], 0) self.assertNotIn("A3", reg._authorities) - def test_register_binding_standalone_safety(self): - """rev12-verifier Finding: the public replay registration cannot - forge a reservation — behavioral fields are typed-rejected with - zero dunder dispatch, exact-string fields without an in-flight - consumed attempt are refused with no mutation, and a later - legitimate consume of the same digest succeeds exactly once.""" - calls = {"hash": 0, "eq": 0, "repr": 0} - class Behavioral: - def __hash__(self): - calls["hash"] += 1 - return 1 - def __eq__(self, other): - calls["eq"] += 1 - return True - def __repr__(self): - calls["repr"] += 1 - return "B1" + def test_replay_insertion_is_not_independently_callable(self): + """rev13-verifier HIGH: replay-index insertion is not exposed as a + standalone operation — a live ALLOCATED attempt cannot reserve any + digest except through the atomic consume transition, every global + binding entry corresponds to an attempt durably in CONSUMED with + that digest (no orphan after pre-consumption recovery), and the + global replay guard still rejects duplicates.""" gens, reg = self._gens_and_registry() - b = Behavioral() - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding(b, "A1", "T1") - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding("B1", b, "T1") - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding("B1", "A1", b) - self.assertEqual(calls, {"hash": 0, "eq": 0, "repr": 0}) # zero dispatch - self.assertEqual(reg._consumed_bindings, {}) # no reservation - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding("B1", "A1", "T1") # no such authority/attempt - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding("B1", "no-such-authority", "T1") - self.assertEqual(reg._consumed_bindings, {}) a = reg.authority("A1", "ns-A", gens, ("G1", "G2"), expires_at=1000, currentness=lambda: True) a.allocate("T1", 10) - self.assertEqual(a.consume("T1", "B1", 20), CONSUMED) # legit consume succeeds - self.assertEqual(reg._consumed_bindings["B1"], ("A1", "T1")) - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding("B9", "A1", "T1") # state now CONSUMED: refused - self.assertNotIn("B9", reg._consumed_bindings) - with _expect(AdmissionError, FAIL_CLOSED): - reg.register_binding("B1", b, "T1") # behavioral after replay: typed first - self.assertEqual(calls, {"hash": 0, "eq": 0, "repr": 0}) + self.assertFalse(hasattr(reg, "register_binding")) # no standalone API + self.assertEqual(reg._consumed_bindings, {}) # nothing reservable while ALLOCATED + self.assertEqual(a.consume("T1", "B1", 20), CONSUMED) + self.assertEqual(reg._consumed_bindings, {"B1": ("A1", "T1")}) + self.assertEqual(a.attempts["T1"]["state"], CONSUMED) + self.assertEqual(a.attempts["T1"]["binding"], "B1") + # no orphan is possible: pre-consumption recovery tombstones without + # ever touching the global registry, and post-consumption recovery + # retains the binding alongside the durable attempt record + a2 = reg.authority("A2", "ns-A", gens, ("G1", "G2"), expires_at=1000, + currentness=lambda: True) + a2.allocate("T2", 30) + a2.consume("T2", "B2", 40) + a2.recover_post_consumption() + self.assertEqual(reg._consumed_bindings["B2"], ("A2", "T2")) + self.assertEqual(a2.attempts["T2"]["state"], UNKNOWN_RECONCILE_ONLY) + self.assertEqual(a2.attempts["T2"]["binding"], "B2") + # and the global replay guard still rejects a duplicate digest + a3 = reg.authority("A3", "ns-A", gens, ("G1", "G2"), expires_at=1000, + currentness=lambda: True) + a3.allocate("T3", 50) + with _expect(AdmissionError, REJECT): + a3.consume("T3", "B1", 60) + self.assertEqual(a3.attempts["T3"]["state"], ALLOCATED) + self.assertEqual(reg._consumed_bindings, {"B1": ("A1", "T1"), "B2": ("A2", "T2")}) def test_authority_pair_reassignment_does_not_rebind_attempts(self): """Accidental-alias case (constrained posture): reassigning the -- 2.54.0