- requirements.md (new): the three use cases pinned as REQ-1..4 with the node/pool/fleet/engine vocabulary defined once — ends per-session re-derivation of consumer facts. REQ-2 (replicator fleet over one shared pg pool, incl. large-blob serving) is recorded as a planning fact predating all POCs, on the operator's authority. - ADR-008: Backend trait gains size(key) length probe (before any backend ships data — ADR-002 one-way-door discipline); fleet GC mechanism (DB-backed pin rows committed atomically with entries, TTL+renewal semantics, liveness = embedder-owned table, protect callback is single-node-only, advisory-locked single sweeper, staged re-arbitrated delete window on both engines); fs tier becomes engine-selectable (local default; pg-lo named candidate) with the fleet locality contract (shared media or re-routing; mixed tiers are a documented deployment invariant, not a constructor-provable one). - poc-pglo-spec.md (new): POC #7 spec — pg Large Objects as the fs tier's pg-lo engine; instruments, decision gate, registered in phase-0.md OQ-BL-06. - ADR-003/005 status amendments point to ADR-008's extensions; specs ripple (backends/store-api/gc/ops/overview/README). - open-questions.md: deferral-policy header gains the decisions-vs- sequenced-work distinction; pg-lo's why-not-parked audit trail recorded. - research fixes: postgres POC renumbered #4->#5 to the canonical register (phase-0 OQ-BL-06), redb cross-refs fixed, thinking- artifact sentence in B1 replaced with the honest reading. Verification: docs-only change; reference-integrity sweep across the tree (ADR/REQ/POC refs resolve); architecture-reviewer pass on the delta — original 3 criticals addressed, its follow-up (fleet liveness form, pin TTL, staged-delete semantics, enforceability, shared-media caveats) fixed in this commit.
8.5 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-10-03 |
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); 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.
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 fs media routes
large-blob access through a single storage node via these ops, with
client nodes' fs tiers serving only node-private 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 |
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)