v20.3.0: adopt persist v41.2.0 + leviculum v0.25.0, the #568 dial detach, #573 pyo3 hybrid, leviculum#52 rotation seal, #575 quorum - #574
Merged
Conversation
…ld any envelope since v19.0.0 (#573) Additive: one new optional argument and one new static method. No existing call signature changes. `Edge.build_signed_inbound_envelope` exists so a harness can hand `dispatch_inbound_bytes` a verifiable envelope without reverse-engineering the wire format. Since v19.0.0 it could not build a single envelope on the wheel that ships it: RuntimeError: build_signed_inbound_envelope: sign_envelope failed: … signer node-… has no ML-DSA-65 (PQC) half — every signature is the FULL Ed25519 + ML-DSA-65 hybrid, no fallback (CIRISEdge#425) v19.0.0 made every signature the full hybrid — correctly — and this surface was not moved with it. It hard-coded `None` for the PQC half, and its doc justified that with two claims that were both false on this wheel: that software-only signers don't carry a PQC half, and that the verify pipeline's Ed25519Fallback policy makes the field optional. The policy no longer decides whether a PQC half is required. The signer does, and it refuses at source. The Rust twin the doc names as its counterpart (tests/trust_short_circuit.rs::FedKey::local_signer) HAD been moved. That is why nothing here caught it: the two codepaths the doc calls identical had silently stopped being identical, and the only caller of this one is Python. THE FIX. `pqc_seed_bytes` (optional, 32 bytes) builds the PQC half and signs the full hybrid. The seed is TAKEN, not derived from `seed_bytes`: a convention ("same seed", "seed with byte 0 flipped") must be known identically by whoever REGISTERS the ML-DSA pubkey and whoever SIGNS with it, and when the two disagree the only symptom is a verify refusal at the far end with nothing in it pointing at the cause — a bill this codebase has already paid twice. `derive_ml_dsa_65_pubkey_base64` (new, static) returns the matching pubkey_ml_dsa_65_base64 for the federation_keys row from the same code that signs, so registering and signing cannot drift. The argument stays OPTIONAL so the classical-only refusal stays reachable and loud: omitting it fails at sign_envelope naming the missing half, which is the correct answer and not a silent downgrade. Signer construction moved into `software_hybrid_signer` so the tests drive the function the wheel calls rather than a copy — the copy drifting IS the bug. Three tests: the hybrid signature is present, classical-only refuses with the exact token CIRISConformance keys its imperative xfail on, and the derived pubkey equals the one the signer signs with. The false doc paragraph is deleted rather than corrected; it documented the defect. Surfaced by CIRISConformance#91. Unblocks test_230_intake_gate (3 tests) and test_520_wire_vocabulary::test_tier1_and_opaque_variants_accepted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…lives its round (#568) Additive at every public surface. Two substrate adopts and one transport fix, riding with the #573 FFI fix already on this branch. ## Adopts persist v41.0.0 -> v41.1.0. All four ABI constants unchanged, verify stays v14.2.0, so the >=41,<42 wheel floor holds. Its fix (#807) is in list_widening_candidates, which edge drives none of — taken for currency. leviculum v0.24.0+ciris.1 -> v0.25.0+ciris.1. leviculum#64 unbreaks OUR wheels: the upstream catch-up (+92) brought a BLE interface declaring bluer/dbus unconditionally, both pulling libdbus-sys, which builds on neither macOS nor Windows. leviculum's changelog names the consumer — CIRISEdge's darwin and win_amd64 wheels resolve this crate as a git dependency and would have failed at build time. Also carries leviculum#63 (below) and leviculum#52's rotation dwell note (edge drives rotation; worth an audit, not changed here). ## CIRISEdge#568 — a round no longer destroys the link it paid for The v20.2.0 fix addressed the missing-signer race. Re-measured: 181 s and 210 s, unmoved. The owner's key was on the peer at 31 s, so the key was never the bottleneck. Second, independent mechanism. The scheduler abandons a round at DEFAULT_ROUND_TIMEOUT (10 s); a pathed peer gets LINK_ESTABLISH_TIMEOUT (30 s) to establish. The dial ran INLINE in the round's future, so a cold dial could never finish inside the round that started it — arithmetic, not load — and abandoning the round DROPPED the half-built link. Every retry began cold; the peer converged only when it dialled us. Observed shape matches: timeout at +10 s, next completed round ~150 s later = five 30 s cadence ticks. This is the #532 establish:identify gap one layer up. #532 stopped N coordinators discarding each other's links; nothing stopped a round discarding its own. FIX: the dial runs in a spawned task holding a cloneable DialCtx, so abandoning a round abandons the WAIT and not the LINK. The task completes and publishes into reusable_dialed_link (dial_and_identify's existing PUBLISH LAST step), so the next round takes the fast reuse path. DialCtx is a context struct rather than a Weak<Self> because ReticulumTransport::new returns Self: a self-handle needs installing at every construction site, and a site that forgot would silently lose the detach. The three moved fns (dial_and_identify, reusable_link_to, path_table_snapshot) moved VERBATIM onto it — field names match, so no body changed — with thin delegates left behind so no existing call site moved. The dial-gate permit is acquired INSIDE the task: held by the caller, it would release on cancellation while the dial it gates still ran, leaving #532's single-flight property true only for callers that survive. MEASUREMENT: ladder.owner_binding_converged now records leviculum#63's retry_queued / retry_queue_cap / retry_dropped_total beside elapsed_ms. A slow convergence with a flat queue and one with dropped_total climbing are different bugs that read identically as a stopwatch value — leviculum reported that exact pattern from the live canonical ("the retry queue climbed past its warning threshold and began discarding traffic while the log read as quiet"). TESTS: the_dial_ctx_shares_the_transports_link_pool pins the load-bearing property by pointer identity — if dial_ctx() ever handed out copies, a detached dial would publish into a map nobody reads and the fix would look applied while changing nothing. a_cold_dial_cannot_fit_inside_a_round pins the inequality and says in its failure message why the detach is not a workaround for it. NOT CLAIMED: that 181/210 s is fixed. The tests pin the mechanism; only a mesh run measures the outcome. One run each was not a regression call and is not a fix call either. Verified: cargo tree -i shows ONE copy each of persist 41.1.0, the verify trio 14.2.0, leviculum 0.25.0. clippy -D warnings clean on pyo3-full --all-targets, three transport combos, the mesh-harness binary and the test-anchor lane. 1461 lib + 1867 integration tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…nsitions Matches CIRISPersist#809, which made this call first on the same registry and the same guard. Its reasoning applies here unchanged: CIRISRegistry v3.0.0 went live 2026-09-04 and holders and authorizations move during that migration, so a tag run blocked on a mid-migration count is a release blocked on somebody else's deploy window. Edge has a specific reason to weigh that heavily. v20.2.0 shipped with NO GitHub Release at all because this same pre-flight refused a registry that was healthy (#571). Losing a release to a registry state edge does not control has already happened once. Say plainly what it costs, because a check that cannot fail is a report: for the length of the transition this leg CANNOT catch a registry serving a bundle whose quorum has collapsed. What still fails in that case is the registration step that follows — this leg is early warning, and this half of it is warning only. The downgrade moves EXACTLY ONE case, re-verified against the #571 fault matrix: live bundle, quorum met pass -> pass quorum NOT met (1 of 2/3) FAIL -> pass + ::warning:: no holders fail -> fail no charter_root_key_id fail -> fail unparseable consensus_protocol fail -> fail old shape, deployed steward pass -> pass old shape, none deployed fail -> fail unrecognised third shape fail -> fail An absent or unrecognisable root still reds the lane; only a countable but short quorum is waved through. CIRISEdge#575 tracks restoring it, and says the two repos should restore together — two siblings enforcing different thresholds against one registry means one can release while the other cannot, for reasons neither repo's code explains. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…e observable never reached Python (leviculum#52) Audit of leviculum#52 against edge. Edge is exposed as the API SURFACE, not as a driver: it wraps the three IFAC rotation phases (ifac_install_next / ifac_activate_next / ifac_seal_rotation) and re-exports them to Python, but nothing in src/, tests/ or src/bin/ sequences activate -> seal. Edge cannot commit the zero-dwell defect itself. It hands it to an operator. TWO REAL DEFECTS, both in what edge tells that operator. 1. THE DOCSTRING NAMED THE WRONG HAZARD. Both wrappers said "call after the convergence window". That window is about MEMBERSHIP — every member holding the new key. But sealing also retires the old key for INBOUND, so it rejects packets already on the wire under the old mask, and those were sent by the members that DID re-key. An operator who followed the instruction correctly still stranded traffic. Upstream measured a zero-dwell rotation losing its packet in ~50% of runs with the peer's drops_ifac incrementing exactly once. Worse than no guidance, because it reads as complete: it documents the INTENDED exclusion and is silent on the accidental one. 2. THE ONE OBSERVABLE NEVER REACHED PYTHON. retry_queue_gauges (added this cut for #568) had exactly one caller, src/bin/edge_node.rs. So the operator holding the one call that can strand traffic could not see the one thing edge is able to see, and had to guess the whole window blind. Now exposed on PyEdge. WHAT THE DOCS NOW SAY. Four things carry the retired mask: this node's retry queue, its socket buffer, bytes in flight, and the peer's receive buffer. Edge observes exactly one. retry_queued == 0 is NECESSARY, NOT SUFFICIENT — stated as such, because a reader who takes it as sufficient has been given a new way to be wrong. Its worth is collapsing the wait from "however long anything could take" to one link-latency of slack, defensible per medium. leviculum's 500 ms is quoted as what upstream calls it: a loopback floor, not a deployment value. And the dwell is PER-NODE from that node's own activate, so a fleet need not seal in lockstep. NO HELPER, DELIBERATELY. A seal_after_drain() that polls the gauge must invent a policy for the queue never draining, and both answers belong to the operator, not to edge: block forever and the member being excluded stays admitted indefinitely; time out and seal anyway and the hazard returns, now buried in a function whose name promises it cannot happen. Which is right depends on why the rotation is happening. NO TEST, AND SAYING SO. Nothing in edge sequences the IFAC trio, so there is zero coverage and nothing to regress from. scope.rotation and conformance.rotation_frame_loss drive the MLS epoch advance and edge's scope-address table, not IFAC; the activate_next -> seal_rotation pair in scope_addressing.rs is the address table, pure in-memory with an injected Instant, and cannot detect a drain hazard in either mechanism. Per upstream's own evidence the natural test is a ~50% flake, so a single green run would prove nothing anyway — a test worth having here has to be a soak. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…rotation seal (leviculum#52) persist v41.1.0 -> v41.2.0. Tag derefs to 52f6592, verified green at job level upstream before tagging. All four ABI constants unchanged, verify stays v14.2.0, >=41,<42 floor holds. Its fix (#810) adds `rejected` to CIRISLens TaskStatus with a SQLite V136 rebuild — edge has no TaskStatus, no lens tables, and enables none of the lens features. Currency only. cargo tree -i: ONE copy. DEFAULT_IFAC_ROTATION_DWELL = RESOURCE_TRANSFER_TIMEOUT (120 s), exported to Python as default_ifac_rotation_dwell_ms(), referenced from both seal docstrings. ANCHORED, NOT CHOSEN: 10 s was proposed, and 10 s is shorter than all four of this file's in-flight windows (5 / 30 / 30 / 120 s). Edge already asserts a resource transfer can legitimately be in flight for two minutes, and since leviculum#62 a single logical delivery spans many segments over that whole window under the key being retired — a shorter dwell guillotines a transfer edge itself still considers healthy. It is the SAFE default, not the right number for every rotation: ejecting a compromised member is a reason to override downward on purpose and accept the drops. That decision depends on why the rotation is happening, which is why it is a constant to deviate from and not a helper that decides. No prior art to borrow: upstream Reticulum's IFAC is static per-interface config with no rotation ceremony — the three-phase rotation is a leviculum fork invention (leviculum#52). leviculum's own 500 ms is a loopback floor upstream explicitly disclaims as a deployment value. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…was justified by the wrong mechanism Two corrections from the leviculum#52 audit, both verified against the pinned source before editing. 1. SEAL BREAKS INBOUND ONLY, SO retry_queued POINTS THE OTHER WAY. leviculum consults the alternate key in exactly one place, verify_ifac — the inbound path. apply_ifac (outbound) never reads it. So a node's seal changes only what THAT node accepts, and what it rejects is old-masked traffic arriving FROM PEERS. Upstream's failing run showed drops_ifac at the relay: the relay's seal rejecting a member's in-flight output. retry_queued counts THIS node's OUTBOUND backlog. Reaching zero therefore licenses this node's PEERS to seal — not this node. The true precondition for a node sealing is "every peer's retry_queued is zero", which no single node can observe. Under lockstep rotation (the only mode edge supports, since nothing sequences the phases) every node polling its own gauge to zero approximates that fleet condition, and the local reading is a reasonable proxy. It is NOT a measurement of what this node's own seal will reject: a local zero while one congested peer still holds 800 old-masked packets is a green light that means nothing. All four docstrings (both seals, the gauge accessor, the pyo3 gauge) now say so. The earlier text told the operator to poll their own gauge to zero and then seal — a procedure that would have passed on the very node whose seal was about to drop packets. 2. THE 120 s ANCHOR: RIGHT NUMBER, WRONG STATED REASON. The earlier doc claimed leviculum#62 made a whole resource transfer old-masked at once, so the drain window was a transfer's 120 s duration. Wrong on both counts. Old-masked bytes stop being GENERATED at each peer's activate — every packet dispatched after it is new-masked, and receivers accept those because they installed the new key in phase 1. A transfer straddling activate is part old, part new; its duration is not the drain window. And #62 is receiver-side reassembly; it says nothing about when the sender masks. What actually bounds the window is retry-queue residency, and that has NO time bound: leviculum masks before enqueueing (transport.rs:423-448 pushes the wrapped bytes on BufferFull), re-sends queued bytes verbatim (driver/mod.rs:5205-5228), caps the queue by COUNT (1024), and drains it only as fast as the interface unblocks. There is no principled number to derive. 120 s stays — conservative, and conservative is correct for a safe default — but it is now stated as a CHOICE that reuses edge's committed answer to "how long can traffic legitimately remain in flight", not as a derivation from a mechanism that does not exist. Both corrections also applied to the release notes and PR body. Verified from the checkout: no alt branch in apply_ifac; retries.push(SendRetry{data: send_data}) after apply_ifac; retry.data re-sent verbatim. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
…onized (leviculum#52, closes the audit) A rotation has two dwells measuring different things, and this repo carries a constant for each: install -> activate key DISTRIBUTION DEFAULT_CONVERGENCE_WINDOW 300 s activate -> seal packet DRAIN DEFAULT_IFAC_ROTATION_DWELL 120 s Activating early is not a safety failure — upstream keeps the old key accept-only, so a straggler's outbound still lands while its inbound degrades until it upgrades (driver/mod.rs:2823-2826). Sealing early is the silent one. The pre-fix prose collapsed both into "the convergence window", which is how the hazard was mis-framed. Both seal docstrings and the dwell constant now distinguish them and cross-reference each other, so a later cleanup that notices two dwell constants and equalises them has to argue past the doc. Residency sharpened to what is checkable: the retry queue is a bare VecDeque<Vec<u8>> with no age, TTL or expiry; drain_retry_queues is head-of-line and rate-gated by next_slot_ms; a packet leaves only by send, cap-overflow eviction of the oldest, disconnect, or interface removal. The cap bounds DEPTH, not time. RESOURCE_TRANSFER_TIMEOUT is cited as the longest window this file already treats as survivable in-flight, not as a claim about masking duration. Audit closed: the subagent reports nothing else outstanding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LWqmRPPVvfgCEbwF7fU9zb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The whole cut in one PR. Closes #573. Addresses #568 (second mechanism).
Additive at every public surface — two substrate adopts, one transport fix, one FFI fix.
Adopts
persist v41.0.0 → v41.2.0. Two minors, both currency. All four ABI constants unchanged across both, verify stays v14.2.0, so the
>=41,<42wheel floor holds. v41.1.0 (#807) fixeslist_widening_candidates; v41.2.0 (#810) addsrejectedto the CIRISLensTaskStatusvocabulary with a SQLite V136 rebuild. Edge drives none of it — noTaskStatus, no lens tables, no lens features enabled. v41.2.0's tag derefs to52f6592, verified green at job level upstream before tagging.leviculum v0.24.0+ciris.1 → v0.25.0+ciris.1. This one unbreaks our wheels. The upstream catch-up (+92) brought a BLE interface declaring
bluer/dbusunconditionally, both pullinglibdbus-sys, which builds on neither macOS nor Windows. leviculum's changelog names the consumer:Also carries
leviculum#63(below) andleviculum#52, which is addressed in its own section.#568 — a round no longer destroys the link it paid for
The v20.2.0 fix addressed the missing-signer race. Re-measured: 181 s and 210 s, unmoved. The owner's key was on the peer at 31 s, so the key was never the bottleneck. This is a second, independent mechanism.
DEFAULT_ROUND_TIMEOUTis 10 s. A pathed peer getsLINK_ESTABLISH_TIMEOUT= 30 s to establish. The dial ran inline in the round's future, so a cold dial could never finish inside the round that started it — arithmetic, not load — and abandoning the round dropped the half-built link. Every retry began cold; the peer converged only when it dialled us. The observed shape matches exactly: timeout at +10 s, next completed round ~150 s later = five 30 s cadence ticks.This is the #532 establish:identify gap one layer up. #532 stopped N coordinators discarding each other's links; nothing stopped a round discarding its own.
The fix. The dial runs in a spawned task holding a cloneable
DialCtx, so abandoning a round abandons the wait, not the link. The task completes and publishes intoreusable_dialed_link—dial_and_identify's existing "PUBLISH LAST" step — so the next round takes the fast reuse path.Two deliberate choices worth review:
DialCtx, notWeak<Self>.ReticulumTransport::newreturnsSelf, so a self-handle needs installing at every construction site, and a site that forgot would silently lose the detach.The three moved functions (
dial_and_identify,reusable_link_to,path_table_snapshot) moved verbatim ontoDialCtx— field names match, so no body changed — with thin delegates left behind so no existing call site moved.Measurement, not inference
ladder.owner_binding_convergednow recordsleviculum#63'sretry_queued/retry_queue_cap/retry_dropped_totalbesideelapsed_ms. A slow convergence with a flat queue and one withdropped_totalclimbing are different bugs that read identically as a stopwatch value — and leviculum reported that exact pattern from the live canonical: "the retry queue climbed past its warning threshold and began discarding traffic while the log read as quiet."Tests
the_dial_ctx_shares_the_transports_link_pooldial_ctx()ever handed out copies, a detached dial would publish into a map nobody reads and the fix would look applied while changing nothinga_cold_dial_cannot_fit_inside_a_roundNot claimed: that 181/210 s is fixed. The tests pin the mechanism; only a mesh run measures the outcome. One run each was not a regression call, and it is not a fix call either.
leviculum#52 — the rotation seal named the wrong hazard
Audited (subagent) against leviculum's finding that
sealretires the old IFAC key for inbound, so a zero-dwell rotation strands in-flight traffic — ~50% packet loss upstream,drops_ifacincrementing exactly once each time.Edge is the API surface, not a driver. It wraps the three phases and re-exports them to Python; nothing in edge sequences them — no
activate → sealpair insrc/,tests/, orsrc/bin/. Edge cannot commit the defect itself. Two things it told the operator were wrong:verify_ifac;apply_ifacnever reads it. A node's seal rejects old-masked traffic arriving from peers (upstream sawdrops_ifacat the relay).retry_queuedis this node's outbound backlog — zero licenses its peers to seal, not itself. The true precondition (every peer at zero) is unobservable from one node; under lockstep rotation each node polling its own gauge approximates it. Now onPyEdge, documented as a fleet proxy, never a measurement of what this node's own seal will reject. The retry queue is the long pole: masked before enqueueing, re-sent verbatim, count-capped (1024), no time bound.A default — a conservative choice, not a derivation.
DEFAULT_IFAC_ROTATION_DWELL = RESOURCE_TRANSFER_TIMEOUT(120 s), exported asdefault_ifac_rotation_dwell_ms(). Residency has no time bound, so nothing to derive from; 120 s reuses edge's committed answer to how long traffic can legitimately remain in flight. An earlier draft justified it by transfer duration and leviculum#62 — wrong (old-masked bytes stop atactivate; #62 is receiver-side reassembly), caught by the audit and corrected before merge. 10 s was proposed; 10 s is shorter than every in-flight window edge already asserts. The safe default — ejecting a compromised member is a reason to go lower on purpose.Two clocks, not to be harmonized.
install → activatewaits on key distribution (DEFAULT_CONVERGENCE_WINDOW, 300 s — activating early only degrades stragglers).activate → sealwaits on packet drain (DEFAULT_IFAC_ROTATION_DWELL, 120 s — the silent one). Both docstrings and the constant now cross-reference each other so a later "cleanup" equalising them has to argue past the doc. Residency is bounded by neither a clock nor any deterministic quantity: no TTL, head-of-line, airtime-paced, and the 1024 cap bounds depth.No
seal_after_drainhelper. It would have to invent a never-drains policy, and both answers are the operator's: block forever and the excluded member stays admitted; time out and seal and the hazard returns, buried in a function whose name promises it can't.No test, stated plainly. Nothing sequences the IFAC trio, so zero coverage and nothing to regress from. The
activate_next → seal_rotationpair inscope_addressing.rsis the address table — in-memory with an injectedInstant, structurally blind to a drain hazard. Per upstream the natural test is a ~50% flake; a real test is a soak.No prior art: upstream Reticulum's IFAC is static config with no rotation ceremony. The three-phase rotation is a leviculum fork invention, and 500 ms is its loopback floor.
#575 — the AV-28 quorum assertion reports while the trust root transitions
Matches CIRISPersist#809 on the same registry and the same guard. Downgraded to
::warning::; re-ran the #571 fault matrix to prove the change moves exactly one case — absent holders, missing charter root, unparseable protocol, old-shape-none-deployed and unrecognised shapes all still red the lane. Tracked in #575, paired with persist's #809 so the two restore together.#573 — the pyo3 envelope helper signs hybrid
build_signed_inbound_envelopehard-codedNonefor the PQC half, so since v19.0.0 it could not build a single envelope on the wheel that ships it. Nothing here caught it because its Rust twin had been moved to hybrid — the two codepaths the doc calls identical had silently diverged, and the only caller of the broken one is Python.pqc_seed_bytesis taken, not derived: a derivation convention has to be known identically by whoever registers the ML-DSA pubkey and whoever signs with it, and when they disagree the only symptom is a verify refusal at the far end with nothing pointing at the cause.derive_ml_dsa_65_pubkey_base64closes that loop from the same code that signs.Unblocks CIRISConformance's
test_230_intake_gateandtest_520_wire_vocabulary::test_tier1_and_opaque_variants_accepted.Verification
cargo tree -i— one copy each of persist 41.1.0, verify trio 14.2.0, leviculum 0.25.0clippy -D warningsclean: pyo3-full--all-targets, three transport combos, the mesh-harness binary, the test-anchor lane81c18ac; integration suite re-running (1867 passed at757f97a; exit status captured from cargo, not a filtered pipe)🤖 Generated with Claude Code