- new ADR-016: channels open-op params pinned as {repo, service}
(channels/git/sub, additionalProperties: false); direct-ALPN GitAdapter
parses the git-daemon request line (POC-1-verbatim grammar, capture-
backed); session tuple gains the service dimension on both substrates;
GitSession mirrors the shapes; version deliberately stays out of the
preamble (service fully determines the state machine)
- amend ADR-002/005/010 (tuple, substrate inputs, open-op params pin) and
transport.md/doors.md/overview.md/backend.md accordingly
- add the ADR-016 wire shapes to OQ-03's freeze inventory; note in
AGENTS.md convention 9 that the alkgit-specific framing now exists and
is pinned
- review 001: A-3 marked resolved
verification: cargo test, clippy -D warnings, fmt --check, doc — clean
196 lines
9.9 KiB
Markdown
196 lines
9.9 KiB
Markdown
---
|
||
status: draft
|
||
last_updated: 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/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.
|
||
|
||
### 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 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 (`<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_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 <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`, 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 |
|
||
|
||
## 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) |