Skip to content

feat(blob_swarm): serve gate on persist's chunk→manifest link; withdraw-and-evict witnesses (#771) - #812

Merged
emooreatx merged 5 commits into
mainfrom
feat/771-dag-link
Oct 7, 2026
Merged

emooreatx merged 5 commits into
mainfrom
feat/771-dag-link

Conversation

@emooreatx

@emooreatx emooreatx commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Adopts persist #979's chunk→manifest link at edge's serve gate (CIRISEdge#771). This is not released: no version bump and no tag.

What replaced what

Witnesses (each one fails on main or on a named mutant)

Witness Failing run
A forged pointer widens no DAG (persist I485 shape) main: left: "bytes", right: "ChunkNotInNamedDag"
Two-node withdraw-and-evict: armed, warm and cold doors plus the wire, then eviction main: ["Withdrawn", "bytes", "bytes"] for the chunk shared with a live DAG
Same, with the named-DAG fold removed the same assertion fails
(b) a withdrawal only the register knows register check disarmed: left: "bytes", right: "Withdrawn"
A linked DAG refuses a non-member chunk with zero walks, through a direct door and through the builder-attached bag linked-non-member return removed: walks left: 1, right: 0
A link-less sealed DAG serves every chunk through the walk, and each serve is counted (control)
A whole blob named as the DAG is refused with zero walks chunk_dag head check removed: left: 1, right: 0
An unreadable binding state fails closed (attestation table renamed for one request) warn-and-continue restored: left: "PolicyDenied", right: "ChunkNotInNamedDag"
family_files_wire_736 leg (d): F's blob store holds no family bytes (CIRISConstitution#141) a mutant that copies the family blob rows into F fails on F's head for the inline file

The two-node witness runs on the real A-to-B pull. After the withdraw, it asserts on stored state. The manifest and every chunk X holds alone leave B. The shared chunk stays, and so does all of the live DAG. The relation rows stay too.

Gates

  • lib tests in three parts, --skip swarm --skip replication, swarm and replication: ok (1211, 240, 465).
  • blob_federation_e2e (41), family_files_wire_736 (2), self_dag_field_path_717 (1), delivery_receipts_738 (4), chat_message_federates (30), scoped_body_identity_link_718 (3): all ok.
  • All three CI clippy sets with -D warnings: clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL

