Files
alkgit/docs/architecture/backend.md
T
glm-5.3-flash e76f91f6d7 docs(architecture): resolve OQ-04 — receive-pack state machine (ADR-013)
- ADR-013: V0-framed push machine grounded in real git 2.43.0 captures
  (file://, git://, smart-http mock, raw stdio into real receive-pack):
  V0-shaped ref advertisement (caps on first ref line, capabilities^{}
  sentinel only for empty repos), served capability set, shallow requests
  rejected for v1, thin packs accepted with server-odb bases (no
  capability involved; push.thin default), ingestion bound to
  Bundle::write_to_directory_eagerly + gix-fsck + one gix-ref transaction
  per push (.keep-guarded), unpack-first CAS timing with observed
  upstream order, band-1 pkt-line-framed status report, http framing
  (probe/Content-Length/chunked), v1 update policy (CAS only; deletes
  and force-push allowed)
- docs/research/push-captures.md: the normative push wire record
- transport.md/backend.md/doors.md: receive-pack sections rewritten to
  the decided shapes; backend.md ingestion composition bound; stale
  OQ-04 references resolved
- ADR-003 amended: V2-only governs fetch; push is V0-framed by upstream
  design (fixes the V2-only contradiction found in review)
- ADR-009 amended: haves default reconciled with the client's stateless
  ceiling (16384); blocking-pipeline budget covers generation+ingestion
- OQ-04 resolved; tracker task closed; CAS-fail-fast optimization
  tracked (tasks/architecture/oq-13-cas-failfast.md)
- research index: poc findings + capture docs listed

Verification: cargo test / clippy -D warnings / fmt --check / doc pass
2026-09-25 04:05:07 +00:00

8.6 KiB
Raw Blame History

status, last_updated
status last_updated
draft 2026-09-25

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 — ADR-011). 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).
  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); blocking-thread friendly.

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 moved into spawn_blocking tasks (POC-2's shape: store shared, handle per session, generation on blocking threads).
  • Poisoned locks: unwrap_or_else(|e| e.into_inner()) (convention 2).
  • The traits are Send + Sync object-safe; impls run under the adapter's tokio context. 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.

Public API surface

Crate-root re-exports (the alktty pattern): backend traits + types (including RepoRecord, AccessAction, the authorize policy function, 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), gated by required_scopes (global tier) + ownership (self-owned tier, alkcall ADR-011 mint-and-own at create). Registered by the assembler wherever it wants them exposed. Managing records (op ACL) and git access (repo grants) are different capabilities — ownership never implies git access (ADR-011, ADR-012 §3).

Design Decisions

ADR Decision Summary
004 Pack pipeline data::output gen / data::input ingestion
007 ACL first registry returns rule inputs
008 Repo identity wire names are registry ids
009 Budgets limits flow into gen/ingest
010 Pure protocol crate traits in-crate, impls behind features
011 Per-repo authorization grants in records, policy in core
012 Registry + ops write trait, file default, CRUD ops, feature split
013 receive-pack thin-pack ingestion, transaction CAS, report framing
014 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)