- N-4: GitPackIngest's prepare binding carries push_options: Option<&PushOptions> (parsed (key, value) pairs, verbatim and un-interpreted; None until the config gate opens) — pinned in ADR-013 §11 and backend.md's trait description so opening the config gate later is value-additive, not a trait redesign - N-5: ls-refs=unborn verification recorded as a rider in transport.md §ls-refs + tracker task tasks/architecture/ n5-unborn-head-rider.md (unborn fixture, real client, both substrates; drop the token if it cannot be served — ADR-003) - review 001: N-4, N-5 marked resolved verification: cargo test, clippy -D warnings, fmt --check, doc — clean
11 KiB
status, last_updated
| status | last_updated |
|---|---|
| draft | 2026-09-29 |
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/gitALPN 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-ALPNGitAdapterparses, 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
gitfeature) — 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 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-archiveis 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-prefixfiltering is client-driven. - Unborn-HEAD rider (review 001 N-5):
ls-refs=unbornis 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-formaton every command; ADR-013 pins the push-side check to the same rule). - Negotiation policy: the full ack loop (ADR-014) — no-
donerounds get anacknowledgmentssection (ACK <oid>per recognized have via the backend'scommon_haves,NAKwhen none, flush; neverready), thedoneround generates closure(wants) − closure(haves) viaGitPackGen, with the boundary set the recognized subset (request haves filtered throughcommon_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 internalspawn_blockingis 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 setreport-status report-status-v2 delete-refs side-band-64k atomic ofs-delta object-format=sha1(+push-optionsunder 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 — 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_eagerlywith the repo's pack dir,.keepguard;gix-fsckconnectivity 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 viagix-reftransactions, 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-refok|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
atomicrollback 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
thiserrorenum; no panics in library code (convention 2). - At the wire-mapping point (review 001 N-2),
RegistryError::NotFoundand 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 §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 | Session boundary | duplex + stateless entry points |
| 003 | V2-first | honest advertisement; V0/V1 declined on fetch, push V0-framed (ADR-013) |
| 004 | Pack pipeline | generation on blocking threads, O(counts) |
| 005 | Substrate types | request reader, sideband sink, http framing rules |
| 009 | Budgets | Limits in every session tuple |
| 010 | Pure protocol crate | wire layer is backend-trait-only |
| 013 | receive-pack | V0-framed push machine, thin-pack acceptance, unpack-first CAS |
| 014 | Negotiation | ack loop, no ready, wait-for-done stays |
| 016 | Native session preamble | {repo, service} open-op params, request-line preamble, service in the tuple |
| 017 | Consumer half | GitSession typed client (ls_refs/fetch/push), custom alkcall Transport + gix-protocol, hand-rolled push |
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)