- new ADR-017: GitSession is a real typed client in v1 (ls_refs/fetch/ push), grounded in the two deployment use cases — the p2p replicator is the named downstream and needs the client protocol layer; the thin-wrapper reading is superseded - fetch client reuses gix-protocol (async-client) over a custom alkcall gix_transport::client::Transport impl (handshake writes the ADR-016 request line on the direct path; the channels open-op params carry it otherwise); push hand-rolled to ADR-013's shapes (gitoxide has no send-pack client) - storage-agnostic: packs stream both ways to caller-owned consumers; no in-session credentials (alkcall transport authenticates); client sessions carry ADR-009 Limits (client is also internet-facing) - manifest: gix-protocol/gix-transport gain async-client features (verified against published tree, MSRV 1.88) - amend ADR-010 (consumer-half bullet) and ADR-012 §4 (the deferred gix-protocol call — resolved; rider superseded); backend/transport/ overview/doors wording; review 001 A-5 marked resolved verification: cargo check (default, --all-features, --no-default-features, --features sha256), cargo +1.88 check, cargo test, clippy -D warnings, fmt --check — clean across the matrix
11 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 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:
GitRegistry— repo id →RepoRecord(storage root, visibility, grants keyed on the stable logical identity id, action set{read, write, manage}— ADR-011, ADR-015). Asyncresolve; 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).GitRegistryStore: GitRegistry— the write supertrait (put_repo/update_repo/remove_repo; alknet ADR-035's read/write split shape). Thegit/repo/*ops wrap it (ADR-012 §3). Grants ride the record, so grant mutation isupdate_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).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).GitPackGen— (repo, wants, haves, limits) → streaming pack (io::Writeconsumer), pluscommon_haves(repo, haves) -> recognized subsetfor 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).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.keepguard; missing objects →unpack ng. It prepares the validated ref updates; the transaction itself is applied byGitRefs(single CAS home — ingest validates, refs commits; one transaction per push is what makesatomiccorrect — 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
gixfacade crate is not a dependency (ADR-012 §4): the backends drivegix-odb/gix-pack/gix-refat the component level (POC-2's shape); repo opening from a registry-resolved root isgix-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:
sha1pinned (the compile-time-rejected invariant fromdocs/research/gitoxide.md);sha256passthrough 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-reftransactions withPreviousValue::MustExistAndMatchCAS (ADR-004, ADR-013 — the full push state machine is decided there).
Concurrency model
gixstructures:parking_lotshort-held locks; per-session handles andspawn_blockinglive 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 bareasync fn), signatures per ADR-012 §1.GitRegistry::resolveis 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/GitPackIngestcall; 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, notspawn_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_blockingwith 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.
- 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
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 —
the ADR-017 typed client: ls_refs/fetch/push), 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 (
channels/git/subviaregister_openable; open-op params{repo, service}— ADR-016) — the binary half: negotiation + service selector + ACL point (ADR-007; the service selects theauthorizeaction, read for fetch / write for push), yields the duplex git session. - Call ops (
git/repo/{create,delete,update,get}) — the JSON half: thinOperationSpec+handler pairs overArc<dyn GitRegistryStore>,Visibility::External, always-on (no gitoxide).createis gated by the static registry check (required_scopes: ["git:repo:create"]);delete/update/getcarry an emptyAccessControland the handler evaluates the ADR-015 gate — scopegit:adminOR the record'smanagegrant, via theauthorizepolicy 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 | 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 (amended: ADR-015) |
| 012 | Registry + ops | write trait, file default, CRUD ops, feature split (gate amended: ADR-015) |
| 015 | Manage grant + op gate | manage tier, admin-scope-OR-manage gate, create seeds manage |
| 013 | receive-pack | thin-pack ingestion, transaction CAS, report framing |
| 014 | Negotiation | ack loop, common_haves seam, no ready |
| 016 | Native session preamble | {repo, service} open-op params, request-line preamble, service in the tuple |
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,
.keeplanding, transaction CAS). - OQ-02 resolved (ADR-014 —
common_havesack seam added toGitPackGen).
References
docs/research/gitoxide.md(API contract notes — normative for the gix impl)docs/research/poc2-findings.md(generation pipeline + prerequisites)- alktty
backend.rs/localmodule (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)