Skip to content

release(40.0.0): persist v53 + CIRISVerify 19 — one audience resolver, stream-epoch file keys, durability at every tier, device classes, build standing on both planes - #802

Merged
emooreatx merged 19 commits into
mainfrom
release-40.0.0
Oct 4, 2026

Conversation

@emooreatx

Copy link
Copy Markdown
Contributor

Release PR for v40.0.0 (MAJOR from v39.1.0). It is prestage/persist-v53 (bac50f3, 18 commits on main dc1ab8c, built and adopted by CIRISServer as it landed) plus the release commit (version, evidence TSV, docs/RELEASE_NOTES.md § v40.0.0).

Local gates on this tree: lib suite (1865 in two halves), clippy -D warnings on both feature sets, the file/withdrawal/membership integration tests, and the evidence and hash tests on the release commit.

Known, open: #771, #796, #800, #801.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

emooreatx and others added 19 commits October 2, 2026 14:41
Pins both ciris-persist entries to rev b68f694a88fc9e5973fb706584ee176304c0c4c7
(head of CIRISPersist refs/heads/v53). The crate at that rev still reports
version 52.0.1 (persist has not bumped Cargo.toml on the v53 branch), so the
requirement stays `version = "52"` and pyproject is `ciris-persist>=52.0.1,<53`
(the pin-skew check resolves the rev's version). One ciris-verify-core: persist
at this rev still pins CIRISVerify v18.0.0, so edge's verify pins and
src/bundle_gate.rs are unchanged (CIRISEdge#786 waits on verify 19).

Adaptations (all test fixtures; no production call site moved):
- CIRISPersist#973 "bundle only": a new unlabelled delegates_to is no charter
  and no acceptance. Fixture charters now carry `dimension:
  TRUST_CHARTER_DIMENSION`; acceptance edges are built from persist's
  `canonical_community::acceptance_edge_envelope` (bridge.rs
  seed_acceptance_edge) or carry TRUST_ACCEPTS_DIMENSION (mesh_config.rs,
  first_contact_ladder_659.rs, relay_roster_752.rs). Grants stay unlabelled:
  an unlabelled grant still confers.
- CIRISPersist#973 witness_quorum: DEFAULT_WITNESS_QUORUM removed; fixture
  charters declare witness_quorum 0 (silence = 0 = witnessed mode off; a
  declared 1 is refused at the charter door).

rooted_with_uncached only composes persist's trusted_roots_of /
trust_root_valid, which apply the labelling rule themselves; no edge change.
No pinned constant moved: *_hash_pinned and field_conformance are green at the
old values, and edge pins no DirectoryOp digest (ABI still 7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…asks both standing planes (#786), not for release

Pins both ciris-persist entries to rev c9d070af5ce32a98e4691cb097f8bfd55430661d
(CIRISPersist v53 branch: "pin CIRISVerify v19.0.0 on all seven crate pins").
The crate still reports 52.0.1, so the requirement stays `version = "52"` and
pyproject stays `ciris-persist>=52.0.1,<53`. Edge's three verify pins
(ciris-keyring, ciris-crypto, ciris-verify-core) move to tag v19.0.0, as
persist pins them; `cargo tree -i ciris-verify-core` shows one, v19.0.0.

CIRISEdge#786, CC 3.1.2.1 (rc6 22ea349): a reader asks BOTH standing planes
before it finds a pipeline without standing. `bundle_gate::pipeline_blessing`
asks persist's capability walk `capability_roots_to_trusted_root(dir, reader,
pipeline, "infra:attest")` first (Delegation / FamilyQuorum map to
`PipelineBlessing::conferred`; the walk's AccordCoScrub arm maps to
`accord_role`, as CIRISRegistry 18a138c does), and only if it finds nothing
`admission::is_infra_attest_effective` (`PipelineBlessing::accord_role`).
Neither -> `BundleGateRefusal::PipelineWithoutStanding`. The pre-19
`(pipeline_record, accord_anchors)` path is gone, with `NoAccordAnchors`
and `MalformedPipelineRecord`: verify's co-scrub copy could not see a quorum
role withdrawal.

The walk is reader-relative, so `RootingDirectory::verify_peer_build_bundle`
and `gated_save_provenance` take the reader's key id; the reticulum transport
passes its `local_key_id` (AnnounceCtx gains it; EventCtx already had it).
`bundle_pipeline_key_id` reads the manifest's signed `row.attesting_key_id`
(verify 19 has no top-level attester in the envelope).

Verify 19 producer changes, test fixtures only: `sign_build_manifest_contribution`
takes `manifest_size` and a DateTime; `produce_build_attestation_bundle` takes
`presents: PresentedBuild::SelfVerify` and a DateTime. No production edge code
called them, nor `to_attestation_entry`, nor matched exhaustively on
`BundleRejection` / `ManifestRejection`.

Witnesses (bundle_gate lib tests):
(a) walk-conferred plain pipeline, reader accepts root-r -> Verified with
    Conferred{Delegation}; the same rows read by a stranger reader -> refused;
(b) ceremony co-scrub only, walk None -> Verified with AccordRole;
(c) neither plane -> PipelineWithoutStanding, the save downgrades;
(d) co-scrubbed pipeline whose infra:attest the quorum withdrew: the stored
    row and accord anchors the pre-19 path verified are unchanged, persist's
    effective read says no -> refused.

No pinned constant moved (hash_pinned / field_conformance / policy_hash green).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…e v3), not for release

Moves the persist pin c9d070af -> ee5e2106 (crate 52.0.2, which carries
v52.0.2's CIRISServer#705 pool-connector fix) and the pyproject floor to
ciris-persist>=52.0.2,<53. Verify stays at v19.0.0 (one ciris-verify-core).

Compile-forced adaptations only:
- R2a (V174): Community/Family literals carry empty prev_head_digest and
  charter_digest (founding records and fixtures; chat's builder only founds).
- R1 (CC 3.2 T3): pre-rotation commitments bind CommittedKey (key id and both
  public keys); fixtures commit to the successor's registered record or to
  persist's test_committed_key.
- #969: ChunkRef.epoch is None in clear manifests and fixtures;
  KeyGrantAxis::Stream wakes nothing yet (CIRISEdge#797 adds the wake).

Tests whose premise #969 changes (an epoch terminator chunk per stream,
stream-epoch grants, v4 DAGs the puller does not adopt yet) are ignored
against CIRISEdge#797: 4 in chunk_grants_779_tests, 9 integration tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…he holds_bytes carrier exactly

CIRISEdge#797 (persist v53 at ee5e2106, CIRISPersist#969 — one DEK per
(stream, epoch), manifest v4, a zero-length terminator per epoch at
seq 2^62 + epoch):

- key_grant:stream:v1 wakes DAGs parked on a stream epoch
  (PullSink::key_grant_admitted -> KeyWaits::wake_stream, Awaiting::Stream).
  Routing and emission were already axis-generic (prefix router,
  emit_pending_key_grants / dirty_axes).
- The puller adopts each chunk at its manifest epoch (v4), or the one-shot
  label (v2); an adopt batch never crosses an epoch.
- Readiness: #787's per-chunk get_at_rest_grant loop is replaced by ONE
  Engine::sealed_dag_readiness call for v2 and v4 (persist I314c); the pull
  parks on exactly what `missing` names.
- blob_chunk_key_not_yet_granted is the wait, never NotGranted: the read
  side maps it to GroupContentError::ChunkKeyPending /
  UnopenedReason::AwaitingKey (is_pending), naming the chunk and the key;
  a promote that returns it parks as DagAwaitingKey. The edge-side
  refused_chunk probe (wrong under v4: chunk rows hold no wrap) is gone.
- Terminators: chunk counts, layout (empty extents at the file's end),
  FileRow::chunks (skips them), the serve membership set (members) and
  receipt K (= persist's STH tree size, terminators included) checked.

CIRISEdge#791: is_holds_bytes_type matches ^holds_bytes:sha256:[0-9a-f]{8}$
whole and byte-exact at the three meaning.rs sites; 16-hex fixtures fixed.

The 13 #797-ignored tests run un-ignored. New witnesses: a two-epoch v4
DAG (test-only producer epoch roll) pulled with epoch 1's set late, woken
by it, read and range-read across its terminators, one receipt with
K = 7; the pending-key refusal; the matcher; the pure adopt/park rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…evice_class (#799)

provision_engine_occurrence compared only the content-KEM pubkeys, so a node
first provisioned `server` (every node the server ever provisioned, phones
and desktops included) stayed `server` forever; persist S1 derives the
replication audience from device_class, and a server-class node gets no
self/family content.

- Same pubkeys, another class: re-sign and publish under the new class
  (same keys, newer asserted_at, the stored valid_until carried) and report
  Provisioned::Reclassed { from, to }. persist's signed upsert is
  last-signed-wins on device_class (sqlite.rs ON CONFLICT ... WHERE
  excluded.asserted_at > stored), on this node and on a peer.
- Drifted (other keys) is still never touched.
- device_class is checked against persist's closed set
  (types::device_class::is_valid) at both doors before anything is written.
- Provisioned is #[non_exhaustive] (MAJOR, in the v53 cut).
- ensure_content_occurrence had the same gap: an unsigned row is re-written
  under the new class (every other column carried); a signed row is refused
  by name, since the local door never mutates one and only its signer can
  re-issue it.

Witnesses (lib): server -> phone is Reclassed on the node's plane and on a
peer's, a second call is AlreadyCurrent with no asserted_at churn; the
expiry survives; the local door reclasses, never writes drift, refuses a
signed row and an unknown class. Each fails with its hunk reverted.

Closes #799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…the signed plane (#799)

d547bbc reclassed ensure_content_occurrence's rows through the trusted-local
door only. On an actor/node split (CIRISServer provision_with) the wire key's
occurrence is usually SIGNED: the identity publishes it at login
(self_at_login), and the local door never mutates a signed row. So the split
path would have refused, with no way to move the class.

- ensure_published_content_occurrence(backend, signer, identity, class, enc):
  the signed door for an occurrence keyed by a key other than the engine's
  (occurrence = the signer's derived key; persist's
  publish_signed_content_only_occurrence, lifted by its owner binding).
  provision_engine_occurrence's rules exactly: Created publishes;
  AlreadyCurrent heals only an off-plane row; Reclassed re-signs under the
  new class, valid_until and hardware_attestation carried; Drifted is never
  touched.
- Both signed doors now share one plan (publish_plan), so the engine path and
  the wire path cannot decide differently.
- ensure_content_occurrence's refusal of a signed row names that door.

Witness (lib): a wire occurrence on the plane as `server` with an expiry is
reclassed to `phone` by the wire key's signer (same keys, expiry carried,
newer signature, a peer admits it); a second call re-signs nothing; other
keys are Drifted and untouched; the local door refuses the signed row and
points here. Each rule fails the witness with its hunk reverted.

Refs #799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…rve gate; membership planes gated (#761)

Persist pin ee5e2106 → 8f9d2d61 (v53 S1, CIRISPersist#963; crate still
52.0.2). Re-pins REPLICATION_POLICY_HASH (1860451c…, KindPolicy.audience)
and CONSENT_GRAMMAR_HASH (4d473eac…, per-node `cohorts`).

- Attestation audience gate (advertise + fetch twin): the row half is
  persist's `may_receive(peer, row)`, memoized per sweep on the fields it
  reads; the #955/#756 membership-ceremony arms stay ORed on top.
- Group records and the five membership planes (revocations, widenings,
  listing): per row, per peer, persist's `may_receive_group_plane` on the
  advertise and the fetch twin, over one (scope, group, named) reader
  (`replication::group_plane`); an unbound requester gets only public
  groups' rows. Retires the `public_group` stand-in and the held-invite
  memo.
- A `key_grant:` set is no longer first-party to a device it names: fetch
  and subject-Pull ask `may_receive` (persist I397).
- SERVE_ADVERTISE_POLICY_HASH re-pinned e3070d53… → e7b1ba86….
- Fixtures: device classes match what each models (personal devices that
  hold self/family content are phone/laptop; hosts and forwarders stay
  server); runtime-node fixtures publish their own signed occurrence.
- Witnesses: #799 reclass under S1, a membership-plane row reaching a live
  invitee and not an outsider, a server-class owned node getting rooms but
  not self/family, advertise/fetch agreement; the #762 public-group test
  now uses an authorized infrastructure community.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…lassed by the owner (ReclassNeedsSigner)

Reverts 6730bcb. Occurrence rows are owner-signed; a node re-signing its own
occurrence with its wire key is a trust change persist and the server both
rejected. A signed row whose class differs is left for its signer to
re-issue (`Provisioned::ReclassNeedsSigner`, next commit).

The S1 fixture edits that rode on 6730bcb's files are kept (the device
class parameter and the #799 S1 witness in chunk_grants_779_tests).

Refs #799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…sSigner, not an error

`ensure_content_occurrence` refused a SIGNED row with the same keys under
another `device_class` with Err. The server's split-node wire-key
occurrences are signed rows whose signer is the OWNER (the `self_at_login`
fold), and the server provisions on every self-room tick and before
drive/chat writes, so the Err meant the node never provisioned and every
self write was refused. It now returns the non-error
`Provisioned::ReclassNeedsSigner { from, to }`, leaves the row untouched
(only its signer re-issues it), and logs at WARN once per
(identity, occurrence, from, to). The witness
`the_local_door_reclasses_and_never_touches_drift_799` asserts the variant
and that the active and signed rows are unchanged.

Refs #799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…legacy v2 writer)

Persist 8f9d2d61 → 32fe46f9; the crate is still 52.0.2 and both pinned
hashes are unchanged. What it forces on edge:

- S3 (#963, CC 6.1.5.3): `projection_for(FountainContent, self | family)`
  is now `Cohort`, not `SelfOwn` (self/family bytes hold-and-forward
  within their audience). `swarm::scope` pins move with it; an outsider
  is still withheld (`PeerNotInRoster`, now `projection: "cohort"`).
- S2 (#942, CC 3.1.3.3, `custody:ack:v1`): `blob_custody.copies_observable`
  is true at every tier; the e2e custody test and the `FileRow::custody`
  doc move with it.
- I397b–d: a device re-classed into its owner's self audience is a
  newcomer to the self keys the receiving node holds. The #799 S1 witness
  now also asserts the self file published while the phone was labelled
  `server` opens on it after the reclass (the gap reported on the S1
  adoption is closed).

Refs #761, #799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…R2c backdated-lag fix)

Persist 32fe46f9 → 0df60dcf, four commits: `write_legacy_v2_dag` takes the
caller AAD a v52 writer bound (edge's `content_aad`), and R2c's backdated
roster row no longer escapes the lag (no edge surface). Crate still
52.0.2; REPLICATION_POLICY_HASH and CONSENT_GRAMMAR_HASH unchanged.

Refs #797

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…ther device (#797)

End-to-end witness over persist's one v2 writer (`write_legacy_v2_dag`,
test-anchor): A (alice's laptop) writes a v52-shape per-chunk-keyed file,
manifest and chunks sealed under edge's `content_aad` of the row that names
it, and emits its key-grant sets. B (her phone, personal class: under S1 a
server-class node gets no self keys) pulls through edge's DAG path with
chunk 0's content grant held back: the pull adopts the manifest and every
chunk and parks on that grant; the retry ladder runs out; the grant's
arrival wakes the pull, which promotes. On B the manifest is v2 with no
chunk epochs, readiness reads readable on content keys, and whole and range
reads equal the plaintext.

Fails without edge's v2 handling: with `awaiting_of` not parking a v2 DAG
on its chunks' content grants, the grant never wakes the pull ("the parked
v2 DAG was never re-pulled").

Refs #797

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…membership ceremony; pin persist 8fcbeb9e

Persist 8fcbeb9e closes the S1 gaps: may_receive routes a
membership:proposal to every node whose principals include the invitee,
an acceptance/decline to the proposer's nodes (through the held proposal
it answers), and every stage to the group's membership-plane audience;
affiliations is a room's membership plane (live_invitees_of included);
is_public_group accepts a Rooted keyless trust-root community.

Edge's own ORed checks (peer_is_proposal_invitee, peer_is_reply_proposer,
proposal_invites_peer and their owner/principal/proposal memos) are
removed from audience_withholds. The first-contact carve stays, as the
TRANSPORT half only: under Reach::FirstContact a ceremony row at a
family/community audience that may_receive admits as RefersTo crosses
the reach and skips the Rooted floor; who it is addressed to is persist's.
MayReceiveKey now keys on references_attestation_id, which persist's
answer arm reads.

SERVE_ADVERTISE_POLICY_HASH re-pinned e7b1ba86 -> 68c5298b (the
Attestation cell's text). REPLICATION_POLICY_HASH and
CONSENT_GRAMMAR_HASH unchanged at 8fcbeb9e.

Witnesses: an affiliations-placed proposal makes a live invitee of the
room; a Rooted keyless ciris-canonical record reaches a stranger and an
unbound requester while an unrooted squat does not.

Refs #761

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…s (CC 6.1.5.3)

Persist v53 S2/S3 (custody:ack:v1, FountainContent projecting Cohort at
self/family, durability_mode / durability_deficit) adopted on edge's
existing paths:

- A completed self/family pull (inline or DAG, the receipt hook's two
  sites) files this node's `here`. For a chunk DAG `here` means the
  manifest and every chunk; edge checks that itself through the
  readiness door under the row's AAD, because persist's
  custody_ack_input_for opens the manifest without it and refuses an
  edge-sealed DAG as an AEAD mismatch. The row is persist's
  custody_ack_envelope, emitted by the engine's self-signing door, and it
  replicates through the existing audience gate (may_receive) only.
- BlobPuller::durability_sweep (on its own cadence, default 15 min):
  over each principal's self room and active families (files::in_room),
  persist's deficit per file (content_audience + deficit_over, read from
  the directory, so a device without the bytes still computes it);
  re-files a lapsing `here` (24 h), corrects a lost copy to `none`, and
  returns the repairs rarest first; the run loop dispatches them.
- The self/family holder rung adds the deficit's live `here` nodes to the
  author's, so a repair pulls from any cohort holder.
- A promoted sealed DAG missing a chunk is no longer AlreadyHeld: the
  walk re-fetches the chunks the readiness door names and reports Stored
  once whole. persist 8fcbeb9e refuses re-adopting the identical
  (seq, sha) at the surviving stream position ("seq N already exists"),
  so the repair stops there today; the full witness is ignored with that
  reason.
- Fountain holdings: self/family announcements answer from persist's
  resolve_projection_recipients(FountainContent, ..., group key) instead
  of the address table; the same answer admits an inbound holding claim
  (signer and peer_id), so a claim from outside the cohort reaches
  neither the converged view nor the converger. A self/family content
  with an audience under N + K = 26 is Full: never ejected, RepairNeeded
  below the audience size; the tuple governs at 26 and above.

Table-roster witnesses that used a family group move to a community
group (the tier that still reads the table).

Refs #763

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…'s AAD; lost chunks and whole lost files repair (#763); persist v53.0.0

Pin persist at tag v53.0.0 (39687ec9; source identical to 973de4f9),
crate 53, pyproject floor ciris-persist>=53,<54.

- file_custody calls Engine::put_custody_ack with the row's content_aad
  and the row's cohort; edge's own completeness check, custody_ack_envelope
  build and emit_attestation_self are gone. Persist's door refuses a DAG
  not held whole (custody_ack_here_dag_incomplete) or a manifest that does
  not open under the data (custody_ack_here_seal_did_not_open).
- A lost chunk is repaired: persist re-adopts the identical (seq, sha) at
  its kept position. The refusal-asserting witness now expects Stored, and
  a different chunk at that position is still refused and stores nothing.
  a_chunk_repair_refiles_here_for_the_whole_dag_763 is un-ignored and goes
  through the sweep: none, repair, Stored, a new `here`, byte-identical.
- Whole-file loss: the sweep reports none and repairs the file rarest
  first, and it reads whole with no key_grant set re-applied (eviction
  keeps the at-rest grants).
- A withdrawn file reads Withdrawn for a grant-holder and the sweep does
  not repair it. A device that filed `here` still reads it: persist's
  tombstone fold counts the custody report as a live binding (persist gap,
  pinned by name).
- A wrong-AAD read of a v4 DAG, whole and by range, is SealMismatch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…withdrawn file is refused to peers and never reported `here` (#763)

CC 2.3 at the bytes plane. A device's custody:ack:v1 `here` report cites the
blob in evidence_refs. Edge counted it as a reference, so after the owner
withdrew a file, the device's own report kept the revocation register Live
and PersistBlobChunkSource::read_chunk served the withdrawn DAG's chunks to
peers. The register is the only chunk guard until persist links chunks to
their manifest (CIRISPersist#979).

- meaning: is_custody_report (persist's CUSTODY_ACK_DIMENSION) and
  is_possession_row (holds_bytes or custody report). BlobMeaning::project
  refuses a custody report as PossessionIsNotMeaning; both referenced_shas
  return nothing for one.
- revocation: observe, the withdraws target and the binding-list completion
  skip possession rows (persist's v53.0.0 binding list includes custody
  reports).
- durability::file_custody refuses `here` for a withdrawn or recanted row
  (files::row_lifecycle, the drive's composer predicate), on the post-pull
  report and the daily re-file alike.
- blob_serve_refusals["withdrawn"] (observability::BLOB_SERVE_REFUSED_WITHDRAWN)
  counts serves refused Withdrawn at the BlobChunkFetch responder.
- Every production PersistBlobChunkSource (edge_node) arms the register.
  Refs #771 (whole-DAG eviction on withdrawal stays open).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
…anding at every use (#793); BundleGateRefusal is non_exhaustive; #786 witness (d) runs the pre-19 path

PeerBundleStore cached a VERIFIED verdict by the bundle's bytes, so a later
withdrawal of the pipeline's infra:attest, or the reader no longer accepting
the root, never reached a cached peer: its Rooted saves stayed Rooted until it
re-registered different bytes. Standing is not a property of the bytes
(CC 3.1.2.1).

- The cache keeps the bytes' verification and the blessing it ran under,
  for the reader that asked. At each use, gated_save_provenance re-resolves
  the standing through the new RootingDirectory::pipeline_standing
  (pipeline_blessing, both planes): the same blessing is Rooted, none is
  Advisory and drops the entry, and a different one re-verifies.
  note_verified takes the reader and the verdict.
- BundleGateRefusal is #[non_exhaustive]. There are no exhaustive matches
  outside its own Display.
- #786 witness (d): the withdrawn-pipeline test now runs verify 18's
  co-scrub standing check, reconstructed from verify 19's unchanged public
  primitives, over the same directory rows. It roots the withdrawn pipeline
  and refuses a plain one (control), while neither CC 3.1.2.1 plane admits
  it and the gate refuses.

Closes #793

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gxb3KeL2LhGdoiAuZmknNm
… wheel floor >=53.0.1; B reads a withdrawn file as Withdrawn (#763)

persist v53.0.1 (85fa53a4) fixes the shared content-binding predicate: a
custody:ack:v1 row never binds content, so a device that filed `here` for a
file no longer keeps it Live after the file is withdrawn. The withdrawn-file
witness's B leg flips from pinning the v53.0.0 leak (B reads Ok; its own ack
is a binding) to asserting the fix: B's ack is not a binding, the fold is
Withdrawn, and B's read is Withdrawn like A's. The wheel floor rises to
53.0.1 so edge never runs on the leaky 53.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…, stream-epoch file keys, durability at every tier, device classes that follow the hardware, build standing on both planes

MAJOR from v39.1.0. persist tag = "v53.0.1" (85fa53a4), version 53, wheel
floor >=53.0.1,<54; CIRISVerify v19.0.0. REPLICATION_POLICY_HASH 1860451c…,
CONSENT_GRAMMAR_HASH 4d473eac…, SERVE_ADVERTISE_POLICY_HASH 68c5298b….
Evidence TSV at ciris-edge@v40.0.0. Notes: docs/RELEASE_NOTES.md § v40.0.0.

Closes #761, #763, #786, #791, #793, #797, #799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 56e5844b5e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/swarm/runtime.rs
Comment on lines +786 to +789
if !self
.claim_admission(&claim.content_id, &claim.peer_id)
.await
.is_announced()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce cohort claim admission without a scope table

When the runtime is created through FountainSwarmRuntime::start or default SwarmRuntimeOptions, scope_table is None; in that configuration claim_admission reaches HoldingsScopeGate::admits, whose unarmed early return is Announce. Consequently this new check still accepts an outsider's self/family claim into ObservedClaims, allowing it to affect rarity and repair decisions. The self/family path now resolves its audience from the directory and does not need the address table, so it must not inherit the legacy table-arming bypass.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in v40.0.1 (PR #804, commit 68df857), with a witness that fails without the fix (or, for the unread-audience case, by code review: no failing-directory double exists).

Comment thread src/swarm/runtime.rs
Comment on lines +1290 to +1296
Err(e) => {
tracing::debug!(
content_id,
error = %e,
"swarm_runtime.converger: the cohort audience is unreadable — the tuple governs"
);
None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep cohort content when audience lookup fails

For self/family content, a transient audience_nodes directory error returns None, which is indistinguishable from tuple-mode or non-cohort content to converger_tick; the code then runs the generic should_eject_with_diversity path and may emit an eviction for content whose actual policy requires every audience node to retain a full copy. On an unreadable audience, the safe behavior is to skip eviction/retain the copy rather than silently falling back to the tuple.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in v40.0.1 (PR #804, commit 68df857), with a witness that fails without the fix (or, for the unread-audience case, by code review: no failing-directory double exists).

Comment thread src/swarm/runtime.rs
Comment on lines +1451 to +1454
if let Some(audience) = full_holding_audience(holdings, directory, &content_id).await {
if let Some(sink) = sink {
sink(if observed_count < audience {
SwarmEvent::RepairNeeded {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare the full-holding target with actual holders

For a below-threshold self/family audience, audience is the total number of audience nodes, but observed_count is weighted_holder_equivalents: normal inbound SignatureOnly claims contribute only half a holder, and the local node is absent because publishing skips self. Thus even when every audience device is holding and claiming the content, the count remains below the audience size and this branch continually emits RepairNeeded; use a holder count in the same units that includes the local holding, or derive completion from custody reports.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in v40.0.1 (PR #804, commit 68df857), with a witness that fails without the fix (or, for the unread-audience case, by code review: no failing-directory double exists).

Comment thread src/swarm/runtime.rs
Comment on lines +1451 to +1454
if let Some(audience) = full_holding_audience(holdings, directory, &content_id).await {
if let Some(sink) = sink {
sink(if observed_count < audience {
SwarmEvent::RepairNeeded {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve hard deletion for revoked cohort content

When a self/family audience is below the feasibility floor, this branch ignores the previously computed consent and unconditionally continues after emitting RepairNeeded or Keep. If an authorized withdrawal marked the content Revoked but immediate eviction was unwired or failed, the generic policy's EjectHardDelete is explicitly the retry path; this early continue prevents that retry forever and can instead request repair of withdrawn content. Handle ConsentState::Revoked before the full-holding branch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in v40.0.1 (PR #804, commit 68df857), with a witness that fails without the fix (or, for the unread-audience case, by code review: no failing-directory double exists).

Comment thread src/blob_swarm/pull.rs
None => std::future::pending::<()>().await,
}
} => {
let sweep = self.durability_sweep().await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move durability sweeps off the pull receive loop

On each durability tick, the sole puller loop awaits the entire sweep before it can receive new rows, process due retries, or dispatch key-woken DAGs. A sweep can perform thousands of sequential directory/blob reads, so on a large drive or slow backend the bounded 256-entry PullSink fills and its nonblocking offer path permanently drops subsequent pulls until those rows happen to be offered again; retries and newly arrived grants are also stalled for the sweep's duration. Run the sweep concurrently or incrementally so the receiver remains serviced.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in v40.0.1 (PR #804, commit 68df857), with a witness that fails without the fix (or, for the unread-audience case, by code review: no failing-directory double exists).

Comment thread src/blob_swarm/pull.rs
Comment on lines +1615 to +1617
cursor = page.resume;
if cursor.is_none() || read >= MAX_FILES {
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resume durability scans beyond the file cap

Once read reaches MAX_FILES, the scan breaks without retaining a resume cursor, while every later sweep starts each room again from None. A room containing more than 4096 files therefore has all rows after the first 4096 permanently excluded from custody refresh and repair; with multiple rooms, once the shared counter reaches the cap, subsequent rooms repeatedly process only their first page because the outer loop continues with read already over the limit. Persist a rolling cursor or otherwise rotate the bounded scan so every file is eventually visited.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in v40.0.1 (PR #804, commit 68df857), with a witness that fails without the fix (or, for the unread-audience case, by code review: no failing-directory double exists).

@emooreatx
emooreatx merged commit 468bf11 into main Oct 4, 2026
26 checks passed
emooreatx added a commit that referenced this pull request Oct 4, 2026
…804)

* fix(swarm,blob_swarm): six durability defects from the #802 review (#763)

- A self/family holding claim is decided by persist's cohort audience
  before the scope table's arming check: on a runtime with no table, the
  unarmed Announce admitted an outsider's claim into rarity and repair. With
  no directory, the holding is withheld (CC 5.2), never announced to an
  unknown peer. The v18 'no table means open' pin now covers only the tiers
  the table governs.
- The converger's holding target is a CohortTarget { Full(n), Tuple,
  Unknown }: an unreadable audience keeps the content this tick, rather than
  falling through to the tuple's eviction.
- Full holding counts each signed holder once, plus the local copy, not the
  #582 weighted equivalents (those guard deletes), so a fully held content is
  kept, not repaired forever.
- A Revoked cohort content skips the full-holding branch, so EjectHardDelete
  stays its retry path.
- The durability sweep runs as its own task (one at a time, aborted on
  exit), so the puller keeps receiving rows, retries and key wakes.
- The bounded sweep rolls: it saves its room and page cursor and resumes
  there, so a room past MAX_FILES is eventually visited in full.

Witnesses (each fails with its fix reverted):
a_cohort_claim_is_refused_from_an_outsider_without_a_scope_table_802,
a_fully_held_cohort_content_is_kept_not_repaired_802,
a_revoked_cohort_content_is_not_kept_by_the_full_holding_branch_802,
the_durability_pass_rolls_past_its_file_budget_802.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

* release(40.0.1): six durability fixes from the v40.0.0 review (#763)

PATCH, same pins (persist v53.0.1, verify v19.0.0), no API change. Evidence
TSV at ciris-edge@v40.0.1. Notes: docs/RELEASE_NOTES.md § v40.0.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant