Files
alkgit/docs/architecture/backend.md
T
glm-5.3-flash 85bde4c241 docs(arch): review-001 doc batch — A-4, D-2, D-3, N-1, N-2
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.
2026-09-29 08:30:26 +00:00

194 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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)