Amendment batch (no new decisions, all doc-level): - A-4: done-round boundary set is the recognized subset — request haves filtered through common_haves, the same honest-boundary rule as the ack rounds (never honor an unverified have); amendment clause in ADR-014 §2, same rule restated in transport.md §fetch. - D-2: amendment note on ADR-007 step 3 — the per-repo check is ADR-011's authorize policy function (static ACL engine fails closed on None identity); step order unchanged. - D-3: authorized-repo marker added to both substrate input tuples in transport.md and to backend.md's public-API list (ADR-007's type-level enforcement promise is now findable from the transport spec). - N-1: advertisement ref cap is fail-closed (breach is an error, never a silent truncation) — transport.md §Limits. - N-2: RegistryError::NotFound and authorization failure collapse to the same wire error at the variant→wire mapping — transport.md §error taxonomy. - review 001: A-4/D-2/D-3/N-1/N-2 marked resolved. Verification: cargo doc --no-deps, cargo test — clean.
194 lines
11 KiB
Markdown
194 lines
11 KiB
Markdown
---
|
||
status: draft
|
||
last_updated: 2026-09-29
|
||
---
|
||
|
||
# Backend traits: the storage seam
|
||
|
||
## What this is
|
||
|
||
The payload side of the protocol crate: the traits a service deployer
|
||
implements (or consumes via the feature-gated gix implementation) so the
|
||
git protocol can talk to storage. Per ADR-010 this mirrors alktty's
|
||
`TtyBackend` pattern — traits in-crate, real implementation behind a
|
||
feature. Unlike alktunnels (no backend trait), git needs the seam: pack
|
||
generation/ingestion is too heavy to hard-wire.
|
||
|
||
## The trait family (ADR-010 sub-decision 4, ADR-011/012)
|
||
|
||
Five traits, kept small and orthogonal — the protocol crate never sees
|
||
gix types:
|
||
|
||
1. **`GitRegistry`** — repo id → `RepoRecord` (storage root, visibility,
|
||
grants keyed on the stable logical identity id, action set
|
||
`{read, write, manage}` — ADR-011, ADR-015). Async
|
||
`resolve`; the authoritative mapping (ADR-008); resolution failure is
|
||
indistinguishable from authorization failure (ADR-007). Authorization
|
||
is alkgit-core's policy function evaluated on the record (data in the
|
||
backend, policy in core — an embedder with a foreign permission
|
||
system maps its ACL into grants at resolve time).
|
||
2. **`GitRegistryStore: GitRegistry`** — the write supertrait
|
||
(`put_repo`/`update_repo`/`remove_repo`; alknet ADR-035's read/write
|
||
split shape). The `git/repo/*` ops wrap it (ADR-012 §3). Grants ride
|
||
the record, so grant mutation is `update_repo` (convenience wrappers
|
||
additive later, two-way) — which is also the app's promotion path
|
||
(roles/teams compile to flat grants upstream; ADR-015 §6) and the
|
||
shape a future replication sync would write through (ADR-015 §8).
|
||
3. **`GitRefs`** — listing for advertisement (refs + peeled tags + symref
|
||
targets, the ls-refs response data) and ref transactions (CAS apply
|
||
for receive-pack, name validation per git ref rules + reserved-
|
||
namespace deny-list).
|
||
4. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack
|
||
(`io::Write` consumer), plus `common_haves(repo, haves) -> recognized
|
||
subset` for the negotiation ack loop (a have is recognized iff it
|
||
exists in the object store and is a commit — the honest boundary:
|
||
never ack what we cannot subtract; ADR-014). Negotiation-agnostic
|
||
generation. Missing objects abort with an error, never a broken pack
|
||
(ADR-004).
|
||
5. **`GitPackIngest`** — client pack stream → indexed pack + fsck/
|
||
connectivity report. Thin-pack bases resolve from the server's own odb
|
||
(push sends thin packs by default — ADR-013 §5); the pack lands with a
|
||
`.keep` guard; missing objects → `unpack ng`. It *prepares* the
|
||
validated ref updates; the transaction itself is applied by `GitRefs`
|
||
(single CAS home — ingest validates, refs commits; one transaction per
|
||
push is what makes `atomic` correct — ADR-013 §7). Budgeted (ADR-009
|
||
max pack size); the impl runs its blocking work off the async
|
||
executor (concurrency model below).
|
||
|
||
Minimal-vs-full was the open sub-question; resolved as **full family** —
|
||
each trait is one or two methods plus types, and collapsing them (e.g.
|
||
refs into the registry) would force one impl block per downstream where
|
||
independent seams are cheaper to satisfy. The `gix` feature implements
|
||
the three object-storage traits (3–5: `GitRefs`, `GitPackGen`,
|
||
`GitPackIngest`); a downstream with its own object store implements 3–5
|
||
and reuses 1–2 (`GitRegistry`/`GitRegistryStore`), or none of it.
|
||
|
||
## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story)
|
||
|
||
Two independent seams, two default-on features:
|
||
|
||
| Feature | Contents | gitoxide |
|
||
|---|---|---|
|
||
| `gix` | `GitRefs`/`GitPackGen`/`GitPackIngest` impls — the local-disk object-storage engine (odb/pack/ref/object/fsck components) | yes |
|
||
| `registry-file` | `GitRegistry`/`GitRegistryStore` default impl (per-repo record files + in-memory index, config-seeded, op-mutable, atomic writes) | no |
|
||
|
||
- `default-features = false` — wire/protocol layer only (an embedder
|
||
brings its own backends). Both features default-on (batteries
|
||
included); they are independent seams — DB-registry + gix-engine and
|
||
S3-storage + simple-registry combinations both exist downstream.
|
||
- The `gix` **facade crate is not a dependency** (ADR-012 §4): the
|
||
backends drive `gix-odb`/`gix-pack`/`gix-ref` at the component level
|
||
(POC-2's shape); repo opening from a registry-resolved root is
|
||
`gix-discover` + `gix-odb::Store::at`. Verified at implementation.
|
||
- The registry-file store needs no gitoxide; persistence adapters
|
||
(SQLite et al.) are future, separate, additive — ADR-012 §2.
|
||
- Hash: `sha1` pinned (the compile-time-rejected invariant from
|
||
`docs/research/gitoxide.md`); `sha256` passthrough feature (OQ-05
|
||
policy unchanged).
|
||
- Encodes the POC-2 prerequisites by construction: odb handle sharing
|
||
(`Arc<Store>` shared, per-session handles, `prevent_pack_unload()` +
|
||
`ignore_replacements = true`), generation on blocking threads,
|
||
O(counts) memory, missing-objects abort.
|
||
- Received-pack ingestion via `gix-pack::Bundle::write_to_directory_eagerly`
|
||
(`streaming-input`, thin-base lookup) + `gix-fsck` + `gix-ref`
|
||
transactions with `PreviousValue::MustExistAndMatch` CAS (ADR-004, ADR-013
|
||
— the full push state machine is decided there).
|
||
|
||
## Concurrency model
|
||
|
||
- `gix` structures: `parking_lot` short-held locks; per-session handles
|
||
and `spawn_blocking` live inside the gix impls (POC-2's shape: store
|
||
shared, handle per session, generation on blocking threads).
|
||
- Poisoned locks: `unwrap_or_else(|e| e.into_inner())` (convention 2).
|
||
- **Trait execution model** (review 001 A-6): all five traits are
|
||
`#[async_trait]` — `Send + Sync`, dyn-compatible (E0038 forbids bare
|
||
`async fn`), signatures per ADR-012 §1. `GitRegistry::resolve` is
|
||
async (one call per session/request, before the first protocol byte —
|
||
ADR-011 §6); no sync-hot-path constraint exists here, unlike alkcall's
|
||
accept loop. The execution model the signatures imply:
|
||
- **The wire layer enforces the pipeline-concurrency budget itself**
|
||
(ADR-009's "max concurrent blocking pipeline tasks" — a permit
|
||
acquired in the wire layer around each `GitPackGen`/`GitPackIngest`
|
||
call; the admission point of "enforced at assembly/acceptance
|
||
time"). The wire layer is backend-trait-only (ADR-010) and knows
|
||
nothing of stores, handles, or threads — so the permit, not
|
||
`spawn_blocking`, is its entire concurrency contract.
|
||
- **Implementations must not block the async executor** and own their
|
||
internal threading: the gix impls run pack generation/ingestion on
|
||
`spawn_blocking` with the owned handle moved in *inside the trait
|
||
impl* (POC-2's shape, restated at its true layer — it is the impl's
|
||
internal structure, invisible from the wire layer).
|
||
- The traits are async so an embedder whose storage is async (DB
|
||
registries, network object stores) implements them natively; the
|
||
gix impl's blocking work is an internal detail, not part of the
|
||
seam.
|
||
|
||
## Public API surface
|
||
|
||
Crate-root re-exports (the alktty pattern): backend traits + types
|
||
(including `RepoRecord`, `AccessAction`, the `authorize` policy
|
||
function, the authorized-repo marker type (the door constructs it only
|
||
after the ADR-007 check passes — session tuples carry it, transport.md
|
||
substrate layer), and the `git/repo/*` op spec+handler pairs),
|
||
`GitAdapter`/
|
||
`register_openable` (producer), `GitSession` (consumer), substrate
|
||
types, `Limits`, protocol error enums; feature types (`GixBackend`
|
||
family under `gix`, the file registry under `registry-file`) exported
|
||
under their features. The embedder-facing freeze point remains OQ-03
|
||
(narrowed: it is now this crate's own publish).
|
||
|
||
## The two op kinds (ADR-012 §3, the classification)
|
||
|
||
alkgit ships both alkcall op kinds from one crate — the first family
|
||
payload to do so (alktty/alktunnels: open ops only; alknet-docker,
|
||
planned: call ops only):
|
||
|
||
- **Open op** (`alk/git` via `register_openable`; repo id in the
|
||
open-op params) — the binary half: negotiation + ACL point (ADR-007),
|
||
yields the duplex git session.
|
||
- **Call ops** (`git/repo/{create,delete,update,get}`) — the JSON half:
|
||
thin `OperationSpec`+handler pairs over `Arc<dyn GitRegistryStore>`,
|
||
`Visibility::External`, always-on (no gitoxide). `create` is gated by
|
||
the static registry check (`required_scopes: ["git:repo:create"]`);
|
||
`delete`/`update`/`get` carry an empty `AccessControl` and the handler
|
||
evaluates the ADR-015 gate — scope `git:admin` OR the record's
|
||
`manage` grant, via the `authorize` policy function, generic FORBIDDEN
|
||
denial, unknown-repo ≡ unauthorized (ADR-008). Registered by the
|
||
assembler wherever it wants them exposed. Managing records (op scopes)
|
||
and git access (repo grants) are distinct sources — but the record is
|
||
the single per-repo authz surface, and repo creation seeds the
|
||
creator's `{read, write, manage}` grants (ADR-015).
|
||
|
||
## Design Decisions
|
||
|
||
| ADR | Decision | Summary |
|
||
|---|---|---|
|
||
| [004](decisions/004-pack-pipeline.md) | Pack pipeline | `data::output` gen / `data::input` ingestion |
|
||
| [007](decisions/007-acl-before-advertisement.md) | ACL first | registry returns rule inputs |
|
||
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids |
|
||
| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits flow into gen/ingest |
|
||
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, impls behind features |
|
||
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in records, policy in core (amended: ADR-015) |
|
||
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split (gate amended: ADR-015) |
|
||
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-scope-OR-manage gate, create seeds manage |
|
||
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing |
|
||
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` |
|
||
|
||
## Open Questions
|
||
|
||
- **OQ-05**: sha256 policy (deferred(scope)).
|
||
- **OQ-03**: publish-time API freeze inventory (the op set enters it).
|
||
- OQ-04 resolved (ADR-013 — ingestion composition bound: thin-base
|
||
lookup, `.keep` landing, transaction CAS).
|
||
- OQ-02 resolved (ADR-014 — `common_haves` ack seam added to
|
||
`GitPackGen`).
|
||
|
||
## References
|
||
|
||
- `docs/research/gitoxide.md` (API contract notes — normative for the
|
||
gix impl)
|
||
- `docs/research/poc2-findings.md` (generation pipeline + prerequisites)
|
||
- alktty `backend.rs`/`local` module (the trait + feature template)
|
||
- alknet ADR-033/035 (the repo/adapter + read/write-split pattern)
|
||
- ADR-010 (the structural decision), ADR-011/012 (the seam this doc
|
||
specifies) |