Files
alkblobs/tasks/ops-foundation.md
T
glm-5.3-flash 6feaa2ac72 feat(ops): the ops foundation — the token layer, the custody split, the namespace binding, and the fanout registry (wave 7)
The ops module's state substrate under feature `ops`, no op handlers
yet (they land with ops-json-ops/ops-fetch/ops-put):

- `OpsContext` — the context type the handler pairs capture: the
  `Arc<Store>`, the module config, the token registry, the per-
  namespace converter bindings, the fanout registry (re-exported
  from lib.rs)
- The pin-token layer (ADR-010 §4 / ADR-012 §2): CSPRNG `PinToken`
  (32 bytes, `getrandom 0.4` — the decided minting form; counter and
  derivable forms rejected as guessable capabilities), the
  token↔(key, owner) mapping with process lifetime (retirement at
  conversion-confirmed or process end; no expiry machinery), the
  validity domain = the minting node (peers refuse unknown tokens
  with present: false, never an error)
- The guard-custody split: `mint_fleet` (mapping only; the put
  handler's guard drops at response completion — its own act; the
  fleet-declaration and owner-instance checks ride the mint) vs
  `mint_custodial` (the registry holds the guard; nothing expires;
  refused under a fleet declaration)
- The namespace→liveness-source binding: per-namespace `PinConverter`
  registration; the conversion trigger decided as the embedder-called
  confirm verb (probe-triggered conversion rejected: a read-gated op
  never mutates protection), riding `RegistryCore::convert`'s
  ordered step under the arbitration lock
- The crate-internal store-core renewal access:
  `Store::ops_pin_renew`/`Store::ops_pin_check` (pub(crate), cfg all
  (ops, postgres)) routed through the fleet host — the W6-3 named
  consumer; the probe composition decided as
  verify-the-mapping-then-renew; no public-API widening, no backend
  side-door
- The fanout registry (POC #3 finding A3): per-digest broadcasts,
  bounded subscriber channels, the late-joiner degrade signal
  (subscribe → None, channel end → verified get), and the
  Stall/Drop slow-subscriber policies
- Dependency posture: serde/serde_json/getrandom enter as ops-gated
  optional deps; the default build's dependency tree verified
  unchanged; gc.rs's allow-scope notes updated to the landed state

Verification: cargo test (default 73/19/3, module compiled out),
cargo test --features ops (84/19/3, 11 new ops unit tests),
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 count at the pre-existing
baseline).
2026-10-12 23:49:09 +00:00

16 KiB

id, name, status, depends_on, scope, risk, impact, level, tags
id name status depends_on scope risk impact level tags
ops-foundation the ops module's substrate — the token layer, the namespace binding, and the fanout registry completed
moderate medium component implementation
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<Store>, 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

  • 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)
  • 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)
  • 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
  • The namespace→liveness-source binding: per-namespace converter registration riding RegistryCore::convert; the conversion trigger design decided and recorded
  • 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)
  • The fanout registry: per-digest broadcast, bounded subscriber channels, the late-joiner degrade signal, the drop-and-late-join policy point recorded
  • New dependencies (serde, serde_json, the CSPRNG) are ops-gated; the default build's dependency tree is unchanged
  • 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<Store> + 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.