ADR-010 (new): the Backend trait's I/O seams pinned before any code — 'get' returns a crate-internal read cursor (kv engines materialize; 'local' pread loop; pg-lo lo_get windows), 'put' has two named forms (whole-value kv / staged-put large); GC state is store-core-owned via a crate-internal contract-tested engine-state seam on the SQL-backed engines (backends never learn liveness — ADR-005 verbatim); the store core holds the joint entry+pin transaction (corrects ADR-009 §2's ownership statement); kv-tier fleet-validity rule added (mirror of ADR-008 §3); ops pin-token TTL race resolved (the token IS the pin; renewal rides have/re-put; pause-past-TTL falls to delete-then-recover); feature graph pinned (pg-lo does not imply postgres). ADR-011 (new): vocabulary pinned — tier (contract, exactly two) vs engine (concrete impl, exactly five); store instance / node / fleet split (fleet = pool-sharing, not node count — fixes requirements.md's self-contradiction with REQ-2); mem demoted from 'backend'/'testing tier' to the kv tier's third engine (contract-reference engine); fs tier renamed 'large' including the feature name (last free moment before code exists); constructor modes pinned (dual-tier default, kv-only, mem-only) plus the composite fleet-validity predicate. Consistency round across all specs and ADR-003/004/007/008/009: stale pg-lo passages resolved per ADR-009's supersession; ADR-008 owner 'node id' → store-instance id; threshold wording corrected (conservative edge, not midpoint, of the 128-256 KiB crossover zone); SweepReport shape specced; mem classification unified; 008/009 ADR files renamed to match the tier rename; README deferral-policy recap aligned (third category = decided-but-sequenced work, not a parking kind); POC crate list completed. Verification: two independent architecture review rounds (the first found 4 criticals — unpinned trait I/O shapes, missing fleet-state seam, node/fleet self-contradiction, pin-token/TTL conflict — all resolved; final round: zero criticals); all markdown links resolve; ADR tables complete (11 ADRs).
11 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-10-03 (pin-token renewal contract pinned per ADR-010 §4) |
Ops surface (alkcall-backed network operations)
What this is
The network-facing operation family over the store: have/need
announcements, verified fetch, and streaming put — riding the alkcall
substrate behind a feature-gated ops module in this crate
(feature ops, default-off; ADR-001). The base crate stays lean and
alkcall-free; enabling ops adds the alkcall dependency and registers
the op family.
Why alkcall (ADR-001, the short form)
The op family is exactly alkcall's mixed-shape sweet spot (validated machinery, not parallel invention):
- JSON control plane — have/need announcements,
stat-shaped probes, offer/carry lengths: alkcallOperationSpec+ JSON Schema validation (alkcall ADR-016) + typed error schemas +from_calldiscovery + External/Internal visibility. - Binary data plane — bulk bytes one way (fetch:
Sub, alkcall ADR-021) or the other (put:Pub/Sink, alkcall ADR-046), allocated via thechannel_openmarker (alkcall ADR-047); established pump patterns (pump_bidi, alkcall ADR-050). - ACL is solved there —
AccessControl+ ownership checks (alkcall ADR-011) under the ADR-017 privilege model map onto namespace gating directly; an in-band invented scheme would be a second, unreviewed authorization story in the family (AGENTS convention 6).
Producer/consumer vocabulary throughout; no transport enters either
layer — the caller (an embedder, alkgit's replicator, a future alkfs
sync) dials and hands an established Connection in (alkcall is a
pure protocol crate; ADR-012 there).
Op family (WHAT is exposed)
Namespaces on the wire. Every ops payload carries an explicit
namespace field — the consumer-scoped identifier the embedder's
registry recognizes (syntax: non-empty UTF-8 string, embedder-validated;
the minting of namespaces is the embedder's act — e.g. alkgit's
registry maps repo ids to namespaces, alkfs maps workspace roots). The
store core never sees this string (backends are namespace-blind,
ADR-005); it selects the liveness source and the ACL resource. The ops
module's job is to bind namespace → the embedder-registered liveness source for pin conversion, and to present the namespace as the
alkcall ACL resource.
JSON control ops (Visibility per ADR-001 §Decision):
blobs/stat{namespace, digest}→{len}— probe before offering; ACL-gated like fetch (read action on the namespace)blobs/have{namespace, digests[]}→{present[]}— the have half of have/need set diffing (hashes only — content never traverses this op); read-gated: an existence probe over arbitrary digests is a discovery surface, so it is gated exactly like fetch, not publicblobs/delete{namespace, digests[]}— operator machinery;Visibility::Internal, evaluated under the internal authority context per alkcall ADR-017 (internal calls switch authority context, never skip ACL): requires the embedder's operator/admin authority context, not a namespace grant. Thenamespacefield exists for audit/triage scoping (which slice of the pool the deletion targets), not for gating.
Binary channel ops (registered via the channel_open marker):
blobs/fetch(Sub— the server→client streaming op shape) —{namespace, digest, ranges?}in; verified bytes out: the consumer hash-checks each received chunk/whole against the carried digest (ADR-006). Broadcast fanout above the store: one reader, store arm + subscriber arms (POC #3 finding A3); late joiners degrade to a normal post-commitget— identical bytes under CAS. Slow-subscriber policy (drop-and-late-join) is ops-layer policy, not a store concern.blobs/put(Sink— the client→server streaming op shape) —{namespace, digest?, len?}offer + byte stream in; server verifies against the canonical derivation before commit. A known-length offer is the encouraged path (one- pass); unknown-length rides the store's pre-threshold buffering path (ADR-003). Need half of have/need: a fetch miss is the need announcement — the consumer computes its need set by diffingblobs/haveresults and fetches the absent digests; no separate need op exists (the diff is caller-side; the wire carries only concrete fetch requests).
Pin hand-over for remote puts (the cross-wire contract). The
server-side put handler owns the Pin guard; the offering side's
reference registration happens in the remote process, invisible to
the server. The hand-over contract:
- the put lands pinned (ADR-005) — the entry cannot be swept while the handler holds the pin;
- the
blobs/putresponse returns a pin token (opaque handle — minted by the ops layer over the pin row the put committed; the token↔(key, owner) mapping is ops-layer state, not durable state — the durable state is the pin row itself, ADR-010 §4); the putter can later confirm its registration landed by observing the digest present viablobs/have; - the embedder's registered liveness source is the actual root of
record: the contract on the embedder (documented on the ops module)
is that it registers the namespace's roots such that a digest
referenced by a namespace's manifest is either (a) already
registered before the caller's next sweep could run on the serving
node, or (b) protected by holding the pin token until its
registration is confirmed (
blobs/havereturns present after registration). In short: the remote putter may not rely on an unconfirmed put surviving the next sweep — it holds its pin token until its registration shows up inblobs/have. APinconversion on the server side (token → registered liveness source) is the mechanism the embedder plugs its registry into.
Token renewal (ADR-010 §4 — the fleet-TTL race, resolved): the
token is the pin — there is no second pin state. The server-side
handler's in-process Pin guard drops when the blobs/put response
completes (if it never dropped, the TTL machinery would be dead on
the ops path — the pin row's expiry, not the guard's presence, is the
fleet liveness signal). Under fleet
engines the put's pin is an expiry-carrying row (ADR-008: TTL 60 s
default, renewed every TTL/3). The remote putter holding a token is
the row's owner and renews by the ops it already speaks: a
blobs/have probe presenting the token alongside the digest (the
handler verifies the token maps to the digest's pin row and calls the
crate-internal pin_renew via the ops module's store-core access —
the ops module is in-crate and crosses the seam by construction, the
same way the put handler holds the Pin guard; has itself remains
a pure existence probe per ADR-008 — renewal is a token-authenticated
act on the pin row, not a has semantic), or a re-put of the same
digest (CAS dedup renews the row). A putter may pause past the
TTL/3 mark (20 s at the default TTL) only if it renews before expiry;
a putter pausing past TTL lets the row expire — the sweeper reaps
it (ADR-008) and delete-then-recover (ADR-005) is the backstop: the
putter re-offers the digest; CAS makes the re-put idempotent. The
race resolves to a re-put, never to silent loss. For non-fleet
(local-engine) serving nodes the token is the in-process guard —
nothing expires; the process-crash case is ADR-005's in-flight-put
loss with the same backstop.
Placement of the op registration (which embedders wire where they
want them exposed) matches the alkgit ops pattern (its
git/repo/* call ops): the module exports spec/handler pairs; the
embedder assembles.
ACL mapping
Resources and actions, evaluated by alkcall's AccessControl machinery
at op entry — this module never re-implements an authorization check
(convention 6; ADR-001):
- Namespace = resource.
resource_type: "blob-namespace",resource_id_pathselecting the payload'snamespacefield; actions map onto read/put/manage:blobs/fetch,blobs/stat,blobs/have— the read action; unlisted namespaces (no ACL entries) deny by default (OQ-09's resolution; a grant per namespace is the deferred cost, deliberately paid over accidental exposure)blobs/put— the write actionblobs/delete— not namespace-gated at all; internal authority context only (alkcall ADR-017: internal switches context, never skips ACL)
Store-facing relationship
The ops module is a consumer of the store core (store-api.md), not a
side channel: fetch handlers call get/read_range/stat; put
handlers call put; both honor pins and delete windows. There is no
ops-internal path around the store facade (ADR-001 §Consequences —
this is what makes "re-homing the ops surface later loses nothing"
true). One fleet topology additionally rides this module: re-routing
(ADR-008) — a fleet whose nodes do not share large-tier media routes
large-blob access through a single storage node via these ops, with
client nodes' constructors in kv-only mode (ADR-011 §4) for pool
content. The module supports this shape without change; no
fleet-specific op exists.
Design Decisions
| ADR | Decision | Summary |
|---|---|---|
| 001 | Substrate posture & placement | alkcall-backed, feature-gated ops module; conformance-not-deviation rationale |
| 006 | Verification | verified-fetch via carried digests; no chunk trees |
| 008 | Fleet re-routing topology | non-shared-media fleets route large-blob access through a storage node via these ops; no new ops |
| 010 | Pin-token renewal | the token is the pin; renewal rides have/re-put; TTL expiry falls to delete-then-recover |
| 011 | Vocabulary + modes | instance/node/fleet split; kv-only mode named (the re-routing posture's client shape) |
Open Questions
None owned by this document. OQ-09 (default namespace visibility) resolved closed-by-default — recorded in open-questions.md with its reopen condition (a named requirement for open-read-by-default, never mere existence of a deployment).
References
docs/research/phase-0.mdOQ-BL-01 (the substrate settlement + its justification, promoted verbatim into ADR-001 §Context)- alkcall docs/architecture — ADR-011/012/016/017/021/046/047/050 (the machinery this module rides)
docs/research/poc-largeblob-findings.mdfinding A3 (fanout seam)- ADR-001; store-api.md; gc-and-namespaces.md (namespace-as-resource)