alkgit 0.1.0

Git smart protocol for the alk family: pkt-line substrates, protocol V2 serving (upload-pack/receive-pack), producer/consumer halves on alkcall channels, and the backend seam traits — the shipped alkstore+alkblobs composition is the alkgit-substrate crate (ADR-010/025)
Documentation
---
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

| ADR | Decision | Summary |
|---|---|---|
| [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)