--- status: reviewed last_updated: 2026-09-30 --- # 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 substrate types that keep doors and handlers from touching `futures_io` or raw packetline APIs. Depends on alkcall and — only under the `gix` feature — the backend implementations (ADR-010). The wire layer itself is backend-trait-only, which is what makes `default-features = false` compile without gix. ## 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. ## 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): `ls-refs=unborn` is advertised (POC-1 validated the *capability token* was accepted, but serving an unborn HEAD's symref line was never exercised against real git — the only advertised promise without capture evidence). Implementation-phase verification: an ls-refs round against an unborn repo, real client, both substrates (the unborn repo fixture in the POC set has refs — a new one without any). If it cannot be served correctly, the honest move per ADR-003 is dropping the token. ### 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 ` 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 gix impl's internal `spawn_blocking` is its own detail). - 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 (` `), shallow lines rejected up front for v1 (symmetric with fetch's decline, ADR-013 §4), flush, optional push-options section, then the pack stream — which is always expected (missing pack errors at unpack). An immediate flush is a client-side nothing-to-do (reply flush, no report). - **Ingestion** via `GitPackIngest` (ADR-004): thin packs accepted with bases from the server odb (default client behavior, no capability); `Bundle::write_to_directory_eagerly` with the repo's pack dir, `.keep` guard; `gix-fsck` connectivity per new tip; missing objects → `unpack ng`. - **CAS timing**: unpack-first, then per-ref checks (name via `gix_validate::reference::name` + reserved deny-list, CAS via `gix-ref` transactions, 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 `, 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; fetch via gix-protocol's client machinery over a custom alkcall Transport impl, push hand-rolled to ADR-013's shapes), substrate types (including the service selector and the authorized-repo marker), `Limits`, the backend traits (ADR-010's seam), protocol error enums; gix-feature types under the feature. 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 | | [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`), custom alkcall Transport + gix-protocol, hand-rolled push | | [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) | ## Open Questions - **OQ-03**: publish/API freeze (partially resolved — single-crate shape settled by ADR-010; the native-path preamble shapes entered the freeze inventory via ADR-016). - **OQ-05**: sha256 policy (deferred(scope)). - OQ-02 resolved (ADR-014 — ack loop design). - OQ-04 resolved (ADR-013 — receive-pack state machine). ## 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/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)