refactor(bpa,bpv7): stream the ingress spool — validate the payload as it drains, one metadata write - #717
Open
ricktaylor wants to merge 55 commits into
Open
refactor(bpa,bpv7): stream the ingress spool — validate the payload as it drains, one metadata write#717ricktaylor wants to merge 55 commits into
ricktaylor wants to merge 55 commits into
Conversation
ricktaylor
force-pushed
the
refactor/ingress-spool
branch
5 times, most recently
from
September 10, 2026 19:49
fe5c5a6 to
0cf18bb
Compare
ricktaylor
force-pushed
the
refactor/ingress-spool
branch
2 times, most recently
from
September 29, 2026 16:35
c4e01f6 to
bab1eef
Compare
`ExtensionEditor::insert` passed flags, CRC type and block bodies through unchecked, so an edit could be accepted at call time and then fail at `finish()` or produce bytes the parser rejects: a `report_on_failure` flag on an administrative-record or null-source bundle (RFC 9171 §4.2.3-4/-5), an unrecognised CRC type, or a Previous Node / Bundle Age / Hop Count body that does not decode. Each is now refused at call time as `Error::Invalid`, carrying the error the parser raises for it (`InvalidFlags`, `InvalidCrc`, or the body's own decode error); `replace` validates well-known bodies too. The RFC rule gets one statement, `PrimaryBlock::forbids_report_on_failure`, which the parser's block check now consults. `Builder::build` emitted bundles its own parser rejects: `with_hop_count` sets `report_on_failure`, so any null-source or admin-record bundle with a hop limit was unparseable. `build()` now clears the flag on every block of such a bundle, normalised like the fragment flag. Found by the external review of #712 (CRIT-1, HIGH-2, MED-3): the BPA's filter editor mirrors this one, and a Rewriter tripping the gap aborts the node. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
Classification gains registered annotation slots and the policy-epoch stamp, written only through the engine's apply()/clear_classification() paths. Embedders register a slot (stable name, typed value, size bound) at construction and receive an unforgeable SlotHandle; values are canonically-encoded CBOR at rest, name-keyed so a registration that disappears across a restart reads as unset and clears at re-derivation. Blob makes an opaque byte string a first-class slot value (bare byte containers deliberately are not: the blanket [T] encode gives [u8] array semantics), and slot_str/slot_bytes give zero-copy borrowed reads, sound because canonical writes keep the stored payload contiguous. Slot-free records serialize byte-identical to the pre-slots shape. Per refactor_plan C2, BpaBuilder::annotation_slot is scaffolding the FilterPack replaces before any release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
The committed extension-API surface for the Phase 2 filter redesign:
three payload-free, byte-pure kinds behind one verdict, added additively
alongside the old ReadFilter/WriteFilter machinery (which the C3 engine
swap removes). Nothing consumes them yet.
- Verdict<T = ()> — one enum across all kinds: Continue(T) / Drop(reason),
where T is the kind's contribution (a MetadataDelta for a Classifier,
() otherwise), so the drop path and its reason code are identical
everywhere.
- Verifier — read-only admission check; parallel; any hook.
- Classifier — sequential input annotator; contributes a MetadataDelta.
- Rewriter — sequential extension-block editor at the two output
boundaries, distinguished by RewriteContext::{Egress { next_hop },
Deliver}; next-hop context is Egress-only so it rides the variant, not
the method signature.
Resolves filter-redesign open-question 4: Deliver hosts a Rewriter for
stripping transport-scoped extension blocks (network QoS, custody) before
a raw-Service delivery — the dual of the ingress Classifier that consumed
them. It edits extension blocks, never the payload, so it runs before the
payload BPSec decrypt and decrypts what it needs via the KeySource.
ScopedEditor is a placeholder; its operation set and the FilterPack
registration surface follow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The read side of the filter invocation: the three kinds now take a single `&BundleReader<'_>` instead of the `(&Bundle, &[u8], &dyn KeySource)` triple. BundleReader bundles a bundle, its resident source bytes, the decoded BCB OperationSets, and the key source into one borrow, and exposes a curated projection rather than the raw record: - metadata() — the BPA-local metadata, via the record's own field privacy. - primary() / block(n) — the primary block and per-block headers. - block_data(n) — a block's plaintext bytes: raw when unencrypted, BCB-decrypted (via the OperationSets + KeySource) when covered. Same contract as bpsec::block_data; Ok(None) is the not-available path (absent / not resident / covered-with-no-key). There is no raw-ciphertext accessor: a content filter never wants it, and coverage is visible via block(n).bcb. - extract::<T>(n) — decodes T from block_data's plaintext, so it works uniformly on plaintext and covered blocks. The OperationSets are stack-local at the call site (decoded once by parse), lent to the reader — not stashed on the bundle: a serde copy in metadata would cost the same on reload as re-parsing the source CBOR that is already stored. The engine constructs a reader per hook when the C3 swap lands; BundleReader::new carries the until-then dead_code allow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The embedder's shipping unit for a filter pair: annotation slots and
per-hook filter registrations are declared on one FilterPack, and
BpaBuilder::add_filters splices packs into the per-hook chains in call
order. The pack name prefixes both the at-rest slot names
("<pack>.<slot>") and the diagnostic labels, making cross-pack slot
collisions unrepresentable; build() validates names (non-empty, no '.'),
merges and freezes the slot table, splices the four chains, and fixes
the node-wide payload peek P as the max declared across the input-hook
_with_peek registrations. This replaces and deletes C1's
BpaBuilder::annotation_slot scaffolding and its (Self, handle) tuple.
Both filter modules split into a pub surface and a pub(crate) machinery
child, so internals carry plain `pub` capped by the module instead of
per-item pub(crate) noise:
- filter/slots: the embedder surface (Error, SlotValue, Blob,
SlotHandle, MetadataDelta) stays at slots; SlotWrite, SlotMap,
PolicyEpoch, SlotRegistry and SlotTable move to slots::state.
- filter/pack: FilterPack and its Error stay at pack; the frozen
chains (the three entry types, InputChain, OutputChain, FilterChains
and its freeze) live in pack::chains.
Descendant-module privacy does the rest: state.rs constructs SlotHandle
and chains.rs consumes FilterPack's fields directly, with no pub(crate)
accessors. The frozen chains carry the until-C3 dead_code allows; the
old machinery still drives execution, and the ScopedEditor operation
surface lands with the engine swap (noted on the plan's C3 row).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
The draft's header still presented the deleted ReadFilter/WriteFilter API as current, refactor_plan.md's C5 row already recorded it retired into filter_subsystem_design.md, and its two post-fold refinements (the Deliver-host resolution and the next_hop-rides-the-record shape) were verified present in the design doc before deletion (review F-14). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The [Unreleased] section carried duplicate Added/Changed/Fixed heading pairs and four statements contradicting the shipped API: a classify label parameter on a method that no longer exists (FlowController:: queue_for is label-free), a `with_cla` builder method that was always `cla`, fluent setters on a deleted type, and a migration cited as 0006 when the file is 0007_forward_pending_next_hop (in both changelogs). The same false classify claim is corrected in the design doc's one parenthetical (review F-10). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ully Four doc sites said the Deliver chain applies to the raw-Service path only; the code runs it for every local delivery. Ruled code-direction: the docs now state both halves — a Drop verdict applies to Service and Application deliveries alike, while only the raw-Service path observes the rewritten blocks (the Application path receives the decrypted payload alone, so a Rewriter's edits are invisible there). The rustdoc site is clarified rather than retracted: its visibility claim was literally true but misled about hook scope (review F-09). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-hook table an embedder can act on — transplanted from the 2026-08-28 prior-art patch and re-verified against this branch's actual dispatcher arms (its Ingress row reflects the post-store hook this branch runs; the pre-drain gate move updates it when that lands). The Verdict::Drop rustdoc gains the semantics nothing on the public surface previously stated: Some(reason) reports per the bundle's request flags, Originate never reports (the reason returns via services::Error:: Dropped), and None is silent even when the flags request reporting (reviews OF-12 + F-12). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The public doc promised parallel execution; the engine runs every chain synchronously inline (the decoded BCB OperationSets are not Send). The rustdoc now states the truth — inline invocation with order-independence as the contract (no ordering, no cross-talk, no depending on another filter having run) — and the engine doc's crate-private correction trims to the !Send rationale (review F-33). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both dead-code allowances pointed at "the engine swap (C3)" — which landed on this branch without wiring either function — and the slots module doc claimed clearing/pruning in the present tense. The comments now name the real consumer (the Phase 3 restart re-admission path), and the module doc is future-scoped: until Phase 3 a recovered bundle keeps its stored slot values, the fact an embedder designing against this doc needs (review F-11; the metadata.rs placeholder lines are corrected upstack by the route-early rewrite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three sites claimed unforgeability ("no other code can name the slot",
cross-pack collisions "unrepresentable", "privacy by unforgeability");
a throwaway unregistered pack mints a working handle for any pack's
slot, so the claims are corrected to what the scheme provides: other
registered code cannot obtain a handle (duplicate registration fails
build()), distinctly-named packs cannot collide, and the scheme is
cooperative, not cryptographic. No machinery added — registry-bound
handles fail the simplification test for an in-process cooperative
threat model (review F-21).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The FilterPack peek paragraph and all four _with_peek methods now state that the declaration is recorded but has no effect until the Phase 3 streaming ingress gate lands — hooks run with the full bundle resident. Previously an embedder's declared peek silently did nothing today and would silently shrink their filter's input at the Phase 3 release boundary; the methods stay, per the ruling keeping the early licence-boundary API (review F-24). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… compiler Both remaining "compile-time property" claims (editor.rs module doc and the design doc; the third copy died with the redesign doc) now state the real mechanism: out-of-scope targets are refused with a typed error at call time, so payload-purity is enforced by the handle rather than by code review (review F-26). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The forward path re-derived the peer identity it already had by
destructuring the channel's target status behind a hot-path
unreachable!, through an accessor with exactly one caller. Peer now
carries its id, forward composes ForwardPending { peer, queue, next_hop }
directly (the out-of-range clamp clamps the index), and
Sender::queue_status is deleted; send_to's same_queue debug_assert
still covers the queue-identity contract. A future mis-keyed queue is
no longer a forwarding panic (review F-13; the fuller QueueKey redesign
rides the queue tranche).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…non-resident blocks A present block whose extents fall outside the resident bytes surfaced as Err(Altered), diverging from the method's own rustdoc and from Block::extract. A residency pre-check (compared in u64 before any slicing, per the 32-bit rule) now returns the documented Ok(None); extract inherits it. Unreachable until the Phase 3 peek seat delivers truncated buffers, but the contract is public today; the inline test pins both directions (review F-20). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… fold The eight near-identical FilterPack registration bodies reduce to constructor-then-push through a generic Pending<E> and three private labelled-entry constructors — the only places the entry literals are spelled — and chains.rs's four hand-copied max_peek fold loops become one generic drain_pending, so a future hook that forgets the fold is a missing call rather than a silently dropped peek declaration. Public API, chain order, and label format unchanged (review F-31; the closure-pair alternative was verified E0499-uncompilable). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…description deleted
Three additive adoptions from the prior-art comparison: BundleReader
gains creation_time/expiry/has_expired (delegating to the record's
clockless-source-aware arithmetic, so an embedder Verifier stops
re-deriving it by hand); the embedder surface flattens to
filter::{ScopedEditor, FilterPack, MetadataDelta, SlotHandle}; and the
bpa.filter.registered describe block is deleted as ruled — chains
freeze at build(), so the gauge was a compile-time constant described
but never emitted (reviews OF-15, OF-17, OF-13).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… leftovers The crate doc stops claiming no_std compatibility the default build does not have (Tokio, and therefore std, is required); status_label's ten qualified BundleStatus arms become bare names with the import; the slot registry's merge doc loses the prototype's "Absorbs" leftover; and the pack rustdoc spells out "the node-wide payload peek" instead of the bare design-doc token P (review OF-21 a/c/d/e; the RoutingSink rename is extracted to its own PR, and F-29's originate half landed on the acceptance branch). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The refactor/filters deep review routed its out-of-scope findings here rather than onto the branch: the dispatcher-hardening group (deferred dispatcher start, claimed resolutions), the claim-on-dequeue fold into the hardening design's lifecycle::transitions edges, the ingress Final-segment commit points, the undifferentiated forward-error park, pre-store ingress filtering, slot-write validation, and the smaller mechanical items — each entry naming its owner (queue tranche, hardening legs, Phase 3, release boundary) and, where the 2026-08-28 hardening patch already implements the shape, naming it as the starting point. The OF-03 queue-key entry lands beside its R3-4 sibling in the transfer-outcome section; the ScopedEditor concept-revisit note is recorded to survive the review independently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Rewriter's edit is materialised and re-parsed after it returns, and a failure there aborts the process under the fail-stop ruling. But `ScopedEditor::insert` passed flags, CRC type and block bodies through unchecked, so a correct Rewriter could abort the node on an attacker-chosen bundle: a `report_on_failure` flag — which bpv7's own `Builder::with_hop_count` sets — on any admin-record transit bundle at Egress, or on a null-source bundle at Deliver (RFC 9171 §4.2.3-4/-5). `insert` and `replace` now refuse at call time, as `Error::Invalid(hardy_bpv7::Error)` carrying the parser's own error, everything the parser would reject: the forbidden flag (`InvalidFlags`), an unrecognised CRC type (`InvalidCrc`), and an undecodable Previous Node, Bundle Age or Hop Count body. `insert` canonicalizes its block type before the reserved-type check. A refusal is the Rewriter's no-match path; the fail-stop stays for genuine invariant breaks. The engine's two fail-stop sites format their panic message only on failure, and the docs say what the panic does: it aborts the process. New filter_dispositions integration tests pin the Egress and Deliver cases through the real pipeline. Found by the external review of #712 (CRIT-1, HIGH-1, HIGH-2, MED-3, MED-14, L-7). bpv7's `ExtensionEditor` gets the same refusals in fix/bpv7-extension-editor-accept-set. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
The disposition table in the filter module docs had no pipeline test for any cell. filter_dispositions now drives each live Egress and Deliver cell through a running BPA: - Drop(Some(reason)): one deletion report carrying the filter's reason, nothing transmitted or delivered, the record tombstoned. - Drop(None): silent even when the bundle requests reports. "No report" is proven by counting bundle-storage saves after the shutdown barrier: the node saves every report it originates before queueing it, while a report still in flight at shutdown never reaches the CLA. - Stored bytes that do not decode: Deliver parks WaitingForService and Egress parks Waiting (at Egress the fixed per-hop rewrite decodes the stored bytes before the chain, so its park is the one reached). Both wait on the park swap itself, since the post-registration poll may re-dispatch the parked bundle afterwards. From the external review of #712 (HIGH-3, L-26). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…ors the truth - postgres: drop the stale copy of the `next_hop` column entry, whose claim that pre-column `forward_pending` rows no longer decode contradicts the migration beside it (0007 re-parks them to `waiting`). - sqlite: the same false claim is cut from the forwarding-queue entry; `03_forward_pending_repark` re-parks those rows. - bpa: drop the byte-identical serialization claim (records from 0.2.0 do not load at all, per the BREAKING format entry); describe filters as trusted code that reads block plaintext, the payload's included, with only a Rewriter's edits scoped to extension blocks; say the `_with_peek` declarations are recorded but not yet consumed; and map each old filter-API name to its replacement. From the external review of #712 (MED-5, MED-7, MED-12, L-17, L-18). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
- Filters are trusted code with plaintext read access (block bodies BCB-decrypted on request, the payload's included when resident); only a Rewriter's edits are scoped, to extension blocks. The "payload-free" / "payload-independent" claims are gone. - The Rewriter contract states that removing or replacing a per-hop block overrides the BPA's RFC 9171 §4.4 handling, and that an accepted edit which fails to materialise aborts the process. - The engine and design doc state the peak-memory cost of an editing Rewriter (one wire-form copy per editing link, per attempt). - Slots are persisted unencrypted and must not carry BPSec plaintext or secrets; `MetadataDelta::set` keeps the last write that fits. - `services::Error::Dropped` names the origination gate too. - Doc corrections: which bpv7 editor call silently accepts a missing block, the real reason `Blob` exists, and today's per-fragment chain versus the draft reassembly redesign. The user docs no longer call the RFC 9171 checks and the IPN legacy re-encode "filters", and bibe says the egress BPSec seam is not yet built. - Test plans: unit-plan rows for the engine, editor and slot tests; component-plan Suite D (the filter dispositions, and the INT-BPA-06 extent-consistency test pipeline.rs already carried); the coverage report drops the deleted `filters/` modules. - The editor module's intra-doc links resolve. From the external review of #712 (MED-4, MED-5, MED-6, MED-9, MED-10, LOW-11, L-15, L-17, L-19, L-21). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
- Slot values never print: `SlotMap` and `SlotWrite` show names and value lengths in their `Debug`, not the embedder's bytes. - The ingress gate logs reserved primary-block flag bits again — the only operator-visible sign of a peer setting them, lost with the validity filter. - Comments that described the deleted post-store validity filter name the expiry checkpoints that replaced it. - `bpa.filter.modified` is described as what it counts: one per editing link. - `max_cached_bundle_size` gets back the rustdoc that had been spliced onto `max_bundle_size`. - Imports follow the house rules: grouped blocks, bare names at use sites (the CBOR error aliased beside bpv7's), and `RewriteContext` derives `Debug`. From the external review of #712 (MED-10, L-2, L-3, L-4, L-13, L-25). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…switches No test parsed `ipn-legacy-nodes` (whose type changed from a newtype to a plain `Vec<EidPattern>`) or `rfc9171-validity`: the example-config test only proves the file loads. Pin both keys to their typed values. From the external review of #712 (L-28). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
The per-hop rewrite and the scoped editor's guard each restated RFC 9171 §4.2.3-4/-5 (no block of an administrative-record or null-source bundle may request a report on failure). Both now ask the primary block, through bpv7's `PrimaryBlock::forbids_report_on_failure` — the predicate the parser enforces — so the rule has one statement. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…erify The deferred block-1 verification the header pass hands over in `VerifyFacts::deferred_bibs` can now be settled without a resident payload: `checks::begin_payload_verification` constructs one `bib::Verifier` per deferred op-set from the headers-only buffer, the caller feeds the payload's block-type-specific data through `update()` as it streams past, and `finish()` settles the tag — `IntegrityCheckFailed` on mismatch, checked in constant time. A BCB-covered payload and `NoKey` remain the same soft skips as before, and each verifier carries its BIB's block number so a failure attributes to the block that made the claim. Under the RFC 9173 context the verifier shares one set of primitives with the all-in-one path: `ippt_prefix` absorbs the header-resident IPPT parts (scope flags, canonical primary, target and security headers), `MacInner` holds the per-variant HMAC state, and each path then emits the target's byte-string head + body its own way — the resident path from the payload's own length (`absorb_resident_target`), the streaming path from the parsed extent, feeding the body through `update()`. That divergence is necessary: a primary-block target is canonicalized, and the editor's in-flight template carries a placeholder `Block.data` range, so the head cannot always come from the extent. `Operation::verify` and `Operation::sign` are now thin compositions of these primitives, replacing the duplicated `calculate_hmac`/`verify_inner`. `checks::verify_payload` becomes a thin resident driver over `begin_payload_verification` (feed the whole payload, finish), so the iterate-and-skip logic lives once; its now-unused `decrypted_data` / `to_update` params are dropped, updating the one bpa caller. The verifier deliberately owns its state, including a copy of the resolved key inside the MAC, so it can cross the drain's await points and a task boundary — a recorded exception to the header pass's no-key-material-across-awaits rule. `verify_payload` itself is deleted when the ingress pipeline stops calling it. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
TailReceiver wraps the CLA segment stream between parse_headers and the spool: as each segment flows through, it feeds the bpv7 PayloadTail (payload CRC, block/outer break, anti-smuggling) and the block-type- specific-data prefix of the segment to each deferred payload-BIB verifier, then yields the segment onward unchanged. Downstream — the interim in-memory spool now, BundleStorage::store after the storage tranche — sees a plain Receiver<Segment>; validation is invisible to it, surfacing only as a failed pull, with the categorised verdict (Truncated / Invalid / IntegrityFailed) read from finish() once drained. The BIB verifiers need body-only bytes, so PayloadTail gains a body_remaining() accessor: the body is always consumed from the front of a push, so the before/after delta slices each segment's body prefix out from its CRC/break trailer without changing push()'s signature. Not yet wired into the ingress pipeline (a later commit does that), so the type carries #[allow(dead_code)]; it is exercised by its own tests — pass-through fidelity, payload-CRC failure, truncation, trailing data, a deferred BIB verifying over the streamed body and failing on tamper, and a Send bound. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
The ingress gate no longer rewrites the bundle to drop RFC 9172 §5.1.1 failure-drops and honoured delete_block_on_failure unknowns: it schedules them. finalize_with_provider verifies the deferred payload BIBs and hands back the removal set; the bundle is stored exactly as received (no editing on input), and the removals ride BundleMetadata::to_remove. Each output door applies them per attempt through its editor — the egress rewrite head and a deliver-side strip on the raw-Service path — so the BPSec cascade for a BCB-covered BIB whose target list shrinks runs there, and the transmitted / delivered wire form no longer carries the block while the stored bundle keeps it. Prerequisite for the streaming spool, which cannot rewrite a whole buffer at ingress. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…ite (P1) Dissolve the process_received_bundle/ingress_bundle split into a single flow. The Ingress chain now runs in memory on the resident bundle, and only the finished, classified record is persisted — a single insert_metadata directly at Dispatching. This replaces the old insert-New-then-update-Dispatching pair (P1) and lets the now-unused Store::update_metadata wrapper go. New becomes a purely in-memory "under construction" marker that never reaches storage, so no chain-incomplete record can ever be persisted: a crash mid-chain leaves orphaned data that restart's orphan path re-runs from scratch. dispatch_bundle keeps its strict Dispatching|DispatchPending contract; the send's swap to DispatchPending is the queue commit, and a crash between the insert and the send recovers via the Dispatching restart arm. Restart's New arm is removed — with no v0.2.0 upgrade path a persisted New cannot occur. Chain Drop/Err are now pre-insert: they report deletion and delete only CLA-saved data directly, rather than via drop_bundle, which would try to tombstone a bundle that was never admitted. Received::Bundle collapses to a unit Dispatched, and reassemble's Box::pin recursion break is no longer needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
The single-write ingress model (P1) left MetadataStorage::replace with no production caller: a record's non-status metadata is now fixed at insert, and thereafter only its status moves (update_status / swap_status). Remove the trait method, its metadata-mem / sqlite / postgres impls, and the two test mocks. The in-memory store's `apply` helper was replace's only caller, so it goes too. The cross-backend conformance test retargets to update_status (meta_03_update_replace -> meta_03_update_status), and the sqlite poll_waiting and metadata-mem tombstone tests now drive status changes the same way. Document the invariant on update_status. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
Move the Ingress chain to its designed position: it now runs on the resident header prefix before the payload is drained, not post-store. A filter that reads the not-yet-streamed payload gets BundleReader's existing not-resident None, so no peek scaffolding is needed; finalize_with_provider and the in-line drain stay for the streaming leg. parse_headers now returns the header-region BCB OperationSets so the gate can thread them into the chain without re-parsing. The engine splits run_input into a self-parsing entry (originate) and a shared run_input_decoded taking pre-decoded bytes + BCB ops, and run_ingress gains a bcbs parameter plus a has_ingress guard. The gate runs the chain on a throwaway clone of the wire bundle so hv stays whole for finalize, salvaging the classifier deltas back into the real metadata; the clone dissolves in the streaming leg. A chain drop at the gate is pre-store — nothing was spooled — and reports reception-then-deletion like the sibling lifetime/rfc9171 gates. While here, correct a stale claim that BCB OperationSets are !Send (they are entirely Send; the chains run inline because the reader borrows local data, which cannot cross a spawn boundary). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…rop finalize/verify_payload The oversized-payload drain now runs through the I2 TailReceiver inline over the borrowed CLA stream: it feeds the payload CRC / block+outer breaks and each deferred payload-BIB digest as the bytes stream past, accumulating the whole bundle bounded by max_size. A resident bundle (no payload tail) was already fully validated by the header pass — the payload was present, so no BIB was deferred — so it needs no further work. TailReceiver borrows its inner receiver (its only use is this inline drain; the spawned-spool phase will revisit ownership). HeaderVerify is destructured once at the gate, moving the extension fields into metadata now that no post-drain finalize needs it whole. Deletes the interim resident-buffer paths this replaces: bpa's drain_payload and parse::finalize_with_provider, and bpv7's checks::verify_payload (BREAKING — the streaming ingress drain uses begin_payload_verification directly). Coverage moves to the TailReceiver streaming tests and bpv7's incremental-verifier tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…ader pass The key source is resolved once per bundle, inside verify_headers' sync scope: HeaderVerify carries the begun Send verifiers (deferred_verifiers) in place of the deferred op-set map, TailReceiver takes them directly, and the ingress drain's key re-request block dissolves — a begin failure surfaces as an ordinary HeaderFailure::Invalid at the header pass. bib::Verifier documents the key-handling contract for future security contexts: minimum derived state crosses the drain; raw key material stays in sync scopes (resolve at begin, or extend finish() to re-resolve at settle). Also: historical-commentary comments swept (including a stale finalize reference at the ingress gate), and every workspace rustdoc link warning fixed — the /// summaries on filter's pub mod declarations made rustdoc resolve the child modules' //! links at the declaration scope, so they go; cargo doc --workspace --all-features is now warning-free. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…anche ledgers The A-02/03/04 and P1 TODO ledgers close (their subjects are deleted or implemented), the drain's no-report deviation joins the report- conformance ledger, and two key-zeroization gaps (unwiped Key storage, hmac's zeroize feature off) are ledgered in bpv7. The streaming, filter, and refactor-plan docs go present-tense — the Ingress hook at its designed position, the RAM accumulator named as the one storage-tranche seam — and gain the concurrent drain + routing (routing-at-commit) row. CHANGELOGs record the pipeline unification, store-as-received, MetadataStorage::replace removal, and the BREAKING removal of checks::verify_payload. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
TailFailure carries its status-report reason (reason_code): Invalid maps through the shared keyed-failure mapping, IntegrityFailed is FailedSecurityOperation, and Truncated is None — a refused transfer is never reported. The ingress drain raises the RFC 9171 §5.6/§5.10 reception-then-deletion pair on any reportable failure, matching the pre-drain gates; parse_headers' single-report shape is now the one remaining deviation in the report-conformance ledger. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
report_bundle_reception takes the parsed primary parts plus an optional deletion reason: one §6.1.1 admin record asserts reception (receipt-flag gated, carrying the §5.6 Step-2/4 reception reason) and deletion (delete-flag gated, its reason taking the record's one reason slot) — the coalescing ION performs, so every ingress reject emits one report bundle where two were sent before, and a rejected bundle never becomes a metadata record. The keyed header pass now reports the same shape, closing the last ingress invalid-bundle reporting deviation, and the post-verification reject sites thread the Step-4 report_reason through instead of discarding it, so a reception-only reporter still learns the block-unsupported facts the header verify established. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
An unprocessable extension block flagged report_on_failure forces its "Block unsupported" reception report on the block flag alone — the bundle-level receipt flag no longer gates a demand the block itself made. The reception facts ride the new ReceptionReport enum (Requested / FailureDropped / Demanded), which keeps the §5.1.1 failure-drop at its RFC 9172 requested-MAY gating and makes the nonsense states unrepresentable. Expired-on-arrival is now the only remaining §5.6 deviation, and it is the deliberate, documented one. Report construction is gated up front (reportable): with reporting disabled — the default — or a null report-to endpoint (reachable only via the block-flag trigger; the §4.2.4 admin/anonymous combinations are parse-rejected), no report is encoded and the status_report.sent counters no longer tick for reports that were never sent. Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
…vage ledger The delivery_queue regression parks the WaitingForService poll inside a delegating metadata-storage wrapper: registration must return while the poll is still parked, and the parked bundle still arrives once it runs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…iew #14 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CreationTimestamp::now issues process-monotonic (time, sequence) pairs (fix/creation-timestamp-uniqueness, PR #716), so a freshly built bundle's id is unique by construction and the local_dispatch rebuild-and-retry loop no longer has a case: DuplicateBundle can only mean a collision with a pre-restart bundle (a wall clock that stepped backwards across a restart, per RFC 9171 §4.2.7). Build once and surface DuplicateBundle to the caller, who may resend; the store's atomic insert refusal remains the backstop for the restart case. Seated on this leg with the status-report generation rework; the later originate-doors leg already assumed this shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Design-doc statements this PR made stale: the persisted New checkpoint and ingress_bundle() (the single insert is the first persisted status), MetadataStorage::replace (sqlite batch-insert and postgres docs now use swap_status), the Ingress hook running on the fully stored bundle (it runs at the pre-drain gate, in memory), ingress block removals (deferred to the output doors through to_remove), finalize_with_provider and the fixed finalize step, the payload peek (recorded but not yet wired), and deferred payload-BIB checks (the drain feeds the deferred verifiers, so the payload-BIB tee has landed; the hot-forward tee remains). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
MetadataStorage::replace left the storage harness in this PR: META-03 is a compare-and-swap status update and META-17 is gone, so the harness plan, the sqlite and postgres test plans and coverage reports count META-01..16. The FilterPack peek docs said hooks run with the full bundle resident until the streaming ingress gate lands; the gate has landed, the Ingress chain runs on the resident header prefix, and the peek is recorded but not yet wired. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
failed_streamed_forward_does_not_retry_inline flaked under load: the registrations in its setup are routing changes that wake the Waiting poller, and a poll still scanning when the failed bundle parked re-attempted it for a change that predates it. Nothing in the test ordered the bundle's arrival in Waiting after those polls. The poll task now reads a request count before each poll and skips a wakeup whose requests an earlier poll already covered: Notify stores a permit for a request landing between a poll's wakeup and its read, and that permit woke a second, redundant scan. Every wake site goes through Rib::request_poll, which counts the request before notifying. The test moves into bpa's unit tests, where a #[cfg(test)] Rib::poll_waiting_idle waits for a poll covering every request raised so far before the bundle is originated. Without the redundant-scan skip that barrier is inexact: the stored permit starts one more poll after it releases. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The Ingress row of the failure and Drop contract describes the gate: a Drop is disposed of before anything is stored, with one reception + deletion report per the bundle's flags (a silent Drop still sends a requested reception report); the chain decodes nothing itself, so it has no failure of its own. - The CHANGELOG says the RFC 9171 checks screen arrivals only: nothing stored is re-checked, or re-run through the Ingress chain, on restart. - `max_bundle_size` is described as the in-memory accumulation bound on every ingress door; the unconsumed payload peek no longer claims a consumer. - The design doc gives the real reasons: no filter kind needs the payload to decide (a registered filter may still read a resident one), and an invocation cannot migrate across tasks because the reader borrows the caller's state. - The editor module's `Error` link names the enum. From the external review of #712 (L-5, L-6, L-11, L-25, MED-5). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: Rick Taylor <rtaylor@aalyria.com>
ricktaylor
force-pushed
the
refactor/ingress-spool
branch
from
October 1, 2026 10:25
c5fa6f4 to
7417a94
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stream the ingress spool: validate on the drain, store as received, write metadata once
What this leg does
Bundle reception becomes one streaming pipeline: every gate decision is taken on the resident header prefix, the payload then drains through a validating tail, and an admitted bundle is written to the metadata store exactly once. Nothing about a bundle is edited on input — what is stored is what arrived.
bpsec::bib::Operation::begin_verifyreturns aVerifierpre-fed with every header-resident IPPT part; the caller feeds payload data as it streams and settles with a constant-time tag compare. Verifiers are begun inside the keyed header pass — key material is resolved once per bundle and never crosses anawait; only theSenddigest states ride the drain. BREAKING:checks::verify_payloadis removed — an oversized signed bundle needs no whole-buffer verification pass, which is the point.TailReceiver, the validating drain. Payload CRC, bundle framing, and each deferred payload-BIB digest are fed per segment as the payload drains;finalize/verify_payloadand the resident re-verification they implied are gone.Dispatching, carrying the chain's classifier deltas;Newbecomes an in-memory marker that is never persisted, and restart treats a recoveredDispatchingrecord as chain-complete. BREAKING:MetadataStorage::replaceis removed with the last non-status rewrite of a stored record — after insert, only status moves.report_on_failuretriggers its "Block unsupported" report on the block flag alone. Drain failures report like any other parsing failure; truncated streams are refused, never reported.Rides along
add_peermid-construction removal race (whole-codebase review Bump regex from 1.10.4 to 1.10.5 #14) is fixed and pinned: peers are constructed complete, published once, and the addition re-checks its address claim after construction.WaitingForServicepoll is spawned, not awaited inline, so a gRPC-bridge-shaped sink cannot deadlock registration against its own announcements (the Refactor/grpc api v1 #667 salvage, now pinned by test).DuplicateBundleat the originate door is gone.docs/policy_subsystem_redesign.md.Storage backends change shape (
replacedeleted); external implementors should read the BREAKING bullets inbpa/CHANGELOG.mdandbpv7/CHANGELOG.md.🤖 Generated with Claude Code