emooreatx and others added 5 commits October 5, 2026 22:37
…hunk→manifest link (#771)

Persist v53.1 (#979) writes a chunk→manifest relation (V176) at seal and
promote. PersistBlobChunkSource now asks it instead of keeping its own:

- The #766 per-file membership cache is deleted, and so is edge's stream
  walk for every DAG persist has linked. `Engine::dag_contains_chunk`
  answers membership. A linked DAG that does not hold the chunk answers
  ChunkNotInNamedDag, whatever any row's pointer names. Before this a row
  referencing X whose pointer named Y's stream made the gate serve Y's
  chunk under a request naming X (persist I485's shape, on edge).
- The named DAG's own fold (persist `binding_state`) is read first. A
  withdrawn DAG's chunk answers Withdrawn on every door, armed or bare,
  warm or cold, even when the chunk is shared with a live DAG. Before
  this an unarmed door served the shared chunk under the withdrawn name.
- A plaintext DAG, which persist does not link, is still judged by its
  clear manifest.
- A sealed DAG with no link (sealed or pulled before persist v53.1.0, never
  re-promoted) keeps the pre-#771 stream reading, uncached, as a legacy
  fallback until persist backfills its link.
- The revocation register stays armed and is documented at the serve site.
  It is the only refusal for a withdrawal persist's fold cannot see (a
  reference by BlobPointer only).

Witnesses in self_dag_field_path_717: the forged pointer (fails on main:
"bytes" where "ChunkNotInNamedDag" is expected); A withdraws X and B's
door refuses every chunk Withdrawn on the armed, warm and cold doors and
over the wire, then B's eviction removes the manifest and every chunk X
holds alone, and keeps the chunk shared with a live DAG plus all of that
DAG (fails on main: ["Withdrawn", "bytes", "bytes"] for the shared chunk);
and a link-less withdrawn DAG, where persist's gap is measured and the
register is the only refusal (fails with the register check disarmed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…ore holds no family bytes (CIRISConstitution#141)

At the non-member forwarder F, both the inline file and the DAG file are
checked. F has no blob head, F's open is refused, persist relates no chunk
to the manifest, and no chunk sha that P1 sealed is held. A mutant that
copies the family blob rows into F's store fails here, on F's head for the
inline file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…ag", and count it (#771)

The lead's conditions and persist's contract (CIRISPersist#994):

- The legacy stream walk is reached only when persist holds NO relation
  for the named manifest (`chunks_of_manifest` empty) AND the manifest's
  head is a `chunk_dag`. A linked DAG that lacks the chunk refuses it, so
  "not a member" never falls through to the walk. A whole blob also
  answers empty, and it never reaches the walk.
- Every use of the walk is counted in
  `EdgeMetrics::blob_serve_legacy_dag_walks`. It is projected in the pyo3
  snapshot as `blob_serve_legacy_dag_walks` and in the uniffi snapshot as
  `blob.serve_legacy_dag_walks_total`. Each manifest is named once at
  INFO, bounded by LEGACY_LOGGED_CAP. The sunset comment cites
  CIRISPersist#994, and notes that manifests this node cannot open stay
  link-less after that backfill.
- `BlobChunkSource::attach_metrics` is a default no-op, the same shape as
  the transports' hook in CIRISEdge#809. `EdgeBuilder::build` hands it the
  edge's own bag. `PersistBlobChunkSource` keeps the bag in a OnceLock,
  and `with_metrics` sets it for a source built outside the builder.
  Every wrapper forwards the hook: edge_node's RowScopedChunkSource and
  the test fixtures.

Witnesses in self_dag_field_path_717:
- A linked DAG refuses a non-member chunk with zero walks, through a
  direct door and through the bag the builder attached. With the
  linked-non-member return removed, it fails: walks left 1, right 0.
- A link-less sealed DAG serves every chunk through the walk, and each
  serve is counted.
- A whole blob named as the DAG is refused with zero walks. With the
  chunk_dag head check removed, it fails: left 1, right 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
… shrinks with #994 (#771 review)

The named DAG's binding_state is read before its link. If that read
errored, the gate used to warn and go on to the link, so a DAG that might
be withdrawn could be judged a Member on the link alone. It now answers
NotMember (ChunkNotInNamedDag), as every other unreadable read in the gate
does.

Witness in self_dag_field_path_717: B's federation_attestations table is
renamed away for one request, through B's SQLite writer, and then put
back. A live linked DAG's chunk is refused ChunkNotInNamedDag, and it is
served before and after. The warn-and-continue mutant fails with
left "PolicyDenied", right "ChunkNotInNamedDag". The old path did not
serve here only because persist's own chunk door failed on the same read.

The sunset comments now say the legacy walk shrinks with CIRISPersist#994.
They no longer say it goes when #994 lands: manifests this node cannot
open stay link-less after the backfill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
…metrics() of an Edge built normally (#771 on #810)

A fetches the link-less W's chunk from B over the wire. B's door serves
it through the legacy walk, and the walk is counted in
b.edge.metrics(), the one bag EdgeBuilder::build settled on. B's Edge is
built normally, and its wired source is a wrapper that forwards
attach_metrics.

The rebase onto #810 had auto-merged a second EdgeMetrics::new() for the
chunk source ahead of #810's metrics block. The source now attaches the
bag #810 settles on. Reintroducing the second bag fails this assertion
with left 0, right 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ksniaWCvWoUwFQHRG3HQL
@emooreatx
emooreatx marked this pull request as ready for review October 6, 2026 04:03
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@emooreatx

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T22:12:07.161378Z 8889169 Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 8889169d4d

ℹ️ 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".

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