Repository navigation
Releases: CIRISAI/CIRISPersist
Release list
ciris-persist v60.1.0
[60.1.0] - 2026-10-27
Adoption — what CIRISEdge, CIRISServer and CIRISAgent see
No break: nothing to change to keep working on this head. From
git diff v60.0.0.. over python/*.pyi, src/ffi/pyo3.rs, the pub items in
src/ and the pub enums: no Python method or function is removed or
re-signed, no enum gains or loses a variant (BlobError, Error,
DimensionRejectionReason, KeyGrantRefusalReason unchanged), no wire byte
moves (the delivery receipt's signed layout and every envelope kind are
unchanged). Additive:
- New functions. Rust:
federation::accord_threshold(n),
accord_threshold_over_roster,accord_threshold_now,
Engine::accord_threshold(AccordThreshold { m, n });
KeyGrantRefusalReason::is_retryable/from_token;
admission::dimension_rejected_reason::{ALL, RETRYABLE, is_retryable};
identity_amendment::{is_identity_amendment, check_identity_amendment_admission}and its constants;
takedown_tombstone::{takedown_reversal, is_reversal_of, REVERSED_OUTCOME};
custody_ack::{signed_receipt_at, delivery_receipt_dimension};
stream_receipt::try_receipt_signing_bytes(#1132). Python:
Engine.accord_threshold() -> (m, n),accord_threshold_for(n),
key_grant_refusal_is_retryable(reason),
dimension_rejection_is_retryable(reason)(ValueErroron an unknown
token), on the native module and the package. - New members.
RegisteredStreamEpoch.already_registeredand the same
key inregister_stream_epoch_dek's Python JSON (#1069);
ReackReport.restoredand its serialized key (#1113);DeploymentProfile's
two optional memberscalibration_versionandpipeline_metadata
(schema::AgentPipelineMetadata, #1089), absent on every v2.7.x profile,
whose serialized bytes are unchanged. A Rust caller that builds one of
these three structs with an exhaustive struct literal must name the new
member (none of Edge, Server, Lens, Node or Verify does). - Vocabulary.
delegation_scope::INFRA_RELAY = "infra:relay", APPENDED
todelegation_scope::ALL(position 24) andINFRA(position 12). - Meanings that narrowed.
blob_stream_epoch_dek_exists
(StreamEpochDekExists) now means exactly a DIFFERENT DEK for the
(stream, epoch); a re-supply of the held DEK isOkwith
already_registered: true(#1069). Custody'sreceivedis now ordered by
the subscriber-SIGNEDdelivery_receipt:{stream_id}:v1scores row's
instant, not by a stored v1 receipt's storage stamp (#1006). - New refusals, under existing tokens. An
identity_amendment:v1row
admits only whenauthorized_bynames an admitted approved deferral
response about the same agent whose signer held a Wise Authority seat at
its instant (the agent node's owner; on a split home, theuserthe
signing key is an occurrence of, through the
self:delegates_to:agent_occurrence:v1login anchor); otherwise
identity_amendment_unauthorizedunderfederation_invalid_argument,
PythonValueError(#1120). A delivery receipt whosesubscriber_key_id
orstream_iddoes not fit au32length is refused
(BlobError::InvalidArgument, PythonValueError) (#1132).
Fixed (slice A)
-
CIRISPersist#1124 —
CryptoTier::InvisibleEncrypted's doc comment said
"Opt-in / default-off"; self/family sealing has been mandatory, with no
switch, since v43.0.0 (#827). The comment,CryptoTieritself and the
FSD/SELF_FAMILY_DEK_CASCADE.md§0.5 table now say so. Doc only. -
CIRISPersist#1072 — the plaintext DAG doors refuse, before anything is
stored or announced, a version-1 manifest whose chunk carries aseqor an
epoch(finalize_blob_chunks_signing,put_blob_chunks_signing), and an
Externalchunk whosesize_bytesis not its manifest entry's
(put_plaintext_dag_chunk,put_blob_chunks_signing). Both used to be
stored and then fail every read or every finalize. Witness I1400 (sqlite,
postgres). -
CIRISPersist#1069 —
register_stream_epoch_dek: a re-supply of the DEK
a(stream, epoch)already holds isOkwith the new
RegisteredStreamEpoch::already_registered: true(and the same key in the
Python JSON), writing nothing (no terminator, no DEK row, no wrap, no
key_grant:stream:v1row). A DIFFERENT DEK stays
stream_epoch_dek_exists, now meaning exactly the conflict (non-retryable,
naming no key material). Persist compares the two itself, in constant time,
by opening the stored self-retention wrap; the racing-insert case is judged
the same way. The issue's proposedStreamEpochDekConflictvariant would be
a new arm of the exhaustiveBlobError; it is not added in this MINOR (the
existing token now carries exactly that meaning). Witness I1401 (sqlite,
postgres). -
CIRISPersist#1070 — takedown immediate removal, the three Codex r2
cases and the shared-chunk question:- an Engine with no LocalSigner (
with_hardware_signer,from_shared)
refused every immediate-removal takedown atpurge_finalize
(purge_retraction_inadmissible), leaving the bytes on disk. The purge
path now deletes without the retraction it cannot sign (such an Engine
never announced a claim; the tombstone withholds the address at every
read door). Witness I1402. - a takedown whose tombstone committed but whose holder action did not
run could never complete once its notice expired or the signer's duty
lapsed: a re-run re-asked CC 4.5.5. A recorded execution is now RESUMED
(signature still verified; the execution keyed by the notice's own
digest), locally and for a replicated bundle. Witness I1403. - the bytes floor read the tombstone before its write transaction, so a
takedown committing in between let the bytes land after their
tombstone. Every byte write door (put, local store, chunk DAG one-shot
and finalize, adopt, the chunk floor) now reads the address's own
tombstone again INSIDE its transaction; on postgres under a shared
advisory lock the tombstone's write takes exclusively. Witnesses I1404
(plaintext doors) and I1406 (adopt and chunk floor), both red before;
I1408 (postgres: a write past its in-transaction read holds the
takedown off until it commits, then the finalize deletes its bytes) and
I1409 (a write below V187, where no tombstone table exists, still
lands). - decided: a plaintext chunk shared with a live DAG stays (not withheld,
not evicted) when another DAG that uses it is taken down; the existing
purge walk already does this. Witness I1405.
- an Engine with no LocalSigner (
-
CIRISPersist#1006 —
custody_view'sreceivedwas ordered by this
node's storage stamp on a storedciris-delivery-receipt/v1receipt (whose
signed bytes carry no instant), so a receipt stored late outranked the
device's later signednone.receivedis now ordered only by the instant
the subscriber SIGNED on itsdelivery_receipt:{stream_id}:v1scores row
(CC 3.4.6; the spelling CIRISEdge's receiver already emits), its latest
live, unretired, unexpired one (custody_ack::signed_receipt_at). A stored
v1 receipt still joins the STH and names the subscriber as a device, and is
no longer custody evidence on its own. No wire change: the v1 receipt bytes
are unchanged. Not in this cut: the issue's membership check at
put_delivery_receipt(it reversesstream_receipt.rs's documented
consumer-policy posture and awaits a ruling). Witness I1407 (sqlite,
postgres); I513's control now signs its receipt row. -
CIRISPersist#1066 — investigated, not fixed in this cut. The premise
("the room-chunk sweep never evicts adopted stream-keyed chunks on a
holder") is true, and it is not a chunk-only gap: the holder's community
sweep walks only the holder's OWN(community, minter)counter (#848), so a
peer-minted room DAG, its manifest AND its chunks, is retained on the holder
whole;sweep_all_communitiesdoes not even visit a room the holder holds
only peers' bindings for. Witness I1410 (sqlite, postgres) pins the
property that holds either way (a holder keeps or drops a peer's DAG whole,
never half) and the #848 policy as it stands. Holder-side retention of peer
content is a policy ruling, open on the issue.
Evidence (slice A)
- Four rows join
evidence/cc_impl.tsv(from the manifest,
scripts/evidence_tsv.py write; the exact-count pin 342 → 346):
CLM-custody-receipt (3.1.3.3) atcustody_ack.rs#signed_receipt_at;
CLM-one-chunk-seal (5.3.3.2.5) atchunk_dag_cascade.rs#holds_this_dek;
CLM-one-tombstone-row (4.5.3) attakedown_tombstone.rs#already_recorded
andsteward_purge.rs#refuse_tombstoned_at_write.
Mutation (slice A)
scripts/mutants/scope-601-fixes.txt: 92 mutants, 75 caught, 17
unviable, 0 missed (with the merge fix's shared predicate); three weekly
shards inmutants.yml.
Added
- #1117 — the accord threshold is a function (CC 4.2.6 / CC 4.5.1.2,
rc10).federation::accord_threshold(n)is the strict majority
⌊n/2⌋ + 1, the one spelling every accord gate in the crate now counts
through (the co-scrub core, the destructive re-tally, the replicated-evidence
receive gate).accord_threshold_over_roster/accord_threshold_nowand
Engine::accord_thresholdreportAccordThreshold { m, n }over this
node's standing roster (holders whose keys it holds). Python:
Engine.accord_threshold() -> (m, n)andaccord_threshold_for(n). CIRISEdge's
verify_accord_carriercalls it instead of a hard-coded(2, 3).
Witness I1360 (sqlite, postgres): the reportedmis the gate's. - #1119 —
delegation_scope::INFRA_RELAY = "infra:relay"(CC 5.4.6
item 4, rc10): a steward-conferred relay standing, seat-like in the onion
layer cap's denominator. Conferred on the accord plane under the same
co-scrub rule asinfra:serveand read throughhas_accord_conferred_role;
no new row kind. APPENDED todelegation_scope::ALL(positi...
ciris-persist v60.0.0
[60.0.0] - 2026-10-26
An architecture cut (docs/V60_TASK_PLAN.md): signed attestations are the only
state (CEWPOS). The cirisnode classes and Goals become scores rows, the
organisation planes become affiliation rows, the key-record, consent and
sweep doors decide in one shared plan each, and the in-process memory backend
is gone. No feature outside the issues named here. Sections 1–14 are in
slice order (S1–S6, then S0's fixes and tooling) and §15 is the advisories;
§16 is the whole adoption list for CIRISEdge, CIRISServer and
CIRISAgent, derived from git diff v59.1.0.. over the .pyi, src/ffi,
the error enums and the migrations. Migrations V202–V205; witnesses from
I1161; supersets.rs EXPECTED 313 → 342.
Changed — 1. the cirisnode classes are scores rows (S1; CIRISPersist#988 item 1, #803, #854)
Signed attestations are the only state (CEWPOS). Every cirisnode class is a
dimension CC 3.1.9 registers, so each is now a hybrid-signed scores row on
the attestation plane, signed by its contributor's federation key, admitted by
the plane's own door and replicated like any row. The cirisnode.* typed
tables that held self-signed, Ed25519-only, never-replicated copies are gone.
| class | row | admission |
|---|---|---|
| Contribution (CC 2.1) | contribution:{subject_kind}:v1 |
the plane's door |
| vote | vote:{contribution_id}:v1, attested to the Contribution's author |
the plane's door |
| moderation event | moderation:{allegation_type}:v1, attested to and naming the accused |
the moderation gate (moderate duty-holders) |
| slashing | slashing:{outcome}:v1, naming the accused as subject |
the plane's door |
| reconsideration filing / ruling | reconsideration:{grounds}:v1 (a ruling carries outcome) |
the v59 subject-standing rule |
| credits / expertise balance | credits:{domain}:{language}:{subject}:v1 / expertise:{domain}:{language}:v1, this node → the contributor |
the plane's door; PROJECTED into the ledgers |
| takedown notice | takedown_notice:{basis}:v1, subject_kind: takedown_notice, score -1 |
NEW: the CC 4.5.5 duty-holder gate |
- The ledgers are projections (CLM-silo-projection-rebuildable,
federation::ledger_projection).credits_ledger/expertise_ledgerhold
the fold of THIS node's balance rows (a peer's balance about the same
contributor is held, never projected), the latest(asserted_at, attestation_id)per key, written by the put door's executor in the row's
transaction on sqlite and postgres. Each ledger row is a pure
function of one row:last_updated_atandcreated_atare both its signed
instant, and it carries the row'sattestation_id(new column). New
SiloProjection::CreditsLedger/ExpertiseLedger; a pre-V203 balance
(noattestation_id) is a red incheck_projection_rebuilduntil the
rehome (Engine::cirisnode_rehome_ledgers, run first by every balance
write) re-emits it as this node's row. - The takedown notice is a row (#803, CLM-takedown-notice-row). The put
door admits atakedown_notice:{basis}:v1row only from a duty-holder over
the content as this node holds it (takedown_authority, the decision the
tombstone door re-asks); thesteward_purgeleaf stays held shut (its
cosigned door is not built).REGISTERED_FAMILIES_HELD_SHUTnarrows from
takedown_notice:totakedown_notice:steward_purge:. The tombstone door
takes the notice row (TakedownNotice::of_row), verifies its hybrid
signature against the signer's registered keys (an unknown signer is the
retryablefederation_attester_key_unknown), holds the row, and its header
carries the row whole;TakedownReport.notice_attestation_idnames it. A
notice row is about its content, never a binding of it:content_binding
skips the family.LegalBasistakes CC 3.3.2's wire strings:dmca512,
dsa_article16(dmca_512/dsa_article_16no longer decode); the
pinned-divergence test is deleted. - Compat read, one release (v60.x, removed in v61): a v59 peer's purge
bundle still carries its notice as the retiredtakedown_notice
CONTRIBUTION.apply_replicated_takedownstill applies it (Ed25519
self-signature, as v59 verified it; basis read in CC's spelling; CC 4.5.5
re-asked here). The LOCAL door refuses the Contribution
(takedown_unauthorised, "retired"). - #854, one subject gate.
SubjectGateand
validate_subject_key_ids_atare removed;validate_subject_key_idsis the
rule at every door, a replicated row included, so an uppercase subject id
(the raw base64 pubkey the retired Contributions carried) is refused at
ingest. The canonical-spelling clauses run before the general uppercase one
so each refusal names its defect. - Retired, refused by name: a
key_grantContribution
(cirisnode_key_grant_retired; the key-grant plane,key_grant:{axis}:v1,
is the one door for a wrapped key), and a v59 typed envelope offered at a
cirisnode door (it carriessignature/ a contributor id:
cirisnode_typed_object_retired). Newcirisnode::Error::Retired(its
kind()is the token) andcirisnode::Error::Federation(a plane refusal,
itskind()the plane's). - V203 (sqlite + postgres twins): drops
contributions,votes,
moderation_events,slashing_attestations,reconsideration_requests,
reconsideration_attestationsand V056's two watch tables
(cirisnode_consent_sla_watch,cirisnode_revocation_promotion_watch,
nothing read them): 8 tables. Keeps the ledgers (withattestation_id),
scheduled_takedown_actions,federation_delivery_attestationsand
promotion_attestations; their foreign keys into the dropped table go
(sqlite: staged and re-created under the final name; postgres:DROP … CASCADEdrops only the constraints) and the row-naming columns become
TEXTon postgres. Rewritespurge_executions.legal_basisto CC's
spelling. The dropped rows were self-signed by keys persist cannot re-sign;
nothing outside persist wrote these tables. - Kept, with a row rule: a promotion record's targets are rows held here
on the target kind's family (all or none; theis_canonicalflip is gone
with the tables); a delivery attestation's announcement is a held
contribution:federation_announcement:v1row.
Python surface (method names kept; each write plans the row and signs it
with the engine's own composed hybrid signer):
| method | v59 | v60 |
|---|---|---|
cirisnode_put_contribution |
(envelope_json) -> None |
(body_json) -> str (the row id; a takedown_notice subject kind files the notice row) |
cirisnode_cast_vote |
(envelope_json) -> None |
(body_json) -> str ({contribution_id, score, detail?, rationale?}) |
cirisnode_update_credits_ledger |
(update_json) -> None |
(update_json) -> str |
cirisnode_update_expertise_ledger |
(update_json) -> None |
(update_json) -> str |
cirisnode_put_moderation_event |
(event_json) -> None |
(body_json) -> str ({target_contributor, allegation_type, community_id, payload}) |
cirisnode_put_slashing_attestation |
(att_json) -> None |
(body_json) -> str ({moderation_id, outcome, payload}) |
cirisnode_put_reconsideration_request |
(req_json) -> None |
(body_json) -> str ({slashing_id, grounds, payload}) |
cirisnode_put_reconsideration_attestation |
(att_json) -> None |
(body_json) -> str ({request_id, outcome, payload}) |
cirisnode_list_contributions |
(filter_json, cursor_json, limit) |
(author_id, filter_json=None, cursor_json=None, limit=100); items are rows, the cursor an AttestationCursor |
cirisnode_list_votes |
(filter_json, cursor_json, limit) |
(contribution_id, voter_id=None, cursor_json=None, limit=100) |
cirisnode_list_takedowns_for_filtered_json |
filter optional | filter_json must name claimant_key_id; items are notice rows |
cirisnode_process_takedown_admission_json |
(contribution_json) |
(notice_attestation_id); the report adds notice_attestation_id |
cirisnode_get_credits_ledger / _expertise_ledger |
— | the entry adds attestation_id; created_at is the row's instant |
cirisnode_put_promotion_attestation |
— | target_ids are held row ids |
cirisnode_put_delivery_attestation |
— | the announcement must be a held announcement row |
cirisnode_rehome_ledgers |
— | NEW () -> int |
cirisnode_check_ledger_projection |
— | NEW (projection) -> str (credits_ledger / expertise_ledger) |
Removed: cirisnode_list_takedowns_for_json, cirisnode_list_key_grants_for_json,
cirisnode_list_key_grants_for_content_json,
cirisnode_list_key_grants_for_filtered_json,
cirisnode_list_key_grants_for_scope_epoch_json,
cirisnode_put_key_grant_json, cirisnode_retire_key_grants_json.
Unchanged: cirisnode_routable_contributors, cirisnode_read_vote_weight,
cirisnode_list_delivery_attestations, cirisnode_count_delivery_attestations.
Rust surface: NodeCoreService keeps the ledger reads, the promotion
record and the delivery attestations, and adds legacy_ledger_rows; the
typed writes and list reads, put_key_grant, retire_key_grants and the
media reads are gone, with ContributionEnvelope, VoteEnvelope,
ModerationEvent, SlashingAttestation, ReconsiderationRequest,
ReconsiderationAttestation, the old ContributionsFilter, VotesFilter,
ListCursor, the list pages, TakedownFilter, KeyGrantFilter,
KeyGrantPayload and its types, RetireKeyGrantsOutcome,
takedown_notice_from_contribution, check_moderation_or_reject,
check_takedown_or_reject, payload_target_descriptor,
Engine::list_takedowns_for / list_key_grants_for and the sqlite
backend's set_admission_gate (the plane's gate is the gate). New:
cirisnode::rows (the planners, decode_input, the reads) and the
Engine::cirisnode_* doors. TakedownNoticePayload drops claimant_key_id
and asserted_at (the row's) and refuses unknown ...
ciris-persist v59.1.0
[59.1.0] - 2026-10-20
Changed — the production genesis bundle is the v4 amend (CIRISPersist#1106, CC 5.3.4)
src/federation/genesis/canonical_seed.jsonis the production bundle
re-minted as version 4 by the 2-of-3 amend (genesis::amend::AmendState)
in the holders' ceremony of 2026-10-20 (A1 and B1, on hardware, through
ciris-server 0.5.232's/v1/accord/final-genesis/amend). sha256
d17c05c090c73851043cb67c5ba433907e174d3dddfc0e138bfc1f421e2d710b,
181 437 bytes.- Carried byte for byte from the v53.1.1 final genesis (v3, sha256
db5e8e83…): family_key_id, holders, serve_nodes, consensus_protocol,
attestations (charter, grants, lifecycle, family record, canonical birth)
and produced_at. New:serve_transport_destinationswith one row, signed by
and aboutciris-canonical-1-d7bdeu223k, carrying the live canonical's
transit pair. Authorizations: A1 and B1, which is the charter's 2/3. - Why it matters: under the all-or-nothing transit seal (CC 5.3.3.5 E1),
a fresh node can seal to the canonical only through this seeded pair. A node
on v59.1.0 boots holding a Rooted pair for the canonical with no network. - Witness
tests/production_bundle_v4.rs#i1160_the_production_v4_bake_seeds_the_canonical_pair:
on the REAL baked bytes, with no test anchor,verify_ceremony_outputs
accepts the bundle,verify_bundle_quorumcounts A1 and B1, and a fresh
Engine(sqlite) resolvestransit_kex_pair_for(ciris-canonical-1-d7bdeu223k)
byte-equal to the bundle's X25519 and ML-KEM-768 keys. - The pins that named the v3 facts move to the v4 facts (I500 hash, #551,
#554, I428c). The v3 bytes stay astests/fixtures/genesis/canonical_seed.v3.json
so I1157's v3-preimage witness still proves the v3 digest is unchanged.
Evidence
CLM-canonical-rebind(3.1.5) gains its own rows, at
transport_rebind.rs#check_rebind_admissionand#check_serve_rotation.
v59.0.0 had filed the rotation door only under CLM-bundle-seeds-transit-pairs
(found by CC at re-pin 20).
ciris-persist v59.0.0
[59.0.0] - 2026-10-19
Security — an aggregate's sources are distinct ids (CIRISPersist#1102, CC 6.1.1 rc9 37494c0, CLM-member-commitment-unique)
- verify-core's
member_commitmentsorts its leaves but does not deduplicate,
so{a, b, c}and{a, b, c, c}share one root, and the descent set
source_count = n_eff = len(sources)with no duplicate check: a padded
list inflated the count the mass-dominance gate reads. A repeated id is now
refused before anything is read or hashed, at the descent
(fountain::descend_aggregated_sources_on_backend, soEngineand Python
descend_aggregated_sources) and at the builder
(fountain::member_commitment, now persist's: it returns
Result<[u8; 32], federation::Error>— a Rust signature change — instead of
re-exporting verify-core's), through ONE check
fountain::check_distinct_sources. The refusal is
federation_malformed_rowwith the new reason
aggregate_source_duplicate(appended to the closed
malformed_row::ALL); on the store path it rides the new
store::Error::MalformedRow(kindfederation_malformed_row, Python
ValueError("federation_malformed_row: aggregate_sources: aggregate_source_duplicate"), the type a rejected aggregation always had).
A distinct list is unchanged. Witness I1152
(tests/aggregation_tier.rs#i1152_member_commitment_unique, memory, sqlite,
postgres).
Security — a key id names its own key (CIRISPersist#1103, CC 2.6.8 rc9 957d1dd, CLM-key-id-fingerprint-verified)
- At the DOOR only — registration (
Engine::register_federation_key, Python
register_federation_key, and a host's raw registration: the capsule's
PutPublicKeyop and Pythonput_public_key) and first contact (akey:record:v1row offered
throughput_attestation/apply_replicated_attestation/
ApplyReplicatedAttestation, the proof-of-possession door) — a key whose
key_iddoes not end in-<fp>is refusedfederation_malformed_rowwith
the appended reasonkey_id_fingerprint_mismatch(Python
ValueError("federation_malformed_row: key_registration: key_id_fingerprint_mismatch")).fpis verify-core fedcode's fingerprint
of the record's OWN raw Ed25519 key (ciris_verify_core::fedcode, via
register::key_id_fingerprint; not re-implemented). One check,
register::check_key_id_fingerprint, after the registration signature
verifies. - Exempt, keyed off the bundle and the families (never a string prefix): an
accord holder or serve node of the pinned genesis bundle, or an accord
holder this build seeds, under the SAME Ed25519 key; the bundle's family id;
a held family's id. - Grandfathered: a key already held under a non-matching id still resolves
and nothing re-keys it, but it is refused when registered or offered at
first contact again under that id.device-<hex>-<fp>passes (the
roster-id form is a separate rule). - Fixtures: the registration matrix, I976/I977 and I1146 now name their keys
withtier_ingest::test_support::fp_key_id(the rule was not loosened).
Witness I1153 (key_id_fingerprint_invariants, memory, sqlite,
postgres; and the registration door on sqlite, postgres).
Added — the owner may author the community-trust capture grant (CIRISPersist#1096, CC 3.3.1 rc9 1a46e03, CLM-consent-community-trust-owner-authored)
consent:community_trust:v1admits an OWNER-authored grant beside the
node's own: attester a live roster key of the owner's fedID at the grant's
asserted_at(self_roster::acts_as_steward_at, with its W bound
check_roster_act_fresh— the one steward predicate, not a second),
attested the node. Anyone else is refused
community_trust_attester_not_owner(InvalidArgument, Python
ValueError, the type this gate always had; it replaces the v52 "a capture
grant is the NODE's own row" refusal).- In BOTH forms
subject_key_idsnames the node, its owner atasserted_at
and the agent key that acts on it (a heldagent-role key); a NEW grant
missing one is refusedcommunity_trust_subjects_incomplete. Each named
party withdraws (rule 2); only the attester recants. - The fold (
fold_community_trust) and the read door
(community_trust_consent_for, no longer filtered to the node's own rows)
group both forms byattested_key_id; the latest grant after the boundary
Rgoverns, and a revocation of any grant — the owner's recant of its own
included — ends every grant at or beforeR. An agent key's withdrawal is a
boundary over grants only; captured traces stand. - Grandfathered: a held node-authored grant in the v52 shape (the owner
alone) keeps counting, its byte-identical re-offer is admitted as held, and
the sqlite / postgres open counts them in one log line
(legacy_community_trust_grants_at_open,
community_trust_consent::is_legacy_node_authored_grant); nothing stored
is refused or rewritten. - Fixtures I950, I536, I538 and I148 now name all three parties. Witness
I1154 (memory, sqlite, postgres; the open-time count on sqlite,
postgres).
Added — the legacy occurrence revocation's carrier row (CIRISPersist#1104, CC 3.1.5 rc9 c901645, CLM-occurrence-revocation-carrier)
identity:occurrence_revocation:v1
(federation::occurrence_revocation_carrier): persist mints it from a held
framed occurrence revocation the way the key plane mints its rows.- Attester is the revoker; the framed object rides under
occurrence_revocation. - The signature columns are the object's own hybrid signature.
- id =
sha256("ciris:identity:occurrence_revocation:v1:" ‖ JCS(object))[..16]. - Score −1, never withdrawn.
- Federation scope: the key plane's global reach, so a stranger holding the
occurrence receives it. - A receiver re-derives the row and refuses a byte difference.
- Domain and rules:
FSD/KEY_PLANE_ROWS.md.
- Attester is the revoker; the framed object rides under
- Admission is structural, never an instant
(check_carrier_admission):- a
nodeoragentidentity's occurrence is admitted; - a user self's is admitted only when cosigned (row
additional_scrubs,
hybrid over the row envelope) by a strict majority of its CURRENT live
roster (self_roster::roster_now, verify-corestrict_majority); - otherwise it is refused
federation_malformed_rowwith the appended
reasoncarrier_without_roster_quorum.
A backdated framed object freshly signed by one key is refused whatever its
instant. An admitted carrier projects the revocation on every node that
admits it.
- a
- Migration at open: every held
legacy_framedrevocation is offered as
its carrier. The rehome report gainscarriers: minted;without_quorum,
a user self's pairs NAMED in the open's log line for the owner to re-propose
through the two-path door; andnot_carried. Nothing held is rewritten. - The one predicate (for CIRISEdge):
key_plane::occurrence_revoked_by(dir, row) -> Option<(identity, occurrence)>
recognises awithdrawsof anidentity:occurrence:v1row and a carrier. - Witness I1155 (memory, sqlite, postgres; the migration on sqlite,
postgres); I1144 asserts its legacy revocation is carried.
Added — the genesis bundle seeds the serve nodes' transit pairs (v4); amend mode (CIRISPersist#1106, CC 5.3.4 rc9 0edac07, CLM-bundle-seeds-transit-pairs)
GENESIS_BUNDLE_VERSION3 → 4. New member
serve_transport_destinations(wire name the same): each seated serve
node's signedSignedTransportDestinationcarrying its transit KEX pair.- It is serde-defaulted and omitted when empty, so a v2 / v3 bundle parses,
re-serializes byte for byte and behaves as before. - From version 4 the member is inside
authorization_digest, each row
projected to{attesting_key_id, signed_envelope}. The v2 / v3 preimage is
byte-identical; the compiled-in v3 bundle's production authorizations
still verify.
- It is serde-defaulted and omitted when empty, so a v2 / v3 bundle parses,
- Bundle verification:
- Done by
genesis::check_serve_transport_destinations, run by
verify_bundle_quorumand at seeding. - Every row must be about and signed by a key in that bundle's
serve_nodes(hybrid overJCS(signed_envelope)against the record's
keys), carry a transit pair, and pass the separation rule. - Any failure fails the WHOLE bundle with
genesis_serve_transport_destination_invalid. A v3 bundle carrying
rows is refused.
- Done by
- Seeding at first open:
genesis::seed_serve_transport_destinationsruns after the serve nodes'
keys are seeded.- It goes through the ORDINARY signed transport door, so the row lands
Rootedandself_at_login::transit_kex_pair_for(serve_node)returns it
on a fresh node with no network.
- The ceremony carries
CeremonyInputs::serve_transport_destinations
(each seated serve node's current signed row, gathered by the caller) and
an optionalbundle_version. The test ceremony mints v4 with the test
node's row. - Server must re-mint the pinned bundle. The baked
canonical_seed.json
stays v3 and seeds no pair until then. - The AMEND ceremony (
genesis::amend::AmendState):- It verifies the pinned bundle with the existing doors, then carries
holders, serve nodes, consensus protocol, rows, roster records and
produced_atbyte for byte. - It adds the member, and its one signing item is the authorization over the
new digest. It finishes atmax(charter M, strict_majority(roster))
DISTINCT holder authorizations, then runsverify_ceremony_outputs. - Refusals:
genesis_amend_carried_bytes_changed,
genesis_amend_quorum_not_met,genesis_amend_input_refused. verify_ceremony_outputsstill requires every holder's signature on the
accord FAMILY RECORD and every founder's on the community birth. Both are
carried bytes from the original unanimous ceremony, so an amend satisfies
them without re-signing. No door requires every holder's authorization.
-...
- It verifies the pinned bundle with the existing doors, then carries
ciris-persist v58.0.0
[58.0.0] - 2026-10-18
Security — five advisories published after v57.0.0, none reachable (deny.toml)
zip< 9 (RUSTSEC-2026-0367, -0368, -0369): SUID/SGID bits and symlink
targets on EXTRACTION. Transitive only via candle-core and
ciris-verify-core, both held at zip 7.x; persist never extracts a ZIP
archive.zbus_names,zvariant,zvariant_deriveunmaintained
(RUSTSEC-2026-0370, -0371, -0372) via ciris-keyring → secret-service, with
no successor and no known vulnerability. Ignored with that rationale;
re-evaluate on the next candle or ciris-keyring bump.
Changed — the halt latch is not a leg of the trust verdict (CIRISPersist#1094 item 1; CC 3.2 T3/T8, 4.2.1.1; steward ruling 2026-10-15)
trust_root_validno longer foldshalt_latchedintovalid. A
constitutionalhalt is an agent pause (state kept, zero data disruption;
the agent's act gate is its only gate). From v24.0.0 to v57.0.0 the latch was
a conjunct of the verdict, so the conferral walk
(capability_roots_to_trusted_root), the transit walk
(transit_eligibility_walk) and through themcheck_reserved_prefix_admission,
check_revocation_authority,resolve_content_class_flag, serve-tier
resolution, the trusted-publisher fold andresolve_transit_eligibilityall
failed closed under a latched halt — the live G7 breaker CC 4.2.1.1 names
(CIRISConstitution b9fc6c0).halt_latchedstays onTrustRootVerdictas a
reported signal besidedrill_freshness(a halt must be observable); the
Python surface is unchanged.- Witness:
exercise_family_trust_rootstep (e) now asserts the inverse of
what it asserted since v24.0.0 — under a latched family halt the verdict,
itsbounded_untiland the conferral walk are unchanged, and a resume only
clears the report;exercise_transit_eligibilitygains step (h): a latched
halt on the shared root leaves transit eligibility byte-equal. Both run on
the sqlite, postgres and memory backends. Re-adding the conjunct turns both
red. - Evidence:
CLM-nsproc-halt-latched-trust(retired on the CC side) is
replaced byCLM-halt-not-a-trust-legattrust_root.rs#trust_root_valid
andtrust_root.rs#transit_eligibility_walk.
Changed — Art. 17 erasure of an adverse row is its producer's withdraws; the tombstone is the replicated fact (CIRISPersist#1094 item 2; CC 4.5.3 "Erasure at the subject's request", CLM-erasure-subject-route; CIRISConstitution#174)
- Controller-only (steward ruling 2026-10-15, CIRISConstitution
formal/withdraws_admission). Adetection:*row is adverse: the CC
2.4.1.1 carve-out keeps every subject-authority path (rules 2–4) closed to
it, unchanged (admission::resolve_withdraws_admission_rule_at). The
subject's Article 17 request is executed by the row's PRODUCER under rule 1.
Both DSAR doors (delete_traces_for_agent_id_hash,delete_traces_for_agent)
plan, for each row-backed detection of the erased traces, the producer's
withdraws: a local-tier row attested by the detector key that signed the
detection, its signature deferred to the crossing as the detection's own
is, admitted as rule 1 by the one withdraws predicate
(subject_erasure::plan_store_erasure). A producer withdraws already held
is reused. In the door's one transaction the withdraws is stored, the row is
hard-deleted, and a tombstone is written whose header cites it
(subject_erasure::ErasureNotice: the withdraws and target ids, nothing
else;legal_basiscolumnwithdraws, settled at execution). - Refused with a reason. When the host holds no live record of a row's
producer key the row is not erased:ErasureSummaryandDeleteSummary
gaindetection_rows_refused: Vec<ErasureRefusal { record_id, reason }>
(reasonerasure_producer_key_not_held; CC 4.4erasure_policy's
refused-with-reason, GDPR Art. 17(3)). Python: both DSAR dicts gain
"detection_rows_refused": [{"record_id": str, "reason": str}]. - The tombstone replicates. Erasure headers are served by
list_purge_bundles_sinceon all three backends;Engine::apply_replicated_purge
routes an erasure bundle tosubject_erasure::apply_replicated_erasure,
which requires the producer's withdraws held there (else
subject_erasure_withdraws_unheld, retryable), re-derives it as rule 1 on
that node's own state, and destroys the replica on receipt. v57's "header
never served" gap is closed. - A
detection:*row names its subject: the scored agent's CC 2.3.2.1
canonical hash,canonical:sha256:+ sha256(ciris:agent:{agent_id_hash}),
read from the trace this node holds (subject_erasure::agent_subject), so
rows about a subject are discoverable. It is not an authority path. The
steward purge's interim human-subject gate resolves that subject as the key
that signed the scored trace. - The hard case names nobody.
hard_case:trace_erasure:v1carries
TraceErasureCountsalone (the door, counts of traces, LLM calls,
detections erased and refused, and the one-key door's federation counts). - V196 deletes the v57-shaped
hard_case:trace_erasure:v1rows that named
their subject (and their projection rows), pre-v57 silo rows of that kind
that did, and everyart17_erasureheader with its tombstones (each
header's notice was such a row). This is the one place a hard_case row is
ever deleted: its content is what the erasure required gone, and v57.0.0
had no adopters. The boot logs the counts it deleted.
Changed — the subject of the target may file a reconsideration (CIRISPersist#1094 item 3; CC 4.5.5, CLM-reconsideration-subject-standing)
admission::check_reconsideration_admission: areconsideration:{grounds}
FILING naming its target byreferences_attestation_idis admitted from
the subject of the target as this node holds it (its signed
subject_key_ids, or the filer whose key digests to its
subject_sha256_ed25519_raw) or from a review-holder of the target's
community. A RULING (a row carryingoutcome) stays with the review-holders.
The action's community is the target's; a filing naming another is refused
(reconsideration_community_mismatch). A self-declared subject is never
standing; a target not held here, or alicensure:*target (its own arm,
CIRISPersist#1036 item 5), leaves the subject arm empty.- CC 4.5.3 struck "an immediate-removal basis from any claimant": persist had
no such path (takedown_authoritynever read the basis); I970 now pins
every immediate-removal basis to the duty-holder gate on every backend.
Removed
steward_purge::ART17_ERASURE_BASIS,art17_erasure_record_ids,
art17_erasure_executionandhard_case::bind_erased_detection_rows
(clean break, no alias): there is no node-local purge basis for erasure.
hard_case::trace_erasure_eventtakes&TraceErasureCountsnow.
ErasureSummaryandDeleteSummaryare no longerCopy(they carry the
refusal list).
Changed — BREAKING: the key plane as rows (CIRISPersist#991 plane 1 of 3; CC 3.1.3, CC 3.3.6, CC 2.4.1.1; CLM-planes-as-rows)
A key's claim of itself, a key-plane revocation and an identity's occurrence
record are scores rows on the attestation plane; federation_keys,
federation_revocations and the signed rows of
federation_identity_occurrences are their projections
(federation::key_plane).
- Who signs a row. Persist mints each row from the signed object it
verified (thetrace:complete:v1/detection:*precedent), so no producer
changes its wire. The row carries the object under one envelope member
(key_record,revocation,identity_occurrence), its signature columns
are the object's own hybrid signature, and its id is
sha256(domain ‖ JCS(object)). A receiver re-derives the row from the
object and refuses one that differs by a byte
(check_canonical_plane_row); the generic verify defers to that family rule. key:record:v1(attester = attested = the key) is admitted by proof of
possession against the keys it carries
(register::verify_proof_of_possession). A record whose base scrub is
another key's (the pre-rc8 conferral shape: anchor-scrubbed records, the
bakedciris-canonical-1seed, a scrub-upgraded key) is a row too, verified
against its held signer. A record that verifies under neither — Ed25519-only,
unsigned (operator-added peers), a bad scrub — is held without a row. A
key's newer claim (rebind, rotation, scrub upgrade, PQC completion) is a new
row that replaces the older one, whose bytes stay in
federation_key_registration_history.key:revocation:v1(attester = the revoker; subject the key digest the
object carries).put_revocationis one plan on every backend
(key_plane::plan_put_revocation+KeyPlaneStore::store_planned_revocation);
revoked_afteronly ever lowers (a raising revocation is refused
RevocationBoundRefusal::BoundRaised,bound_raised); awithdrawsor
recantsnaming the row is refused, its revoker's included. An
Ed25519-only revocation is refused: the hybrid-pending revocation path
ends (attach_revocation_pqc_signature,list_hybrid_pending_revocations
and the revocation leg ofrun_pqc_sweepare removed).identity:occurrence:v1(attester = the signer, attested = the
identity), for a hybrid-signed occurrence. An occurrence's revocation is a
withdrawson its row, admitted when the issuer acts for the identity
(signer_acts_for, CC 3.3.6); it feeds the local
federation_identity_occurrence_revocationsprojection. CC rc8 names no
family for occurrence revocations, so the signed revocation object stays a
legitimate authoring door (put_identity_occurrence_revocation,
SignedIdentityOccurrenceRevocation,
verify_signed_identity_occurrence_revocation) feeding the same projection
— see "compat reads" below.- The order rule. Every other row is admitted only once its signer's...
ciris-persist v57.0.0
[57.0.0] - 2026-10-15
Fixed — tag CI: the restored cache repair runs --strict (v56.0.0 windows wheel)
-
No library code changed. The v56.0.0 tag run's
maturin abi3-py311 wheel (windows-x86_64)failed as v55.0.0's did (out/bindgen.rsmissing under a
trustedlibsqlite3-sysfingerprint) although the v55.0.1 repair ran: it
removed two otherlibsqlite3-sysunits and KEPT the poisoned one as
"unverified, far from the cutoff". The single-cutoff bound assumes one
prune cycle; a unit no re-saving job compiles survives several, and its
file was pruned under an earlier cutoff..github/actions/ciriscachenow
runsrepair --strict: every manifest-less build-script unit is dropped
(build scripts rerun, minutes) until a sealed save replaces the blob.
The v56.0.0 release body was set by hand, as v55.0.0's was. -
The repair's CLI accepts
--strictin either position (repair --strict <dir>is the restore action's form). PR #1093's first CI run printed usage and exited 2 on every job; the self-test now exercises the CLI (19 checks).
Changed — CIRISVerify v20.2.0 re-pin (CIRISVerify#309)
- The seven Rust pins (
ciris-verify-core,ciris-keyring×4 targets,
ciris-crypto, theserverfeature set) move v20.1.0 → v20.2.0 together,
and the wheel'sRequires-Distfloor tociris-verify>=20.2.0,<21in
lockstep. v20.2.0 addsciris_crypto::transit_kex, the A/V per-hop
transit key (CC 5.3.3.5 E1) that runs the hybrid X25519 + ML-KEM-768 KEX
against the statictransit_kex_pubkeyspair persist stores on the
transport_destinationbinding since v56.0.0 (#1077). Persist stores the
pair; Edge runs the exchange. No persist API changes.ciris-build-sign
stays at the v20.1.0 prebuilt (no v20.2.0 GitHub release carries one).
Added — tooling: scripts/cargo_guard.sh (#1092)
- One build at a time on the box (
flock), an available-RAM floor (8 GB), and a
transient systemd user scope withMemoryMax(14 GB, no swap) so a runaway
cargo is OOM-killed inside its own scope instead of taking the session with
it (four sessions were lost that way during v55/v56). Every agent's cargo,
nextest and maturin call goes through it; exit 75/76 when the lock or the
floor is never met, 137 when the cap fires.
Changed — hard_case and detection onto the plane (#988 items 2, 3)
Two silos become rows on the attestation plane. Each table stays, as the
projection of its rows: written by the row's executor in the same
transaction, never by anything else, and rebuildable from the rows alone
(CC 2.4.2, CLM-silo-projection-rebuildable). Readers of the two tables
(list_hard_case_events, get_detection_events, LensCore's SQL) see the
same rows as before.
hard_case:{kind}:v1rows (CC 3.1.9.4,CLM-hard-case-on-base). Every
hard_caseevent is ascoresrow: attester = attested = this node's key,
cohort_scope = self, local tier, the event whole under thehard_case
member, the row id derived fromevent_id(so a re-record is still a
no-op). One pure plan,attestation_admit::plan_hard_case_row, decides it
for all three backends and all emitters: therecord_hard_casedoor, and
the four emitters that record inside their own transaction (the two trace
erasures, the community membership removal, the peer removal), which plan
the row inside that transaction so it commits with the act it records. The
generic local door refuseshard_case:dimensions (InvalidArgument): the
hard_case door is the only writer.- The row's signature is deferred, as on every local-tier row this node
attests (thetrace:complete:v1precedent): the node signs it as the
actor at the crossing. The crossing is held (below), so today the rows sit
at the local tier unsigned. Signing at write is not possible for the
in-transaction emitters (the hybrid signer is async; an erasure's counts
exist only inside its transaction). - A host must hold a node key before any erasure or removal is recorded.
The attester of every hard_case row is the node key (set_node_key_id),
and it must be held infederation_keys(register_self_federation_key).
Without one, every ACT that records a hard_case row is refused before it
mutates anything, with the new typed refusal
Error::HardCaseAttesterUnavailable(kind
federation_hard_case_attester_unavailable, the const
hard_case::refusal::ATTESTER_UNAVAILABLE; reasonnode_key_unsetor
node_key_not_held). Python raises the new
ciris_persist.NodeIdentityRequired(aPersistError). The acts: both
trace erasures (delete_traces_for_agent,
delete_traces_for_agent_id_hash; refused even when they would delete
nothing, store errorstore::Error::HardCaseAttesterUnavailable), the peer
removal (after its ownPeerNotFound/HardRemoveWithActiveAttestations
refusals), therecord_hard_casedoor itself, and the host-run watchers
that call it. I962 shows each refusal and that the same erasure SUCCEEDS
once the node key is held, on memory, sqlite and postgres. An
OBSERVATION riding on another row's admission or a cascade (a membership
change, a liveness transition, a recipient excluded, a fast-path takedown
applied) records nothing and proceeds (hard_case::record_observation), so
a node with no identity still applies a peer's removal and rotates its key.
AnEnginebuilt with a signer sets the node key; a host that never
registered its own key withregister_self_federation_keymust do so. - Unpurgeable, by the family table.
hard_case:joins
migration::ACCOUNTABILITY_RECORD_PREFIXES, the one list both purge doors
and the serve read consult (CIRISConstitution#170, the protective
direction). No door special-cases it. - HELD: hard_case replication. A
hard_case:*row does not enter the
federation tier:crossing::plan_enter_meshand the put door (which is also
the replicated apply) refuse it with the new
Error::HardCaseReplicationHeld(kind
federation_hard_case_replication_held, the const
hard_case::refusal::REPLICATION_HELD; PythonRuntimeError, the posture
ofNoActorSignature: a held door, not a caller fault). Local-tier rows are
never served, so no peer receives one. What unblocks it: CIRISConstitution#170
ruled (hard_case in CC 2.4.1.1's unpurgeable set), and a way to know which
peers carry the refusal (persist has no peer capability advertisement
today). Thenhard_case::check_hard_case_replication_heldbecomes a
per-peer serve filter. - The N4 proof carries both signed witnesses (CC 6.1.1):
witness::equivocation_hard_casenow puts the two conflicting corpus rows,
each with its Ed25519 and ML-DSA-65 halves, underdetail.witnesses. detection:{detector}:v1rows (CC 3.4.8,CLM-detection-on-base).
put_detection_eventwrites a local-tierscoresrow attested by the
detector's key, the detection whole under thedetectionmember (bytes in
base64), the row id derived fromdetection_id; one pure plan,
attestation_admit::plan_detection_row, holds V008's shape gates and the
dimension gate. Thedetection:family is the detector-only reserved
family, so the door now refuses asigning_key_idwhoseidentity_typeis
notlenscore_detector(behaviour change; the caller's hybrid verify of
canonical_bytesis unchanged). The detector signs the row as the actor at
the crossing; a federation-tierdetection:*row a detector emits itself
throughput_attestation(hybrid-signed, replicable) projects too, after
detection_row::check_detection_body, when it is the detection's own row
(the id derived from itsdetection_id). The envelope names the scored
trace asevidence_refs. The re-put verdict (same bytes
no-op, different bytesConflict) reads the held ROW, never the table.
calibration_bundlesstays configuration.- A detection's erasure goes through the purge door (lead ruling: a
signed row is immutable bytes; one tombstone mechanism). The row's
evidence_refsnames its trace, so a steward purge whose selection
reaches the trace derives the detection row ("scores over purged evidence
follow it"). While the purge tombstone stands, the projection row is the
V080 tombstone (linkage NULL,erased_at= the earliest-admitted
execution'sexecuted_at), written bypurge_put_executionin the same
transaction as the tombstone and rebuilt from it
(detection_row::project_detection_row_under);purge_finalizedeletes
the row and its projection row goes with it. I825 shows the window and the
finalize on memory, sqlite and postgres. - The DSAR erasure of a detection hard-deletes its signed row, and no
row remains. Both DSAR doors (delete_traces_for_agent_id_hash,
delete_traces_for_agent) delete each row-backed detection of an erased
trace in the erasure's transaction. The row is found by its own
detection.trace_id, so a row a steward purge already tombstoned goes
too. Never a rewrite. The projection row leaves with it by cascade.- Behaviour change: an erased row-backed detection leaves NO row in
detection_events. v56.0.0 kept a PII-free V080 remnant (linkage
NULL, analytics kept). No persist read door ever served it, so it was
reachable only by direct SQL on the host.detection_events_tombstoned
still counts the erased detection. The remnant was dropped rather than
carried on the erasure's row becauseconformity_payloadand
cohort_cellare opaque consumer JSON: inside an unpurgeable, signed
hard_caserow they could never be erased. - Pre-v57 rows. Before its transaction, each door moves onto the plane
the pre-v57 detections it can attest, so they are erased the same way. A
pre-v57 row it cannot attest (its detector key not held, or not a
lenscore_detector) keeps the in-place V080 NULLing.
detection_events_tombstonedcounts both kinds. - **The Art....
- Behaviour change: an erased row-backed detection leaves NO row in
ciris-persist v56.0.0
[56.0.0] - 2026-10-14
Security — rustls 0.23.46 (RUSTSEC-2026-0346, RUSTSEC-2026-0347)
- Lockfile only: rustls 0.23.45 → 0.23.46. Both advisories were published
2026-10-14 and stopped this release's push at the pre-pushcargo deny
leg. Neither reaches persist's surface (no customClientSessionStore
Debuglogging; ECH is not enabled), so this is the dependency floor
moving, not a behaviour change.
Fixed — tag CI: a restored cache never carries a build-script unit with missing outputs
No library code changed.
- The failure: the v55.0.0 tag CI (run 38380545287) failed twice, the
same way each time, in one job:maturin abi3-py311 wheel (windows-x86_64), withcouldn't read target\release\build\libsqlite3-sys-…\out/bindgen.rs.- rust-cache found nothing, so the CIRISCache restore that won was the
ciris-substrate-v1-windows-x64-release-rustc1.97.0-phead-enone-v20.1.0
blob an earlier tag's wheel job saved. - That save prunes target/ to its budget by deleting the OLDEST-mtime FILES
(cc_prune_lru). It deleted a build-script run's output (out/bindgen.rs)
and kept the unit's fingerprint. Cargo then trusted the run and
include!d a file that was not there. - Main pushes do not run the wheel jobs, so nothing re-saved that blob, and
every later tag would have failed the same way.
- rust-cache found nothing, so the CIRISCache restore that won was the
- The fix, in persist's workflow (no registry deletion, no CIRISCache
change):scripts/ci_cache_units.py.- Save side: before each CIRISCache save (every
CIRISCache/save@v1.1call site, the wheel save included),seal
writes each build-script unit'sout.manifest(every file in the unit,
sorted) and touches itsout/files to now, so a file-by-file prune
drops them last. This step is continue-on-error, because it is an
optimisation. - Restore side: in
.github/actions/ciriscache, after the restore,
repairremoves a build-script unit whose manifest is missing, or names
an absent file, as a whole unit: itsbuild/<pkg>-<hash>/and its
.fingerprint/<pkg>-<hash>/. Cargo then reruns that build script. The
step is NOT continue-on-error, so a target/ that cannot be checked is not
built on. - A unit with no manifest (every blob saved before v55.0.1) is judged
by a bound, not removed outright.- Why: removing all of them cost the v55.0.1 PR run's
secretsleg 272
units and 15 compile minutes, and it timed out. - The prune is a file LRU, so a pruned file is no newer than the oldest
surviving file (cutoff). A build-script run writesout/during the
run andoutputat its end, so a pruned sibling implies
output <= cutoff + R_MAX, whereR_MAX= 2 h bounds a script's
runtime. - A manifest-less run unit whose
outputis newer than that is kept.
One at or below it is removed with its fingerprint, as is a run unit
with nooutput. A script-compile unit is judged by its newest file. - A restore that clobbered every mtime makes everything fall within the
bound, so everything manifest-less is removed, which is the safe
direction. - The repair prints three counts: removed (manifest), removed
(boundary), and kept (unverified).
- Why: removing all of them cost the v55.0.1 PR run's
- Save side: before each CIRISCache save (every
- The linux test legs' step timeout is now 75 minutes (was 50). The
secretsleg runs about 46 minutes warm, and a cold or repaired cache adds
up to 25. A hung test is still caught by name by.config/nextest.toml. - Witness:
scripts/ci_cache_units.py --self-test, run in the hygiene
job. It builds a fixture target/ with a consistent unit, a sealed
build-script unit, a unit pruned of one out file, and a unit saved before
manifests existed. The repair removes the last two with their
fingerprints and keeps the first two, and the seal touches out files to
now. Turning the repair off, or keeping the fingerprint, fails it.- The bound's own legs, which use fixed offsets:
- a manifest-less run unit far above the cutoff is kept, and one within
R_MAXof it is removed; - a run unit with no
outputis removed; - a script unit far from the cutoff is kept, and one near it is removed;
- clobbered mtimes remove everything.
- a manifest-less run unit far above the cutoff is kept, and one within
- Setting
R_MAX = 0, or inverting the boundary comparison, turns legs
red.
- The bound's own legs, which use fixed offsets:
Added — tooling: size tracking, semver-checks, coverage, machete, diff mutants, evidence-pointer gate (#934)
Six instruments beside #934's consolidation (#1076 slice T). Five are
reports: each exits 0 on a finding and every CI job running one is
continue-on-error, so none turns main red without the operator. One is
a gate, the evidence-pointer check, because a dead pointer is a false
evidence claim. docs/TOOLING.md says how to run each and which is which.
- Size tracking.
scripts/size_report.pywritesevidence/size.tsv:
code/comment/blank lines persrc/**/*.rsfile (stdlib only; comment- and
string-aware,#[cfg(test)]-blind), aBACKENDSrow
(src/store/{sqlite,postgres,memory}.rs: 137 779 lines at v55.0.0) and a
TOTALrow (649 639).checkprints the per-file delta against the
committed table or--against <ref>;--fail-if-backends-grew(exit 1) is
#934 step 1's acceptance criterion for the PR that moves a door. certify's
fast stage gainssizetest(the counter's fixture witnesses — a gate, like
the other tool self-tests, also in ci.yml's lint job) andsizereport(the
delta, always green, totals printed under the fast-gate table). - API breaks.
scripts/api_break_report.sh [<prev-tag>]runs
cargo semver-checks check-releaseon the shipped-shape feature set at
--release-type minor, so a major bump does not hide the breaks. v54.1.1 →
v55.0.0: 12 lints, 71 distinct instance lines. New workflowreports.yml
(non-gating):semveron every PR against the latestv*tag reachable
from the base (artefact + step summary), andsize(the PR's delta vs its
base). - Coverage.
scripts/coverage.sh <leg> [-- <nextest args>]:cargo llvm-cov nextestfor ONE certify feature set (derived, never spelled;
postgres sets throughpg_test_db.sh), lcov + summary + html under
target/coverage/<set>/.reports.ymlruns every leg weekly (legs derived
at run time) and on dispatch. - Unused dependencies.
cargo machete --with-metadatain ci-hygiene's new
non-gatingreportsjob. It names two dev-dependencies,hex-literaland
tracing-subscriber, that no.rsfile uses; not removed — the operator's
call. - Mutants on the diff.
scripts/mutants_diff.sh <base> [-- <cargo-mutants args>]:cargo mutants --in-diffover thesrc/lines changed since the
merge base, with a wall-clock budget;reports.ymlruns it only for a PR
labelledrun-mutants, or on dispatch. The hand mutation tables and the
nightly scoped matrix stay the gates. - Evidence pointers (a GATE).
scripts/evidence_pointers.py check:
everysrc/row ofevidence/cc_impl.tsvmust name a definition in its
cited file, with comments and literals blanked;Owner::membermust be a
member ofOwner; a mention, a string or a call site never resolves, and a
moved symbol fails until repointed. Stricter than the in-crate witness,
which let the v54 → v55 dead pointerblobs.rs#holds_bytes_attestation_envelope
live on a doc comment and a string literal. Clean on this tree (223 rows)
and at every tag v46.0.0..v55.0.0; that pointer sat in the Constitution's
register, andregister <claims.tsv>names it on CIRISConstitution
fca367d^. certify fast gateevidenceptr, ci.ymllintstep,
ci-hygieneevidence-pointersjob (daily);evidenceptrccreports the
Constitution register when a checkout is found.
Added — trace_format v2.8.0 admission (#1089, CIRISAgent#1255)
A v2.8.0 trace now admits. Before this, a v2.8.0 trace's run kind read as
absent, so trace_run_kind_mock never fired. The two new profile members
were also dropped on parse, so the re-canonicalised bytes did not match the
signature.
- Where
run_kindis read. A v2.8.0 trace signsrun_kindinside
deployment_profile, the only signed block the agent supplies. Every
run-kind gate now reads the signed member from either place through
CompleteTrace::signed_run_kind: the top level (v2.7.x) or
deployment_profile.run_kind(v2.8.0).- The ingest mock gate on a canonical node reads it.
- The oversize mint copies it into
manifest.run_kind. steward_purge::trace_run_kindreads it, and so do
trace_attestation_run_kindand the federation-tier attestation gate.- The
trace:*inline-form admission check holds
deployment_profile.run_kindto the closed vocabulary. A top level that
disagrees with it is refused. - The canonical reconstruction is unchanged. It lifts only the top-level
member, because the profile member is signed where it sits.
- Typed, signed profile members.
DeploymentProfilegains
run_kind: Option<String>andtrace_format_version: Option<String>. Each
is skipped when absent, so a v2.7.x profile serializes to the same bytes as
before. A v2.8.0 profile round-trips byte-identically through admission. - The format gate keys on
deployment_profile.trace_format_version. This
is a signed member, distinct fromtrace_schema_version, which keeps
selecting canonicalization. The new module isschema::trace_format."2.8.0"validates the whole trace against the vendored
src/schema/trace_format_v2_8_0.json.- A format this node has not vendored is refused by name:
schema_unsupported_trace_format, with the format string asdetail.
The batch is refused whole and nothing is written. - A trace that does not conform is
schema_trace_format_violation, never
truncated. Thedetailis the format plus the first violation's instance
path and schema path, with no payload values. - `deployment_profile.run_k...
ciris-persist v55.0.0
[55.0.0] - 2026-10-12
Changed — CIRISConstitution v1.0-rc7 vendored (#753, slice A0)
namespace_registry.jsonandnamespace_match_vectors.jsonare copied
byte-for-byte from thev1.0-rc7TAG (536b8cb, annotated tag686d274):
168 families (was 158),registry_sha256 796f6099…(wasf666f334…), 1103
dimension vectors and 25 row-type vectors.scripts/check_vendored_cc.sh v1.0-rc7passes.namespace_match_binds.jsonis regenerated from the
reference matcher at the tag.- Families added (11):
accord:lifecycle:confirmed,community:listing,
group:roster,identity:occurrence,key:record,key:revocation,
key_grant:{axis},ledger:head:{unit}:{version},
ledger:checkpoint:{unit}:{version},ledger:promotion:{version},
settlement:*. Retired (1):holds_bytes:sha256:{prefix}(CC 2.4,
CIRISConstitution#141). Changed:accord:*lists the new leaf;
membership:{stage}gainswidening,removal,resignation. The
key_grant:reserved stem ("carrier row type, never a dimension") is gone
and acommunity:stem is gated ("community:listing is the only leaf"). - The
ledger:staging latch is deleted. CC rc7 registers the CC
3.3.10.1 ledger leaves (CIRISConstitution#92/#105), soledger:*rows now
admit at federation tier, under the L1 triple binding and a new owner rule
(check_ledger_emitter_admission): the signer must act for the attested
owner (its key, a live occurrence, or its node binding). The rc7 rows carry
the CC 3.1.3 section defaultsubstrate-self-report; persist follows the
table cell ("the ledger's single owner … or a delegate") and refuses the
delegates_todelegate path until a door reads the ledger's declared
serialization discipline. settlement:*admits (CC 3.3.10). Ascoresrow on any
settlement:*leaf admits from the payer, on the private arm
(cohort_scope: self) and the auditable arm (federation/public), and
the payee'ssettlement:received:*counter-attestation admits too. Its
envelope is scanned byreject_payment_processor_identifiers, the gate the
operational envelopes already run: a Stripe-shaped identifier anywhere in a
settlement row is refused. The rc7 row sayssubstrate-self-report; the CC
3.1.3 table says "Reserved? No" and CC 3.3.10 names the payer, and persist
follows the text.- Held shut until their door:
key_grant:{axis}:v1as a dimension (v55
slice B, #989) andmembership:widening|removal|resignation:v1(#991).
rc6 refused all four; rc7 registers them; persist has no gate that enforces
CC's rule on them yet, soadmission::REGISTERED_FAMILIES_HELD_SHUTrefuses
them at every federation-tier door. - TEMPORARY carrier exception. rc7 empties
_meta.row_types.carriers.
Persist's blob doors and key-grant sets still write the two rc6 carriers, so
registry::RC6_RETIRED_CARRIERS_ADMITTED_UNTIL_989keeps exactly
holds_bytes:sha256:{8 hex}andkey_grant:(content|epoch|stream):v1
admitted as carrier row types, carrier shape still enforced, until v55
slices A and B retire them. A from-disk gate (I701) fails once a 55.0.0 or
later release is dated in this file while the const is non-empty. - Reserved and not yet gated (admitted as before, when they were unregistered
open vocabulary):key:record,key:revocation,identity:occurrence,
group:roster(#991),community:listing(#1053). - Evidence: the
CLM-ledger-standardrow that cited the staging latch now
citescheck_ledger_emitter_admission. - Witnesses I700–I706 (memory, sqlite, postgres).
Adoption — what Edge and Server must handle (#753)
- New refusal reason token
registered_family_held_shuton the existing
federation_dimension_rejectedkind (Error::DimensionRejected), for a
dimension underkey_grant:ormembership:widening:/
membership:removal:/membership:resignation:. Terminal for this release. ledger:*rows now refusefederation_signer_not_acting_for
(Error::SignerDoesNotActFor, plane"ledger", retryable) when the signer
does not act for the attested owner, andfederation_dimension_rejected/
ledger_l1_binding_violationon a binding mismatch, where they were refused
federation_namespace_family_unregisteredbefore.settlement:*rows refusefederation_payment_processor_identifier
(Error::PaymentProcessorIdentifier) on a processor identifier.- Matcher answers that change:
key_grant:{axis}:v1,
membership:{widening|removal|resignation}:v1, the ledger and settlement
leaves,community:listing:v1,group:roster:v1,key:record:v1,
key:revocation:v1,identity:occurrence:v1and
accord:lifecycle:confirmed:v1now resolve; any othercommunity:*leaf
refusesnamespace_family_unregistered;holds_bytes:sha256:*no longer
resolves to a family.authority_forgives the ledger rows and
settlement:*theSubstrateSelfclass (CC's row data). - No new error variant, no new exhaustive enum variant, no FFI or
.pyi
surface change, no migration.
Slice C — doors and takedown (#952, #951, #1007, #1059)
- #952 — the incremental plaintext chunk-DAG write. Two doors replace
buffering a whole commons file:Engine::put_plaintext_dag_chunk(entry, body, author_key_id)stores ONE chunk against its manifest entry (the
one-shot door's per-chunk checks, now the shared
blobs::prepare_dag_chunk_row: an inline body hashes to the entry and is
exactly its size; the WILL decision asked as the one-shot door asks it) and
writes no manifest row and no claim;Engine::finalize_blob_chunks_signing( manifest, author_key_id)checks the manifest is a plaintext v1 DAG, then in
ONE transaction (BlobStorage::put_dag_manifest_over_held, new trait
method, sqlite and postgres) reads every chunk row it names and writes the
chunk_dagrow only over a whole DAG, then emits the manifest's holding
claim exactly asput_blob_chunks_signingdoes (one shared call site,
announce_plaintext_dag). Until finalize the DAG's address is absent to
every read and listing. A missing chunk isblob_not_heldnaming it, with
nothing written. A chunk stored and never finalized is an unclaimed commons
row the retention sweep may evict like any other. - #951 — a sealed DAG is announced at promotion, never at the manifest
adopt.Engine::promote_adopted_manifest_to_daggains a required
disposition: AdoptDisposition. UnderAnnouncethis node's holding claim
for the root (custody:ack:v1here, whose door asks again that every
chunk is held) is emitted once the promotion's checks pass, on a re-run too;
LocalOnlyemits nothing.DagPromotiongainsannounced. The puller
adopts the manifestLocalOnly. - #1007 — takedown on the CC 2.3 tombstone (CC 4.5.3 step 2, CC 4.5.5).
A takedown is now ONE bytes-plane tombstone,blob:{content_sha256},
written on the steward purge's V187 tables in its row shape (one shape for
both bases, one row per record keyed by its digest): the header carries the
notice'slegal_basis,record_count1, the digest of["blob:{sha}"],
the notice's JCS (whose hash is the notice id) and no cosignatures. New
modulefederation::takedown_tombstone:takedown_tombstone(directory, notice, executed_by)verifies the signed
takedown_noticeContribution (the cirisnode track's own signature rule)
and asks CC 4.5.5 of THIS node's state through ONE function,
takedown_authority, whichput_contribution's takedown gate now calls
too: a signed subject of a row binding the content, a named moderator of a
community the content is BOUND INTO here, or a delegate a steward-bound
duty-holder reaches over a livetakedown-scoped chain. The content's
community is read from the held binding rows, never from the notice's
community_id(operator ruling; before v55put_contributionjudged a
takedown under whatever community the notice named). No binding row held
istakedown_content_unbound(retryable: an ordering gap). Every basis
needs a duty-holder (operator ruling; CC is asked whether "from any
claimant" was meant for the immediate-removal bases);- the tombstone withholds the bytes from every read door at once
(blob_purged, naming the notice) and replicates as aPurgeBundle;
apply_replicated_takedownre-verifies the signature and CC 4.5.5 on the
RECEIVER's state and refuses a bundle naming anything but the notice's
own content on the bytes plane (takedown_not_bytes_plane); - per basis (
basis_disposition, CC 4.5.3): an immediate-removal basis
(tvec_terrorist,ncmec_csam,gifct_cip,perceptual_hash_csam,
court_order) settles at once and the Engine destroys this node's bytes
on receipt (locally and on a replica); a counter-notice basis
(dmca_51210 d,dsa_article_1614 d,osa_illegal_content14 d,
community_standards30 d) withholds at once, retracts this node's own
holding claim, andpurge_finalizedeletes after the window; the age-gate
basis writes no tombstone.steward_purge's settle reads the header's
basis throughtakedown_tombstone::settles_at(W = 72 h for
steward_purge, unchanged). An immediate-removal takedown records ONE
hard_case:fast_path_takedownper notice on each node that executes or
applies it (CC 4.5.3 step 4; thehard_case:{kind}family is registered
at rc7, open vocabulary):target_key_idthe notice id,detailthe
content, basis, signer and origin. The fast path waives the CC 4.5.1
quorum and the counter notice, never the duty-holder gate (confirmed by
the CC session; "any claimant" is corrected in rc8 CC 4.5.3 step 1). - A non-holder's
withdrawsof a holding claim is refused at the door,
stamped or not (custody_ack_withdraws_not_the_holder, CC 3.1.3.3 rule 4):
aholds_bytesclaim, and...
ciris-persist v54.1.1
[54.1.1] - 2026-10-12
Fixed — the v54.1.0 tag's Windows wheel job
- The v54.1.0 tag CI failed in one job,
maturin abi3-py311 wheel (windows-x86_64), at the new #1022 cache-trim step (scripts/ci_cache_trim.py).- The trim itself had run. Its summary line prints
→, and a Windows runner's Python stdio is cp1252, so it raisedUnicodeEncodeError. - The wheel had already been built and uploaded. The failure skipped the tag-gated "Sign + register multi-target build manifests" and "Attest wheels, release assets and cc_impl.tsv" jobs, so v54.1.0's GitHub release has no signed desktop manifests or attestations.
- The trim itself had run. Its summary line prints
- The fix:
- The script reconfigures stdout and stderr to UTF-8.
ci.ymlsetsPYTHONUTF8=1for every job.- Each of the five cache-trim steps is
continue-on-error, because a cache optimisation never fails the build it follows.
- Witness:
PYTHONIOENCODING=cp1252 python3 -c "print('→')"raises; after the reconfigure, it prints. - No library code changed. Adopters of v54.1.0 need nothing new; v54.1.1 exists only to produce the signed, attested release artifacts.
ciris-persist v54.1.0
What's Changed
- v54.1.0 — steward purge door under trust-root moderation (CC rc8@8c8f536), typed retryable refusals (AttesterKeyUnknown, SignerDoesNotActFor), InvalidReplaced key repair, consent basis, proposer-minted accord nonce, run_kind, mutation + evidence CI (#1040 #1041 #1042 #1043 #1044 #1046 #1047 #1037 #1038 #1045 #1051 #1026 #1022 #1003 #1052 #1054) by @emooreatx in #1057
Full Changelog: v54.0.0...v54.1.0