--- id: ops-foundation name: the ops module's substrate — the token layer, the namespace binding, and the fanout registry status: completed depends_on: [] scope: moderate risk: medium impact: component level: implementation tags: [wave-7, ops, tokens, fanout, namespaces] --- ## Description The `ops` feature's state substrate in `src/ops.rs` (currently the ADR-001 stub doc comment) — everything the five op handlers ride, and none of the handlers themselves (they land in the following tasks). The module is a **store-facade consumer** (ADR-001 §4): its store access is the public facade plus the crate-internal renewal seam named below — no backend side-door, no engine types. - **Module structure + the ops context**: the feature-gated module shape (`#[cfg(feature = "ops")]`, re-exported from `lib.rs` per the one-module-per-file convention); the ops-layer context type binding the `Arc`, the module's state (the token registry, the namespace bindings, the fanout registry), and the module config. This context is what the handler pairs (later tasks) capture. - **The pin-token layer** (ADR-010 §4; ADR-012 §2): opaque token minting over a landed put's pin; the **token↔(key, owner) in-memory mapping with process lifetime** (outlives the minting handler — ADR-012 §2's correction of the original handler-lifetime wording); the **validity domain is the minting node** — the registry is per-process state, there is no cross-node form, and fleet peers refuse tokens they did not mint (`present: false`, not an error). Named design points: **the token's minting form** (a CSPRNG id — e.g. `getrandom` gated behind `ops`; a counter or derivable form is rejected as a guessable capability — decide and record), and **the mapping's retention discipline** (when a mapping retires: conversion-confirmed or process end; the mapping carries no expiry machinery of its own — the pin row's expiry is the fleet liveness signal, and nothing expires non-fleet). - **The guard-custody design** (the fleet/non-fleet split ADR-010 §4 pins): under fleet, the put handler's `Pin` guard **drops at response completion** — the pin row's expiry is the only protection from that moment, which is exactly why the TTL/3 renewal duty exists (a guard held for the token's whole life would make the TTL machinery dead code on the ops path); under non-fleet, the token registry **holds the guard** — the token is the in-process guard's handle (ADR-012 §2's generalization: sqlite, single-instance postgres, pg-lo, `local`, `mem` — nothing expires outside fleet mode) until the embedder's registration confirms and the guard converts. The custody split is the token layer's core branch. - **The namespace→liveness-source binding** (ops-surface.md §Op family, hand-over contract step 3): the embedder's converter seam — per-namespace registration of the converter the embedder plugs its registry into ("A `Pin` conversion on the server side (token → registered liveness source) is the mechanism the embedder plugs its registry into"). The conversion rides the crate-internal `RegistryCore::convert` machinery (wave 2). Named design point: ** the conversion trigger** (an embedder-called confirm verb vs a probe-triggered conversion — decide and record; the hand-over contract's duty text is the embedder's, the mechanism is this module's). - **The crate-internal store-core renewal access** (ADR-010 §4: "the ops module is in-crate and crosses the seam by construction, the same way the put handler holds the `Pin` guard"): the renewal probe's access to `pin_renew(key, owner)` and `pin_check(key)` through the fleet host — the `pin_check` deliberately-unconsumed note from the wave-6 review (W6-3) names this consumer. The exact probe composition (verify-the-mapping-then-renew vs renew-answers-the-verification) is a named design point against ADR-012 §2's three-step handler duty. What never happens: a * consumer* reaching the seam, or a public-API widening for this. - **The fanout registry** (ops-surface.md's fetch fanout seam; POC #3 finding A3): per-digest broadcast state — one reader, store arm + subscriber arms; bounded subscriber channels (the backpressure discipline: a slow subscriber stalls the broadcast, which is correct for a put — subscriber count bounded, slow-subscriber handling is policy); the **late-joiner signal** (broadcast ended → the degrade-to-verified-get path); the **drop-and-late-join** slow-subscriber policy as ops-layer policy. The producer wiring lands with `ops-put`, the consumer wiring with `ops-fetch`; the registry's mechanics land here, unit-tested standalone. - **The renewal-cadence boundary** (ADR-013 §1): the module ships the renewal *mechanism* (the have form's handler); the TTL/3 *cadence* — the putter's renewal worker — is embedder composition above the facade (scheduler/queues), never in-crate machinery. Documented on the module, not implemented. - **Dependency posture**: `serde`/`serde_json` (payload handling) and the token CSPRNG enter as `ops`-gated dependencies — the base crate stays lean (convention 8; ADR-013 §4's engine table unchanged — `ops` pulls alkcall plus these, default-off). - **Unit tests**: token mint/lookup/retirement and the mapping's process-lifetime semantics; the custody split (the fleet and non-fleet forms branch as designed); the namespace binding's converter invocation; the fanout registry's subscribe / late-joiner-degrade / slow-subscriber-drop mechanics. ## Acceptance Criteria - [x] `src/ops.rs` carries the module structure under feature `ops`: the ops context, the token layer, the namespace binding, the fanout registry — no op handlers yet (they land in ops-json-ops/ops-fetch/ops-put) - [x] The token layer: opaque, unguessable minting (the CSPRNG design point decided and recorded); the token↔(key, owner) mapping with process lifetime; validity domain = the minting node (a per-process registry; no cross-node form exists) - [x] The guard-custody design decided and recorded: fleet — the guard drops at response completion (the row's expiry is the liveness signal); non-fleet — the registry holds the guard (nothing expires) until conversion - [x] The namespace→liveness-source binding: per-namespace converter registration riding `RegistryCore::convert`; the conversion trigger design decided and recorded - [x] The crate-internal renewal access: the ops module reaches `pin_renew`/`pin_check` through the fleet host (in-crate seam crossing; no public-API widening; no backend side-door) - [x] The fanout registry: per-digest broadcast, bounded subscriber channels, the late-joiner degrade signal, the drop-and-late-join policy point recorded - [x] New dependencies (`serde`, `serde_json`, the CSPRNG) are `ops`-gated; the default build's dependency tree is unchanged - [x] Unit tests green; `cargo test` (default) green with the module compiled out; `cargo test --features ops` green; `cargo clippy --all-targets -- -D warnings` and `cargo fmt --check` green on both shapes; `cargo doc --no-deps` clean ## References - docs/architecture/ops-surface.md (§Op family — the hand-over contract; §ACL mapping; §Store-facing relationship) - docs/architecture/decisions/001-substrate-posture-and-ops-placement.md §3/§4 (placement; the facade-consumer rule) - docs/architecture/decisions/010-backend-io-seams-and-fleet-state-home.md §4 (the token contract: minting, custody, validity domain, the in-crate seam crossing) - docs/architecture/decisions/012-pre-decomposition-consistency-rulings.md §2 (the wire shapes: {token, digest}; the have renewal form; the validity domain; the non-fleet generalization) - docs/architecture/decisions/013-family-composition-reactive-boundary-and-feature-graph.md §1 (the renewal cadence is embedder composition), §4 (the feature graph) - docs/research/poc-largeblob-findings.md finding A3 (the fanout seam shape) - src/ops.rs (the ADR-001 stub this task fills) - src/gc.rs (`EngineFleetState::pin_renew`/`pin_check` — the seam the renewal access crosses; `RegistryCore::convert`) - src/store.rs (`Store::gc()` — the crate-internal access path) - tasks/review-wave-6.md (W6-3: `pin_check` deliberately unconsumed — this wave's renewal probe is the named consumer) ## Notes > Landed as `src/ops.rs` under feature `ops` (the default build > compiles the module out; `cargo tree` default-vs-HEAD verified > byte-unchanged). The store access is exactly the facade plus one > crate-internal seam: `Store::ops_pin_renew`/`Store::ops_pin_check` > (store.rs, `pub(crate)`, cfg all(ops, postgres)) routing through the > existing `fleet_sql_host` — the store core owns the crossing; no > engine type or `Backend` name reaches the ops module; no public API > widened. The fanout registry (`FanoutRegistry`) and the token > registry (`TokenRegistry`) are constructible standalone; the > custody-validated mint verbs live on the context. > > **Decided design points (recorded on the module):** > > - **Token minting form: a CSPRNG id** — `PinToken` = 32 bytes from > `getrandom` (v0.4, the same version alkcall 0.8 pulls — no > duplication). Rejected: a counter (a token is a capability; a > guessable capability is broken) and derivable forms (key/owner- > derived is equally guessable and no longer opaque). Wire form hex > (lowercase canonical, decode case-insensitive); `Debug` masks the > bytes — the token never lands in logs/panics/errors. Malformed hex > ingress returns `None` (the have handler maps it to the > unknown-token answer, present: false — never an error; no new > error-family member invented). > - **Mapping retention**: conversion-confirmed (the confirm verb's > success removes the entry) or process end; no expiry machinery on > the mapping — the pin row's expiry is the fleet liveness signal, > nothing expires non-fleet. > - **Custody split**: `OpsContext::mint_fleet` (mapping only; owner > validated against the bound store's instance id; refused over a > non-fleet-declared store) vs `OpsContext::mint_custodial` (the > registry takes the `Pin` guard; refused on a declared fleet > store). The fleet put handler's guard-drop-at-response-completion > stays the handler's act (the mint deliberately takes no guard); > enforcement of the mirror refusal is structural at the mint verbs. > - **Conversion trigger: an embedder-called confirm verb** > (`OpsContext::confirm(namespace, token)`). Probe-triggered > conversion rejected: have is a read-gated op whose caller is the > remote putter — a protection-lifecycle mutation keyed off it would > give a read op write semantics, while the hand-over contract's > duty text puts the observation (have returns present after > registration) and the act (confirm) on the embedder. Refusals > (unknown/mismatched-namespace/unbound-converter) keep the mapping > intact; the guarded conversion rides `GcHome::convert`'s ordered > step (release + registration under the one arbitration-lock > section; the converter's act is documented as quick, non-blocking > — the `LivenessSource::contains` discipline), the fleet conversion > invokes the converter act with no pin to release. > - **Renewal probe composition: verify-the-mapping-then-renew** — > (a) token lookup (validity domain; unknown → `UnknownToken` → > present: false, the peers' refusal form), (b) the one-token-one- > row digest check (`DigestMismatch`), (c) the store core's > `ops_pin_check` (the W6-3 named consumer) — a row not live > answers `Expired` with no renewal write, (d) `ops_pin_renew` — > its never-resurrect bool is the final answer (a check/renew race > lands on the same Expired). Renew-answers-the-verification > rejected: the verify steps are where the validity domain and the > row bind are enforced. Non-fleet answers from custody (nothing > expires; no engine call). All engine failures fold to > `present: false` — the probes stay probes (ADR-008's discipline). > - **Fanout mechanics**: per-digest (`Key`) map of live broadcasts; > `begin` → exclusive `FanoutPublisher` (`None` when already live — > the producer continues store-arm-only, CAS guarantees identical > bytes); `subscribe` → bounded-channel arm or `None` (the > late-joiner/absent/bound-exhausted degrade signal; taps receive > from the tap moment onward — the A3 tap-time-set shape, the whole > blob's authority stays the verified get); per-broadcast > `SlowSubscriberPolicy::{Stall, Drop}` — Stall is the put's form > (bounded backpressure, the put must land), Drop is the > drop-and-late-join escape (a full arm is dropped, its channel > closes, the consumer degrades); publisher finish-or-drop removes > the entry, existing arms see channel end, late joiners see > `subscribe` → `None` — either way the degrade is the verified > get. Arms are cloned out under the lock and driven outside it > (nothing async under the lock); closed/full arms are reaped at > broadcast time (channels never grow unbounded). > - **The renewal-cadence boundary**: documented on the module (mechanism > here; the TTL/3 cadence is embedder composition, ADR-013 §1 — no > in-crate worker, none may exist). > > **Scope notes**: gc.rs's allow(dead_code) module doc updated — > `pin_check`'s W6-3 "deliberately unconsumed" note now reads its > landed state (the ops module's renewal probe, through the store > core's `ops_pin_check`), and `convert`'s note names the confirm > verb as the guard-custody consumer. `serde`/`serde_json` > (features derive / default) entered as ops-gated optional deps now > (the payload-handling posture's shape) — first consumed by > ops-json-ops/ops-put; the default build's dependency tree is > verified unchanged (`cargo tree` diff against HEAD). > > The fleet forms against a *live* fleet store (a real `pin_check` → > `pin_renew` row cycle; the `mint_custodial`-refused-under-fleet > arm) need the harness server (wave 6's fleet column pattern) and > land with the handler tasks — the foundation's unit tests cover the > custody branching (registry-level fleet entry: no guard held, > renewal degrades closed, conversion is the converter act alone; > custody-mint validation: the fleet mint refused over a non-fleet > store; the custodial path end to end) on the store-free and > mem/sqlite shapes. ## Summary > Completed 2026-10-12 (wave 7, first task of the decomposition). The > ops module's substrate landed: `OpsContext` (the `Arc` + > config + token registry + converters + fanout binding the handler > pairs capture), the pin-token layer (CSPRNG `PinToken`, the > process-lifetime token↔(key, owner) mapping, validity domain = the > minting node, retention = conversion-confirmed-or-process-end), the > guard-custody split (`mint_fleet`: mapping-only, the guard's drop > at response completion is the put handler's act; `mint_custodial`: > the registry holds the guard, nothing expires), the > namespace→liveness-source binding (`PinConverter` per-namespace > registration; the conversion trigger is the embedder-called > confirm verb riding the ordered-step conversion), the crate-internal > store-core renewal access (`Store::ops_pin_renew`/`ops_pin_check` — > the W6-3 consumer; ADR-012 §2's verify-the-mapping-then-renew probe > composition), and the fanout registry (per-digest broadcast, > bounded subscriber channels, the late-joiner degrade signal, the > Stall/Drop slow-subscriber policies) — no op handlers, per the task > text. Five unit tests' worth of mechanics green (11 test fns); > gates green on both shapes: `cargo test` (default, 73/19/3 with the > module compiled out), `cargo test --features ops` (84/19/3), > `cargo test --all-features` (237/125/4), clippy > `--all-targets -D warnings` across the default/ops/all-features/ > pg-pure variants, `cargo fmt`, `cargo doc --no-deps` zero warnings > on the default and gated-ops builds (the all-features build's > count is the pre-existing private-link baseline, nothing added). > Dependencies: `serde` (derive), `serde_json`, `getrandom 0.4` — > all `ops`-gated; the default dependency tree verified unchanged.