Skip to content

Releases: CIRISAI/CIRISPersist

ciris-persist v60.1.0

Choose a tag to compare

@github-actions github-actions released this 27 Oct 14:02
ee30310

[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) (ValueError on an unknown
    token), on the native module and the package.
  • New members. RegisteredStreamEpoch.already_registered and the same
    key in register_stream_epoch_dek's Python JSON (#1069);
    ReackReport.restored and its serialized key (#1113); DeploymentProfile's
    two optional members calibration_version and pipeline_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
    to delegation_scope::ALL (position 24) and INFRA (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 is Ok with
    already_registered: true (#1069). Custody's received is now ordered by
    the subscriber-SIGNED delivery_receipt:{stream_id}:v1 scores row's
    instant, not by a stored v1 receipt's storage stamp (#1006).
  • New refusals, under existing tokens. An identity_amendment:v1 row
    admits only when authorized_by names 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, the user the
    signing key is an occurrence of, through the
    self:delegates_to:agent_occurrence:v1 login anchor); otherwise
    identity_amendment_unauthorized under federation_invalid_argument,
    Python ValueError (#1120). A delivery receipt whose subscriber_key_id
    or stream_id does not fit a u32 length is refused
    (BlobError::InvalidArgument, Python ValueError) (#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, CryptoTier itself 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 a seq or an
    epoch (finalize_blob_chunks_signing, put_blob_chunks_signing), and an
    External chunk whose size_bytes is 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 is Ok with 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:v1 row). 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 proposed StreamEpochDekConflict variant would be
    a new arm of the exhaustive BlobError; 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:

    1. an Engine with no LocalSigner (with_hardware_signer, from_shared)
      refused every immediate-removal takedown at purge_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.
    2. 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.
    3. 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).
    4. 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.
  • CIRISPersist#1006 — custody_view's received was ordered by this
    node's storage stamp on a stored ciris-delivery-receipt/v1 receipt (whose
    signed bytes carry no instant), so a receipt stored late outranked the
    device's later signed none. received is now ordered only by the instant
    the subscriber SIGNED on its delivery_receipt:{stream_id}:v1 scores 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 reverses stream_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_communities does 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) at custody_ack.rs#signed_receipt_at;
    CLM-one-chunk-seal (5.3.3.2.5) at chunk_dag_cascade.rs#holds_this_dek;
    CLM-one-tombstone-row (4.5.3) at takedown_tombstone.rs#already_recorded
    and steward_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 in mutants.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_now and
    Engine::accord_threshold report AccordThreshold { m, n } over this
    node's standing roster (holders whose keys it holds). Python:
    Engine.accord_threshold() -> (m, n) and accord_threshold_for(n). CIRISEdge's
    verify_accord_carrier calls it instead of a hard-coded (2, 3).
    Witness I1360 (sqlite, postgres): the reported m is 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 as infra:serve and read through has_accord_conferred_role;
    no new row kind. APPENDED to delegation_scope::ALL (positi...
Read more

ciris-persist v60.0.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 21:35
8dec0de

[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_ledger hold
    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_at and created_at are both its signed
    instant, and it carries the row's attestation_id (new column). New
    SiloProjection::CreditsLedger / ExpertiseLedger; a pre-V203 balance
    (no attestation_id) is a red in check_projection_rebuild until 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 a takedown_notice:{basis}:v1 row only from a duty-holder over
    the content as this node holds it (takedown_authority, the decision the
    tombstone door re-asks); the steward_purge leaf stays held shut (its
    cosigned door is not built). REGISTERED_FAMILIES_HELD_SHUT narrows from
    takedown_notice: to takedown_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
    retryable federation_attester_key_unknown), holds the row, and its header
    carries the row whole; TakedownReport.notice_attestation_id names it. A
    notice row is about its content, never a binding of it: content_binding
    skips the family. LegalBasis takes CC 3.3.2's wire strings: dmca512,
    dsa_article16 (dmca_512 / dsa_article_16 no 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 retired takedown_notice
    CONTRIBUTION. apply_replicated_takedown still 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. SubjectGate and
    validate_subject_key_ids_at are removed; validate_subject_key_ids is 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_grant Contribution
    (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 carries signature / a contributor id:
    cirisnode_typed_object_retired). New cirisnode::Error::Retired (its
    kind() is the token) and cirisnode::Error::Federation (a plane refusal,
    its kind() the plane's).
  • V203 (sqlite + postgres twins): drops contributions, votes,
    moderation_events, slashing_attestations, reconsideration_requests,
    reconsideration_attestations and V056's two watch tables
    (cirisnode_consent_sla_watch, cirisnode_revocation_promotion_watch,
    nothing read them): 8 tables. Keeps the ledgers (with attestation_id),
    scheduled_takedown_actions, federation_delivery_attestations and
    promotion_attestations; their foreign keys into the dropped table go
    (sqlite: staged and re-created under the final name; postgres: DROP … CASCADE drops only the constraints) and the row-naming columns become
    TEXT on postgres. Rewrites purge_executions.legal_basis to 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; the is_canonical flip is gone
    with the tables); a delivery attestation's announcement is a held
    contribution:federation_announcement:v1 row.

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 ...

Read more

ciris-persist v59.1.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 19:40
32512f5

[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.json is 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_destinations with one row, signed by
    and about ciris-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_quorum counts A1 and B1, and a fresh
    Engine (sqlite) resolves transit_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 as tests/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_admission and #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

Choose a tag to compare

@github-actions github-actions released this 20 Oct 04:38
bb59ea8

[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_commitment sorts 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, so Engine and 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_row with the new reason
    aggregate_source_duplicate (appended to the closed
    malformed_row::ALL); on the store path it rides the new
    store::Error::MalformedRow (kind federation_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
    PutPublicKey op and Python put_public_key) and first contact (a key:record:v1 row offered
    through put_attestation / apply_replicated_attestation /
    ApplyReplicatedAttestation, the proof-of-possession door) — a key whose
    key_id does not end in -<fp> is refused federation_malformed_row with
    the appended reason key_id_fingerprint_mismatch (Python
    ValueError("federation_malformed_row: key_registration: key_id_fingerprint_mismatch")). fp is 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
    with tier_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:v1 admits 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_ids names the node, its owner at asserted_at
    and the agent key that acts on it (a held agent-role key); a NEW grant
    missing one is refused community_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 by attested_key_id; the latest grant after the boundary
    R governs, and a revocation of any grant — the owner's recant of its own
    included — ends every grant at or before R. 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.
  • Admission is structural, never an instant
    (check_carrier_admission):
    • a node or agent identity'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-core strict_majority);
    • otherwise it is refused federation_malformed_row with the appended
      reason carrier_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.
  • Migration at open: every held legacy_framed revocation is offered as
    its carrier. The rehome report gains carriers: 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; and not_carried. Nothing held is rewritten.
  • The one predicate (for CIRISEdge):
    key_plane::occurrence_revoked_by(dir, row) -> Option<(identity, occurrence)>
    recognises a withdraws of an identity:occurrence:v1 row 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_VERSION 3 → 4. New member
    serve_transport_destinations (wire name the same): each seated serve
    node's signed SignedTransportDestination carrying 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.
  • Bundle verification:
    • Done by genesis::check_serve_transport_destinations, run by
      verify_bundle_quorum and at seeding.
    • Every row must be about and signed by a key in that bundle's
      serve_nodes (hybrid over JCS(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.
  • Seeding at first open:
    • genesis::seed_serve_transport_destinations runs after the serve nodes'
      keys are seeded.
    • It goes through the ORDINARY signed transport door, so the row lands
      Rooted and self_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 optional bundle_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_at byte for byte.
    • It adds the member, and its one signing item is the authorization over the
      new digest. It finishes at max(charter M, strict_majority(roster))
      DISTINCT holder authorizations, then runs verify_ceremony_outputs.
    • Refusals: genesis_amend_carried_bytes_changed,
      genesis_amend_quorum_not_met, genesis_amend_input_refused.
    • verify_ceremony_outputs still 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.
      -...
Read more

ciris-persist v58.0.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 22:48
770156e

[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_derive unmaintained
    (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_valid no longer folds halt_latched into valid. A
    constitutional halt 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 them check_reserved_prefix_admission,
    check_revocation_authority, resolve_content_class_flag, serve-tier
    resolution, the trusted-publisher fold and resolve_transit_eligibility all
    failed closed under a latched halt — the live G7 breaker CC 4.2.1.1 names
    (CIRISConstitution b9fc6c0). halt_latched stays on TrustRootVerdict as a
    reported signal beside drill_freshness (a halt must be observable); the
    Python surface is unchanged.
  • Witness: exercise_family_trust_root step (e) now asserts the inverse of
    what it asserted since v24.0.0 — under a latched family halt the verdict,
    its bounded_until and the conferral walk are unchanged, and a resume only
    clears the report; exercise_transit_eligibility gains 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 by CLM-halt-not-a-trust-leg at trust_root.rs#trust_root_valid
    and trust_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).
    A detection:* 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_basis column withdraws, 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: ErasureSummary and DeleteSummary
    gain detection_rows_refused: Vec<ErasureRefusal { record_id, reason }>
    (reason erasure_producer_key_not_held; CC 4.4 erasure_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_since on all three backends; Engine::apply_replicated_purge
    routes an erasure bundle to subject_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:v1 carries
    TraceErasureCounts alone (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:v1 rows that named
    their subject (and their projection rows), pre-v57 silo rows of that kind
    that did, and every art17_erasure header 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: a reconsideration:{grounds}
    FILING naming its target by references_attestation_id is 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 carrying outcome) 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 a licensure:* 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_authority never 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_execution and hard_case::bind_erased_detection_rows
    (clean break, no alias): there is no node-local purge basis for erasure.
    hard_case::trace_erasure_event takes &TraceErasureCounts now.
    ErasureSummary and DeleteSummary are no longer Copy (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 (the trace: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
    baked ciris-canonical-1 seed, 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_revocation is one plan on every backend
    (key_plane::plan_put_revocation + KeyPlaneStore::store_planned_revocation);
    revoked_after only ever lowers (a raising revocation is refused
    RevocationBoundRefusal::BoundRaised, bound_raised); a withdraws or
    recants naming 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 of run_pqc_sweep are removed).
  • identity:occurrence:v1 (attester = the signer, attested = the
    identity), for a hybrid-signed occurrence. An occurrence's revocation is a
    withdraws on its row
    , admitted when the issuer acts for the identity
    (signer_acts_for, CC 3.3.6); it feeds the local
    federation_identity_occurrence_revocations projection. 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...
Read more

ciris-persist v57.0.0

Choose a tag to compare

@github-actions github-actions released this 15 Oct 09:37
d5a8025

[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.rs missing under a
    trusted libsqlite3-sys fingerprint) although the v55.0.1 repair ran: it
    removed two other libsqlite3-sys units 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/ciriscache now
    runs repair --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 --strict in 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, the server feature set) move v20.1.0 → v20.2.0 together,
    and the wheel's Requires-Dist floor to ciris-verify>=20.2.0,<21 in
    lockstep. v20.2.0 adds ciris_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 static transit_kex_pubkeys pair persist stores on the
    transport_destination binding 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 with MemoryMax (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}:v1 rows (CC 3.1.9.4, CLM-hard-case-on-base). Every
    hard_case event is a scores row: attester = attested = this node's key,
    cohort_scope = self, local tier, the event whole under the hard_case
    member, the row id derived from event_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: the record_hard_case door, 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 refuses hard_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 (the trace:complete:v1 precedent): 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 in federation_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; reason node_key_unset or
    node_key_not_held). Python raises the new
    ciris_persist.NodeIdentityRequired (a PersistError). The acts: both
    trace erasures (delete_traces_for_agent,
    delete_traces_for_agent_id_hash; refused even when they would delete
    nothing, store error store::Error::HardCaseAttesterUnavailable), the peer
    removal (after its own PeerNotFound / HardRemoveWithActiveAttestations
    refusals), the record_hard_case door 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.
    An Engine built with a signer sets the node key; a host that never
    registered its own key with register_self_federation_key must 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_mesh and 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; Python RuntimeError, the posture
    of NoActorSignature: 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). Then hard_case::check_hard_case_replication_held becomes a
    per-peer serve filter.
  • The N4 proof carries both signed witnesses (CC 6.1.1):
    witness::equivocation_hard_case now puts the two conflicting corpus rows,
    each with its Ed25519 and ML-DSA-65 halves, under detail.witnesses.
  • detection:{detector}:v1 rows (CC 3.4.8, CLM-detection-on-base).
    put_detection_event writes a local-tier scores row attested by the
    detector's key, the detection whole under the detection member (bytes in
    base64), the row id derived from detection_id; one pure plan,
    attestation_admit::plan_detection_row, holds V008's shape gates and the
    dimension gate. The detection: family is the detector-only reserved
    family, so the door now refuses a signing_key_id whose identity_type is
    not lenscore_detector (behaviour change; the caller's hybrid verify of
    canonical_bytes is unchanged). The detector signs the row as the actor at
    the crossing; a federation-tier detection:* row a detector emits itself
    through put_attestation (hybrid-signed, replicable) projects too, after
    detection_row::check_detection_body, when it is the detection's own row
    (the id derived from its detection_id). The envelope names the scored
    trace as evidence_refs. The re-put verdict (same bytes
    no-op, different bytes Conflict) reads the held ROW, never the table.
    calibration_bundles stays 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_refs names 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's executed_at), written by purge_put_execution in the same
    transaction as the tombstone and rebuilt from it
    (detection_row::project_detection_row_under); purge_finalize deletes
    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 because conformity_payload and
      cohort_cell are opaque consumer JSON: inside an unpurgeable, signed
      hard_case row 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_tombstoned counts both kinds.
    • **The Art....
Read more

ciris-persist v56.0.0

Choose a tag to compare

@github-actions github-actions released this 14 Oct 23:16
b03965c

[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-push cargo deny
    leg. Neither reaches persist's surface (no custom ClientSessionStore
    Debug logging; 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), with couldn'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.
  • 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.1 call site, the wheel save included), seal
      writes each build-script unit's out.manifest (every file in the unit,
      sorted) and touches its out/ 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,
      repair removes a build-script unit whose manifest is missing, or names
      an absent file, as a whole unit: its build/<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 secrets leg 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 writes out/ during the
        run and output at its end, so a pruned sibling implies
        output <= cutoff + R_MAX, where R_MAX = 2 h bounds a script's
        runtime.
      • A manifest-less run unit whose output is newer than that is kept.
        One at or below it is removed with its fingerprint, as is a run unit
        with no output. 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).
  • The linux test legs' step timeout is now 75 minutes (was 50). The
    secrets leg 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_MAX of it is removed;
      • a run unit with no output is removed;
      • a script unit far from the cutoff is kept, and one near it is removed;
      • clobbered mtimes remove everything.
    • Setting R_MAX = 0, or inverting the boundary comparison, turns legs
      red.

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.py writes evidence/size.tsv:
    code/comment/blank lines per src/**/*.rs file (stdlib only; comment- and
    string-aware, #[cfg(test)]-blind), a BACKENDS row
    (src/store/{sqlite,postgres,memory}.rs: 137 779 lines at v55.0.0) and a
    TOTAL row (649 639). check prints 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 gains sizetest (the counter's fixture witnesses — a gate, like
    the other tool self-tests, also in ci.yml's lint job) and sizereport (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-release on 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 workflow reports.yml
    (non-gating): semver on every PR against the latest v* tag reachable
    from the base (artefact + step summary), and size (the PR's delta vs its
    base).
  • Coverage. scripts/coverage.sh <leg> [-- <nextest args>]: cargo llvm-cov nextest for ONE certify feature set (derived, never spelled;
    postgres sets through pg_test_db.sh), lcov + summary + html under
    target/coverage/<set>/. reports.yml runs every leg weekly (legs derived
    at run time) and on dispatch.
  • Unused dependencies. cargo machete --with-metadata in ci-hygiene's new
    non-gating reports job. It names two dev-dependencies, hex-literal and
    tracing-subscriber, that no .rs file uses; not removed — the operator's
    call.
  • Mutants on the diff. scripts/mutants_diff.sh <base> [-- <cargo-mutants args>]: cargo mutants --in-diff over the src/ lines changed since the
    merge base, with a wall-clock budget; reports.yml runs it only for a PR
    labelled run-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:
    every src/ row of evidence/cc_impl.tsv must name a definition in its
    cited file, with comments and literals blanked; Owner::member must be a
    member of Owner; 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 pointer blobs.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, and register <claims.tsv> names it on CIRISConstitution
    fca367d^. certify fast gate evidenceptr, ci.yml lint step,
    ci-hygiene evidence-pointers job (daily); evidenceptrcc reports 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_kind is read. A v2.8.0 trace signs run_kind inside
    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_kind reads it, and so do
      trace_attestation_run_kind and the federation-tier attestation gate.
    • The trace:* inline-form admission check holds
      deployment_profile.run_kind to 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. DeploymentProfile gains
    run_kind: Option<String> and trace_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 from trace_schema_version, which keeps
    selecting canonicalization. The new module is schema::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 as detail.
      The batch is refused whole and nothing is written.
    • A trace that does not conform is schema_trace_format_violation, never
      truncated. The detail is the format plus the first violation's instance
      path and schema path, with no payload values.
    • `deployment_profile.run_k...
Read more

ciris-persist v55.0.0

Choose a tag to compare

@github-actions github-actions released this 13 Oct 10:58
ca6bdf6

[55.0.0] - 2026-10-12

Changed — CIRISConstitution v1.0-rc7 vendored (#753, slice A0)

  • namespace_registry.json and namespace_match_vectors.json are copied
    byte-for-byte from the v1.0-rc7 TAG (536b8cb, annotated tag 686d274):
    168 families (was 158), registry_sha256 796f6099… (was f666f334…), 1103
    dimension vectors and 25 row-type vectors. scripts/check_vendored_cc.sh v1.0-rc7 passes. namespace_match_binds.json is 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} gains widening, removal, resignation. The
    key_grant: reserved stem ("carrier row type, never a dimension") is gone
    and a community: 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), so ledger:* 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 default substrate-self-report; persist follows the
    table cell ("the ledger's single owner … or a delegate") and refuses the
    delegates_to delegate path until a door reads the ledger's declared
    serialization discipline.
  • settlement:* admits (CC 3.3.10). A scores row on any
    settlement:* leaf admits from the payer, on the private arm
    (cohort_scope: self) and the auditable arm (federation / public), and
    the payee's settlement:received:* counter-attestation admits too. Its
    envelope is scanned by reject_payment_processor_identifiers, the gate the
    operational envelopes already run: a Stripe-shaped identifier anywhere in a
    settlement row is refused. The rc7 row says substrate-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}:v1 as a dimension (v55
    slice B, #989) and membership:widening|removal|resignation:v1 (#991).
    rc6 refused all four; rc7 registers them; persist has no gate that enforces
    CC's rule on them yet, so admission::REGISTERED_FAMILIES_HELD_SHUT refuses
    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_989 keeps exactly
    holds_bytes:sha256:{8 hex} and key_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-standard row that cited the staging latch now
    cites check_ledger_emitter_admission.
  • Witnesses I700–I706 (memory, sqlite, postgres).

Adoption — what Edge and Server must handle (#753)

  • New refusal reason token registered_family_held_shut on the existing
    federation_dimension_rejected kind (Error::DimensionRejected), for a
    dimension under key_grant: or membership:widening: /
    membership:removal: / membership:resignation:. Terminal for this release.
  • ledger:* rows now refuse federation_signer_not_acting_for
    (Error::SignerDoesNotActFor, plane "ledger", retryable) when the signer
    does not act for the attested owner, and federation_dimension_rejected /
    ledger_l1_binding_violation on a binding mismatch, where they were refused
    federation_namespace_family_unregistered before.
  • settlement:* rows refuse federation_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:v1 and
    accord:lifecycle:confirmed:v1 now resolve; any other community:* leaf
    refuses namespace_family_unregistered; holds_bytes:sha256:* no longer
    resolves to a family. authority_for gives the ledger rows and
    settlement:* the SubstrateSelf class (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_dag row only over a whole DAG, then emits the manifest's holding
    claim exactly as put_blob_chunks_signing does (one shared call site,
    announce_plaintext_dag). Until finalize the DAG's address is absent to
    every read and listing. A missing chunk is blob_not_held naming 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_dag gains a required
    disposition: AdoptDisposition. Under Announce this node's holding claim
    for the root (custody:ack:v1 here, whose door asks again that every
    chunk is held) is emitted once the promotion's checks pass, on a re-run too;
    LocalOnly emits nothing. DagPromotion gains announced. The puller
    adopts the manifest LocalOnly.
  • #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's legal_basis, record_count 1, the digest of ["blob:{sha}"],
    the notice's JCS (whose hash is the notice id) and no cosignatures. New
    module federation::takedown_tombstone:
    • takedown_tombstone(directory, notice, executed_by) verifies the signed
      takedown_notice Contribution (the cirisnode track's own signature rule)
      and asks CC 4.5.5 of THIS node's state through ONE function,
      takedown_authority, which put_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 live takedown-scoped chain. The content's
      community is read from the held binding rows, never from the notice's
      community_id
      (operator ruling; before v55 put_contribution judged a
      takedown under whatever community the notice named). No binding row held
      is takedown_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 a PurgeBundle;
      apply_replicated_takedown re-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_512 10 d, dsa_article_16 14 d, osa_illegal_content 14 d,
      community_standards 30 d) withholds at once, retracts this node's own
      holding claim, and purge_finalize deletes after the window; the age-gate
      basis writes no tombstone. steward_purge's settle reads the header's
      basis through takedown_tombstone::settles_at (W = 72 h for
      steward_purge, unchanged). An immediate-removal takedown records ONE
      hard_case:fast_path_takedown per notice on each node that executes or
      applies it (CC 4.5.3 step 4; the hard_case:{kind} family is registered
      at rc7, open vocabulary): target_key_id the notice id, detail the
      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 withdraws of a holding claim is refused at the door,
      stamped or not (custody_ack_withdraws_not_the_holder, CC 3.1.3.3 rule 4):
      a holds_bytes claim, and...
Read more

ciris-persist v54.1.1

Choose a tag to compare

@github-actions github-actions released this 12 Oct 04:27
95dd966

[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 raised UnicodeEncodeError.
    • 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 fix:
    • The script reconfigures stdout and stderr to UTF-8.
    • ci.yml sets PYTHONUTF8=1 for 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

Choose a tag to compare

@github-actions github-actions released this 12 Oct 01:57
c03aae6

What's Changed

Full Changelog: v54.0.0...v54.1.0