---
status: stable
last_updated: 2026-10-23 (the wave-5 release gate — the core-side
surfaces froze at the gate: the riders validated, the unborn-line
emission captured to the upstream literal-`unborn` shape, the
size-bomb cap landed in `Limits`)
---
# alkgit: Git Smart Protocol (wire layer)
## What it is
The wire half of the `alkgit` protocol crate: pkt-line session substrates,
protocol V2 state machines (advertisement, ls-refs, fetch, receive-pack),
and the session substrate types (ADR-005) that keep doors and handlers
from touching `futures_io` or raw packetline APIs. Depends on alkcall and the gix
component toolkit — and, since ADR-025, no substrate crates: the backend
implementations live in the sibling `alkgit-substrate` crate, so the
protocol core's substrate-freedom is structural (no feature to
remember; ADR-010's shape, ADR-025's boundary).
## Substrate layer (ADR-005)
Two session entry points over one state-machine core. ADR-005 owns the
full decision (what each substrate owns and why); the surface is:
- **Duplex session** (the `alk/git` ALPN producer, channels-opened git
sessions, embedder stream doors) — input: (peer identity, resolved repo
id, service, authorized-repo marker, duplex stream, `Limits`). The
marker is a type the door/adapter constructs only after the ADR-007
check passes (skipping the check is a type error — ADR-007; D-3). The
service is selected at establishment on every native path (ADR-016:
the channels open-op params `{repo, service}`, or the in-band
git-daemon request line the direct-ALPN `GitAdapter` parses,
POC-1-verbatim), and selects the state machine: the V2 capability
advertisement for fetch, the V0 ref advertisement for push — both are
server-emitted firsts, so the service must precede the stream.
Encapsulates the split/compat/packetline bridge, the request reader
(delim-aware parsing, `reset()` discipline, break-on-error), and the
sideband writer.
- **Stateless session** (smart-http doors, e.g. alkhttp's future `git`
feature) — input: (peer identity, resolved repo id, service,
authorized-repo marker, request-reader, response-writer, `Limits`) per
http POST. The door's route still selects the service (doors.md), but
the substrate input carries it explicitly — nothing in a POST body
distinguishes an upload-pack POST from a receive-pack POST before
parsing (ADR-016). Adds the http-framing rules
(capability-dump skip, flush-only responses, probe handling). IO-abstract:
the door supplies reader/writer; see [doors.md](doors.md) for the
mounting.
Both substrates feed the same V2 state machines; statelessness is a
substrate property (per-request state), not a protocol fork.
**Framing rule for single-stream handlers (the RST discipline,
POC-X's measured fact):** a close while the receive side holds unread
bytes closes with `RST` — and an in-flight reply is destroyed with it.
Every handler reads its request's trailing flush before replying
(the git machines observe this implicitly; the ADR-020 bounded peek
complies). Engine behavior is correct; the drain rule is handler-side
(an alkcall-documentation candidate, ADR-023 §5).
## Protocol core (ADR-003: V2-first)
### Advertisement
- Emitted once per duplex session (never between commands — real git
hangs on re-advertisement); per-request-set on http (stateless: the
client re-sends the dump).
- Honest capability list: exactly what we serve (`ls-refs=unborn`,
`fetch=wait-for-done` — the ack loop needs no capability change, the
acknowledgments section is grammar not capability (ADR-014);
`object-format=sha1`). Unimplemented features are declined by omission
(validated against real git, POC-1). `git-upload-archive` is not
served (fixed refusal — ADR-008's never-execute rule, ssh analog in
doors.md).
### ls-refs
- Parse `command=ls-refs` (peel, symrefs, ref-prefix), stream ref lines
from the backend's listing, flush. `ref-prefix` filtering is
client-driven.
- **Unborn-HEAD rider** (review 001 N-5; validated — OQ-19 rider 1,
`architecture/n5-unborn-head-rider`, wave-5 gate 2026-10-23):
`ls-refs=unborn` is advertised, and the unborn HEAD's symref line is
served and validated against real clients on both substrates
(`git ls-remote` and the unborn-HEAD clone, both doors; the line
consumes as the default-branch resolution and prints nowhere for
ls-remote). The wire shape is the upstream ls-refs.c emission — the
**literal `unborn` token** in the oid field
(`unborn HEAD symref-target:refs/heads/main`), captured live against
git 2.43.0's own server and against ours: a zero-oid HEAD becomes a
fetch want of the null oid and errors the session, so the
zero-oid shape the wave-2 rows first served was corrected by the
rider's capture (the inbound `RefLine` parser accepts both shapes —
peer variance tolerated, emission pinned to the upstream form).
### fetch
- Parse wants/haves/done/args; object-format check (reject mismatches —
the object-format line is validated against the advertisement's
`object-format` on every command; ADR-013 pins the push-side check to
the same rule).
- Negotiation policy: the full ack loop (ADR-014) — no-`done` rounds
get an `acknowledgments` section (`ACK <oid>` per recognized have via
the backend's `common_haves`, `NAK` when none, flush; never `ready`),
the `done` round generates closure(wants) − closure(haves) via
`GitPackGen`, with the boundary set the *recognized* subset (request
haves filtered through `common_haves` — the same honest-boundary rule
as the ack rounds; never honor an unverified have; ADR-014 §2). No
cross-round state on either substrate (the client
re-sends wants + commons each round — negotiation-captures.md).
Advertisement text is unchanged: `fetch=wait-for-done`.
- Pack generation via `GitPackGen` (ADR-004), streamed over sideband on
duplex / sideband-in-response on http; the call is async-trait, with
the pipeline-concurrency permit acquired around the call by the wire
layer (ADR-009's enforcement point — backend.md concurrency model;
the impl's internal `spawn_blocking` is its own detail). The
have-aware trim (the request's/caller's haves filter counts and
ancestry — validated in POC-X's replication driver) rides the same
`haves` plumbing.
- Round/haves budgets enforced here (ADR-009); an empty resulting pack
(client already has everything) is a valid zero-object packfile.
### receive-pack (push)
V0-framed by upstream design (no version negotiation on the push path —
ADR-013); shapes are capture-grounded (`push-captures.md`), not
grammar-inferred.
- **Advertisement**: V0-shaped ref advertisement — caps NUL-attached on
the first ref line, `capabilities^{}` sentinel only for empty repos;
served set `report-status report-status-v2 delete-refs side-band-64k
atomic ofs-delta object-format=sha1` (+ `push-options` under config
gate); ACL before the first ref line (ADR-007).
- **Request**: command lines (`<old> <new> <ref>`), shallow lines
rejected up front for v1 (symmetric with fetch's decline, ADR-013 §4),
flush, optional push-options section, then the pack stream — **amended
for duplex transports (ADR-020 §1)**: delete-only pushes are packless
(commands + flush + nothing, then wait — captured live against real
`git-receive-pack` on a duplex transport; "a pack is ALWAYS expected"
is the stream-end-EOF/stdio truth). The wire layer runs a **bounded
peek** for the pack header after the command flush: header within the
window → ingestion; timeout → the packless CAS-only path (same §7
transaction home, same report); an immediate flush stays the
client-side nothing-to-do. Pinned rules (ADR-020 §1): the
push-options section is consumed *before* the peek; zero pack bytes
at timeout = packless; a *partial* header at timeout = malformed-push
session error, never a silent downgrade; the window is a named
`Limits` field (ADR-009's inventory; POC-tested value 750 ms).
- **Ingestion** via `GitPackIngest` (ADR-004; composition amended for
substrate-backed objects — ADR-019/ADR-020 §2): thin packs accepted
with bases from the object store (substrate `Find` front — no disk
odb; default client behavior, no capability); objects unpack-to-loose
into the store (verification-on-put = oid re-derivation) under the
pin bridge spanning ingest → ref CAS; connectivity per new tip is a
full graph walk over the substrate Find (parents included —
`gix-fsck`'s `Connectivity::check_commit` does not walk parents; the
owning implementation walks); missing objects → `unpack ng`; the
pack itself does not land (working area discarded; pack mirroring is
the optimization bucket).
- **CAS timing**: unpack-first, then per-ref checks (name via
`gix_validate::reference::name` + reserved deny-list, CAS applied in
one relational ref-table transaction — ADR-019 §2, the
atomic-correctness home — policy), atomic rollback, report — the
observed upstream order (ADR-013 §7). One transaction per push.
- **Status report**: band-1 pkt-line-framed (`unpack ok|ng`, per-ref
`ok|ng <ref> <reason>`, inner flush, outer flush) when sideband was
selected; bare pkt-lines otherwise. Substrate owns the framing
(unwrapped reports abort real clients).
- **Http framing**: Content-Length or chunked request (probe POST
answered 200-empty above the client's postBuffer), Content-Type
`application/x-git-receive-pack-request` / `...-result`, response ends
at flush (ADR-005 unchanged).
- Push-options parsing and `atomic` rollback semantics per ADR-013
§10–11.
### Error taxonomy
- io errors are terminal (session ends); protocol errors map to pkt-line
error bands (duplex) or http status + body (stateless). Substrate-level
`thiserror` enum; no panics in library code (convention 2).
- At the wire-mapping point (review 001 N-2), `RegistryError::NotFound`
and an authorization failure collapse to the same wire error — the
unknown-repo ≡ unauthorized indistinguishability rule (ADR-007/ADR-008)
holds at the variant→wire mapping, so no variant leaks an existence
oracle to the client.
## Limits
Every session carries `Limits` (ADR-009): negotiation rounds, haves per
round, receive-pack max size, wall clock, sideband chunk size (fixed
65000), advertisement ref cap. Missing `Limits` is a type-level error.
The advertisement ref cap is fail-closed like every other budget
(review 001 N-1): breach is a session error, never a silent truncation —
a partial ref list is the worst failure mode an advertisement can have
(clones appear to succeed).
## Public API surface (v1)
Crate-root re-exports (alktty pattern; the full list in
[backend.md](backend.md) §public API): `GitAdapter` + `register_openable`,
`GitSession` (the consumer half — ADR-017's typed client:
`ls_refs`/`fetch`/`push` over the ADR-016 preamble; construction
simplified per ADR-023 §2 — a wrapper over gix-transport's git-daemon
`Connection` composed with the alkcall `BiStream` compat halves, not a
new transport trait; fetch rounds ride `Arguments`/`Response` per
ADR-014, push hand-rolled to ADR-013's grammar), session substrate
types (ADR-005 — including the service selector and the
authorized-repo marker),
`Limits`, the backend traits (ADR-010's seam), protocol error enums —
this is `alkgit`'s entire surface; implementation types live in
`alkgit-substrate` (ADR-025's partition). Publish-freeze
point: **OQ-03**.
## Design Decisions
| [002](decisions/002-front-door-blind-core.md) | Session boundary | duplex + stateless entry points |
| [003](decisions/003-protocol-v2-first.md) | V2-first | honest advertisement; V0/V1 declined on fetch, push V0-framed (ADR-013) |
| [004](decisions/004-pack-pipeline.md) | Pack pipeline | generation on blocking threads, O(counts) |
| [005](decisions/005-session-substrate-types.md) | Substrate types | request reader, sideband sink, http framing rules |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | `Limits` in every session tuple |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | wire layer is backend-trait-only; structurally substrate-free (ADR-025) |
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push machine, thin-pack acceptance, unpack-first CAS |
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, no `ready`, wait-for-done stays |
| [016](decisions/016-native-session-preamble.md) | Native session preamble | `{repo, service}` open-op params, request-line preamble, service in the tuple |
| [017](decisions/017-consumer-half-git-session.md) | Consumer half | `GitSession` typed client (`ls_refs`/`fetch`/`push`) — construction simplified: ADR-023 §2 |
| [018](decisions/018-backend-trait-signatures-and-storage-error-model.md) | Trait signatures + storage errors | the seam shapes transport calls (`generate`/`prepare`/`apply_updates` signatures, `StorageError`, boxed-stream ownership) |
| [019](decisions/019-substrate-baseline.md) | Substrate re-baseline | the storage beneath this wire layer (the traits' composition — substrate.md) |
| [020](decisions/020-receive-pack-duplex-amendments.md) | receive-pack duplex | packless delete pushes (the bounded peek), unpack-to-loose ingestion |
| [021](decisions/021-feature-graph-and-dependency-posture.md) | Feature graph | **superseded by ADR-025** (graph re-partitioned) |
| [025](decisions/025-two-crate-workspace.md) | Two-crate workspace | this wire layer is the `alkgit` crate; composition types live in `alkgit-substrate` |
| [022](decisions/022-lfs.md) | LFS | the batch/transfer door surface (lfs.md; alkhttp `lfs` feature) |
| [023](decisions/023-replication-v1.md) | Replication v1 | `GitSession` protocol-native; the RST drain rule |
## Open Questions
- **OQ-03**: publish/API freeze (partially resolved — the two-crate
shape settled by ADR-010/ADR-025; the native-path preamble shapes
entered the freeze inventory via ADR-016).
- **OQ-05**: sha256 policy (deferred(scope)).
- **OQ-19**: implementation-phase wire validations — the unborn-HEAD
rider (the one advertised promise without capture evidence — serve
against a real client on both substrates or drop the token, ADR-003's
honesty rule) and the `common_haves` ack seam over the substrate odb
(POC-X's fetch ran multi-round with NAK-only acks; the refinement is
a negotiator optimization, low).
- OQ-02 resolved (ADR-014 — ack loop design).
- OQ-04 resolved (ADR-013 — receive-pack state machine; §3/§6 amended by
ADR-020).
## References
- `docs/research/poc-1-findings.md`, `docs/research/poc2-findings.md`,
`docs/research/poc3-findings.md` (the normative wire behavior —
observed against real git, not docs' grammar)
- `docs/research/poco-findings.md`, `docs/research/pocx-findings.md`
(the duplex transports' evidence: the packless-push capture, the
packless client shape, the RST discipline, the multi-round fetch
machinery — the re-baseline round's wire facts)
- `docs/research/push-captures.md` (the push-path normative record —
ADR-013's basis; the git:// request-line framing ADR-016 adopts)
- `docs/research/negotiation-captures.md` (the negotiation normative
record — ADR-014's basis)
- `docs/research/git-protocol.md` (inventory + observed corrections)
- `docs/research/gitoxide.md` §"Wire format" (packetline contracts)
- alktty `wire.rs`/`session.rs`/`adapter.rs` (the template's half shapes)