--- id: ops-fetch name: blobs/fetch — the verified Sub op and the fanout seam's consumer side status: completed depends_on: [ops-foundation, ops-call-harness] scope: moderate risk: medium impact: component level: implementation tags: [wave-7, ops, fetch, fanout, verification] --- ## Description `blobs/fetch` (ops-surface.md §Op family) — the server→client streaming op: `{namespace, digest, ranges?}` in; **verified bytes out** over the binary data channel. - **The op shape**: `OperationType::Sub` (alkcall ADR-021's streaming handler kind) with the **`channel_open` marker** (alkcall ADR-047) — the binary data plane rides a `channel_open`-allocated channel; the establishment phase (alkcall ADR-049) carries the fetch's ack shape in the `Establishment` reply fields as the wire requires. The **read** action (ACL exactly like stat/have — closed-by-default). - **Verified streaming** (ADR-006; ops-surface.md): the *consumer* hash-checks the received whole against the carried digest — whole-blob verification; there are no per-chunk or per-range checks derivable under the canonical digest. The handler streams from the facade only: `Store::get` (the whole) or `Store::read_range` (the ranged form). **Ranged fetch** (`ranges?`): the response carries the same ADR-006 slice-digest convention `read_range` returns — SHA-256 over exactly the returned bytes, out-of-band. - **The fanout seam's consumer side** (ops-surface.md; POC #3 finding A3): the fetch handler consults the foundation's fanout registry — a live broadcast for the digest → a **subscriber arm** taps it; no live broadcast, or one that ended (a **late joiner**) → the normal post-commit verified `get` — identical bytes under CAS. The slow-subscriber policy (drop-and-late-join) is ops-layer policy, not a store concern. Named design point: **the producer side's shape** — the put handler's stream reader as the canonical producer (serve-while-persisting, A3's shape), and whether fetches also produce read-side broadcasts (concurrent fetchers of one stored digest sharing one store read) — decide and record against the A3 text. - **The miss shape**: a fetch of an absent digest is the typed missing-error schema (the need announcement *is* the fetch miss — the have/need diff is caller-side; no separate need op exists). - **Tests through the harness**: the whole-blob verified round-trip (byte-exact; the client's hash-check passes); the ranged form (bytes + slice digest matching `read_range`'s convention); the fanout consumer side (a live broadcast tapped — the subscriber receives while the producer runs; a late joiner degrades to the get path — asserted by which path served); the miss shape; the ACL denial (no read grant on the namespace → denied). ## Acceptance Criteria - [ ] fetch exported as a Sub spec/handler pair with the `channel_open` marker + ALPN; the establishment reply fields carry the specced ack shape - [ ] Whole-blob verification: the received whole hash-checks against the carried digest (the consumer-side discipline documented on the op; the server streams verified-store bytes) - [ ] Ranged fetch carries the ADR-006 slice-digest convention (out-of-band, over exactly the returned bytes) - [ ] The fanout consumer side: a live broadcast → subscriber arm; a late joiner → the verified get path; the producer-side design point decided and recorded - [ ] The miss shape is the typed missing error; the have/need diff stays caller-side (no need op invented) - [ ] ACL: the read action; closed-by-default verified by test - [ ] Handlers touch only the store facade (no backend side-door) - [ ] Harness tests green; `cargo test` (default) green; `cargo test --features ops` green; clippy/fmt green on both shapes; `cargo doc --no-deps` clean ## References - docs/architecture/ops-surface.md (§Op family — the fetch bullet + the fanout text; the need-half note) - docs/architecture/decisions/006-verification-posture-and-transfer-encoding.md (whole-blob verification; the slice-digest convention) - docs/architecture/decisions/008-trait-size-probe-fleet-gc-and-large-engines.md §3 (the re-routing topology this op serves — large-blob access through a storage node) - docs/research/poc-largeblob-findings.md finding A3 (the fanout seam: one reader, store arm + subscriber arms; late joiners degrade to get-after-commit) - alkcall (upstream checkout): ADR-021 (streaming handler for subscriptions), ADR-046/047/049 (the sink kind, the channel_open marker, the establishment phase), `src/channels/client.rs` - tasks/ops-foundation.md (the fanout registry's mechanics), tasks/ops-call-harness.md (the harness) ## Notes Implementation agent, 2026-10-13. The op landed as `fetch_openables(ctx) -> (OperationSpec, OpenEstablisher, OpenHandler)` in `src/ops.rs` — the `Sub` open triple the embedder assembles through `ChannelCore::register_openable_with_establisher` (spec `channels/blobs/sub`, `OperationType::Sub`, `channel_open` marker on the `alk/blobs` ALPN `BLOBS_CHANNEL_ALPN`; the standard shape, so the ALPN derives from the name and no explicit `channel_open_alpn` rides). Exports: `fetch_openables`, `BLOBS_CHANNEL_ALPN`, `FETCH_OP_ID`. Design points decided and recorded (op doc + the module doc's fetch section + ops-surface.md's fetch bullet): - **The ack rides ADR-049 amendment 3's establishment reply fields**: the open reply merges `{channel_id, source, len?, slice_digests?}`. `source` names the serving path (`store`/`fanout` — the test criterion "asserted by which path served" is wire-visible, and a consumer knows whether the stream is a live put's tail); the whole form acks `{source: "store", len}` or `{source: "fanout"}` (no length on a tapped broadcast — the hash-check runs at stream end); the ranged form acks `{source: "store", slice_digests[]}` — read_range's convention verbatim, one per range in request order, over exactly the served bytes (over-reads clamp; the digest covers the clamp). - **The establisher's consult order**: store `get` first (a commit that raced the broadcast's end is the same bytes under CAS; the Blob opens into the plan at establishment, closing the establish→pump race window), then the fanout subscribe (a live broadcast taps), then the miss. The ranged form serves `read_range` on the committed entry only — a not-yet-committed digest's ranged fetch is the miss (ranges are the committed entry's shapes; no tap-window feed serves them). The ranged form's reads happen at establishment because the slice digests must ride the ack, which lands before the pump runs. - **The miss maps onto ADR-049's established vocabulary** — the only op-plane failure surface an open op has: the establishment phase refuses with reason `unknown_resource` ("the requested resource does not exist"), the digest in the refusal's message; the declared `BLOB_MISSING` ErrorDefinition (same code + digest-details schema the JSON ops declare) is the op's disclosed missing-error contract. `channel:open_failed` + `details.reason` is what a consumer branches on; a phantom channel with an in-band error frame (the ADR-049 antipattern) is never reachable. The same closure covers the refusal-to-serve-unverifiable shape: a store-core error mid-establishment (including a verification-failed read) refuses with reason `handler_error` rather than streaming unverified bytes. - **The producer-side design point (decided, against the A3 text)**: fetches do NOT produce read-side broadcasts — the put handler's stream reader is the canonical producer, concurrent fetchers of one stored digest each ride their own facade read. Recorded in the module doc's fanout section (a named design point paragraph), the op doc, and ops-surface.md's fetch bullet. Rationale: A3's fanout is the put stream's seam (its late-joiner note degrades to get, never get-then-rebroadcast); a read-side broadcast adds a second producer class with no lifecycle owner (its end would be an arbitrary policy, its arms hold memory where the large tier exists to avoid materializing), for a concurrency case bounded by the put window the put-side broadcast already covers; under CAS every per-fetcher read returns identical bytes. - **Slow-subscriber policy stays producer-owned** (per-broadcast, set by the put at `begin` — the foundation's registry semantics); a fetch tap rides the broadcast's policy. The tap-time-set discipline (a mid-put tap has no prefix) is documented on the op + registry: the consumer's hash-check on a partial feed fails and the re-fetch is the degrade, identical bytes post-commit. - **A mid-stream note for the whole form**: between a store-hit establishment and the pump, the entry cannot vanish (the Blob read is already open; committed entries are immutable), so the ack's `len` is trustworthy; a mid-stream store/transport failure ends the pump (teardown closes the stream), the consumer's hash-check fails, and the re-fetch is the degrade. Verification: `cargo test` (default) green, `cargo test --features ops` green (the five harness tests: verified whole round-trip + consumer hash-check; ranged + ack slice digests vs `read_range`; live-broadcast tap served without a commit; late joiner served by the store path; the typed miss; the closed-by-default read denial), `--all-features` green, clippy `-D warnings` on all three shapes, `cargo fmt --check` green, `cargo doc --no-deps` clean on both shapes (the private `FETCH_WINDOW` doc link de-linked, the PGLO-1 class). The pure `--no-default-features --features ops` build fails on main already (engine-typed match arms upstream of this task, unreachable without an engine feature) — pre-existing, not this task's regression. ## Summary `blobs/fetch` landed complete: the verified `Sub` op as the `fetch_openables` open triple (`channels/blobs/sub` + the `channel_open` marker on `alk/blobs`; read-gated, closed-by-default), the establishment ack in ADR-049's reply fields (`{source, len?, slice_digests?}`), whole-blob consumer-side verification per ADR-006, the ranged form carrying `read_range`'s slice-digest convention, the fanout seam's consumer side (live tap / late-joiner store degrade, the path named in the ack), the miss as the typed missing-error refusal (BLOB_MISSING disclosed; the wire shape is ADR-049's `unknown_resource`), and the producer-side design point decided and recorded against the A3 text (fetches never produce read-side broadcasts). All acceptance criteria verified; harness tests green; all gates green on both feature shapes.