Adopting persist v39.0.0 / edge v19.0.0 leaves three safety tests red, and they are not fixture noise — they are the API contract.
What changed
attestation_promote re-signed every promoted row with THIS NODE's key. v39.0.0 replaced it with enter_mesh + widen_audience, and Engine::custody_for now decides custody from the row:
| row |
signer in hand |
custody |
| unsigned, attested by this node |
— |
this node signs as the actor |
| unsigned, attested by another key |
that key's signer |
the actor signs now |
| unsigned, attested by another key |
none |
AwaitingActor — it waits |
That third row is the one we land in, and it is v39 working correctly: the fabric may no longer author a claim it is only carrying.
Where it bites
Three paths write a row attested by someone who is not this node, from an unsigned request, and used to rely on the node signing it into the federation:
safety::age::emit_age_assurance — attesting_key_id: subject_key_id. Age assurance is a SELF-declaration; the subject attests it.
safety::watchlist::{enable_watchlist, disable_watchlist} — attesting_key_id: signer_key_id, the enabling authority.
safety::moderation (record) and the seed_track_record shape behind existence_invariant_* — a member's own track record.
Each now returns Ok(AwaitingActor) and the row stays local-tier. The read side is federation-tier, so the feature reads as absent. Nothing errors. I added tracing::info!/warn! at each site so it is at least legible, and a warn! specifically on the watchlist WITHDRAW, where a silent wait is the worst of the three (peers keep reading the enable).
Failing, with the assertion each makes:
age_assurance_emit_read_roundtrip_and_misdeclaration_does_not_slash
existence_invariant_auto_promotes_highest_track_record_on_lapse
"with a live moderator the merit rows DO reach federation tier" 0 vs 3
watchlist_publish_hook_is_opt_in_and_defers_the_matcher 0 vs 1
The decision this needs
The claim genuinely belongs to its author, so one of:
- The client signs. These endpoints accept a claim plus its author's signature, stored as the row's base scrub at write; the node then co-scrubs at the crossing (
custody_for's first arm, no actor signer needed). Truest to CC, and the shape edge assumes — Signers.actor is documented as "the human's FedID signer as they hit send".
- The owner-signer capsule.
src/owner_signer_capsule.rs already exists for exactly this asymmetry — its module doc says the gap it closes is that "an agent recording a human authorization ... could not put the authorizing human's own signature on it". It holds an Arc<LocalSigner> and offers one verb. Threading it into these paths would cover the case where the author IS this node's owner, which is the single-user node. It cannot cover a third party, and it would need an accessor enter_mesh can take (&LocalSigner), which widens a surface that was deliberately narrow.
- Re-model the claim as node-attested about a subject — the node attests "subject X declared adult" rather than the subject attesting it. This changes authorship semantics and I do not think it is right for a self-declaration, but it is the only option that needs no signature from anyone else.
(1) for the general case and (2) as the bridge for owner-authored rows looks right to me, but this is an authorship decision, not an adoption detail, so I have not made it under a substrate repin.
Status
Everything else on the triple is green: 113/115 test binaries, 1194 tests, all 15 release gates, clippy and fmt clean. contacts_chat is 25/25 — the chat plane took its own v39 work (the reader now folds placement widenings, and a message's community identity is the widened row's id).
The other red suite, equivocation, is blocked upstream on CIRISPersist#801 (a widening drops the claim's asserted_at, so a signed instant is unrecoverable off-node) and is tracked separately.
Adopting persist v39.0.0 / edge v19.0.0 leaves three
safetytests red, and they are not fixture noise — they are the API contract.What changed
attestation_promotere-signed every promoted row with THIS NODE's key. v39.0.0 replaced it withenter_mesh+widen_audience, andEngine::custody_fornow decides custody from the row:AwaitingActor— it waitsThat third row is the one we land in, and it is v39 working correctly: the fabric may no longer author a claim it is only carrying.
Where it bites
Three paths write a row attested by someone who is not this node, from an unsigned request, and used to rely on the node signing it into the federation:
safety::age::emit_age_assurance—attesting_key_id: subject_key_id. Age assurance is a SELF-declaration; the subject attests it.safety::watchlist::{enable_watchlist, disable_watchlist}—attesting_key_id: signer_key_id, the enabling authority.safety::moderation(record) and theseed_track_recordshape behindexistence_invariant_*— a member's own track record.Each now returns
Ok(AwaitingActor)and the row stays local-tier. The read side is federation-tier, so the feature reads as absent. Nothing errors. I addedtracing::info!/warn!at each site so it is at least legible, and awarn!specifically on the watchlist WITHDRAW, where a silent wait is the worst of the three (peers keep reading the enable).Failing, with the assertion each makes:
The decision this needs
The claim genuinely belongs to its author, so one of:
custody_for's first arm, no actor signer needed). Truest to CC, and the shape edge assumes —Signers.actoris documented as "the human's FedID signer as they hit send".src/owner_signer_capsule.rsalready exists for exactly this asymmetry — its module doc says the gap it closes is that "an agent recording a human authorization ... could not put the authorizing human's own signature on it". It holds anArc<LocalSigner>and offers one verb. Threading it into these paths would cover the case where the author IS this node's owner, which is the single-user node. It cannot cover a third party, and it would need an accessorenter_meshcan take (&LocalSigner), which widens a surface that was deliberately narrow.(1) for the general case and (2) as the bridge for owner-authored rows looks right to me, but this is an authorship decision, not an adoption detail, so I have not made it under a substrate repin.
Status
Everything else on the triple is green: 113/115 test binaries, 1194 tests, all 15 release gates, clippy and fmt clean.
contacts_chatis 25/25 — the chat plane took its own v39 work (the reader now folds placement widenings, and a message's community identity is the widened row's id).The other red suite,
equivocation, is blocked upstream on CIRISPersist#801 (a widening drops the claim'sasserted_at, so a signed instant is unrecoverable off-node) and is tracked separately.