# ADR-017: The consumer half — `GitSession` is a real typed client in v1 ## Status Accepted (resolves review 001 A-5; amends ADR-010's consumer-half sentence and resolves ADR-012 §4's deferred `gix-protocol` call) ## Context ADR-010 named the consumer half — "`GitSession` typed client with `connect_direct` and `open_via_channels` constructors … the alkcall-native primitive for replication/mirroring in the alknet rewrite" — but no document specified what it *does*, and review 001 (A-5, major) found the largest uncut ambiguity for decomposition: either the half is silently omitted (the headline claim unimplemented) or its scope is invented. The review's initial reading ("thin wrapper like `TtySession`") does not survive the use cases. The consumer half's downstream is the alk system's two deployment targets: 1. **Self-hosted git platform** (the gitea-like app): stock git clients over ssh/http doors; the consumer half is not on this path. 2. **The p2p github-like platform**: replicators push and pull over direct alkcall connections. A replicator is a *full git client* — pull is wants/haves negotiation + pack application; push is V0 advertisement parsing + command/pack send + report parsing. There is no "no downstream yet" hedge here: the replicator is the named downstream, and the client protocol layer *is* the lower-level machinery it requires. Delegating pkt-line framing and machine state to every replicator workstream would repeat the exact mistake ADR-005 exists to prevent on the server side ("if adapters hand-roll the framing, each gets it subtly wrong" — the same principle, client side). The manifest already carries `gix-protocol` and `gix-transport` unconditionally (ADR-012 §4 had deferred "use `gix-protocol`'s response parsing or hand-roll the small client surface" to implementation time), so the scope question also decided what those deps were for. Source verification against the reference clone (0.87.1-equivalent) settled the build-vs-reuse split: - `gix-protocol` models the **fetch/ls-refs client completely**: `Command::{LsRefs, Fetch}`, `handshake()`, the full negotiation state machine (`gix-negotiate`), and response parsing — driven by any `gix_transport::client::Transport` impl. - `gix-transport`'s client side is **transport-agnostic by explicit design**: `client::Transport` is a trait (`handshake` + `request`), built so a custom transport can plug in. A transport over an alkcall duplex stream is exactly its intended use. - **Nothing in gitoxide models the send-pack (push) client at all** — a push client is hand-rolled regardless of this decision. The client grammar is capture-grounded without new capture work: the push client's target grammar is pinned by `push-captures.md` (real git pushes, ADR-013's normative basis) and the fetch client's by `negotiation-captures.md` (ADR-014's) — the client machines speak the same grammar the captures record and ADR-013/014 serve. ## Decision **`GitSession` is a real typed git client in v1: both constructors, the ADR-016 preamble, and `ls_refs`/`fetch`/`push` operations — fetch reusing gitoxide's client machinery over a custom alkcall transport, push hand-rolled to ADR-013's shapes, storage-agnostic throughout.** 1. **Operations, not raw access.** `GitSession` exposes: - `ls_refs` — list remote refs (V2 `ls-refs` command, peel/symrefs/ ref-prefix arguments). - `fetch` — wants + optional haves/negotiation round(s) → pack. Full ack-loop negotiation (no-`done` rounds per ADR-014's grammar — the client side of the loop we serve); `done` whenever the caller has the necessary refs locally (the common replicate-everything case needs no multi-round negotiation at all). - `push` — ref updates + pack → server report (per-remote-ref `ok|ng`, `unpack ok|ng`), `atomic` and `push-options` as caller- supplied options within the ADR-013 grammar. Each operation runs to completion (or error) on the session; the duplex session persists across operations (ALK duplex — no statelessness on the native path). 2. **A `gix_transport::client::Transport` impl over the alkcall duplex stream drives the gitoxide client.** One impl, two construction modes: `open_via_channels` (the preamble is the open-op's `{repo, service}` params — the transport's handshake writes nothing; alkcall carries it) and `connect_direct` (the transport's handshake writes ADR-016's git-daemon request line onto the stream, then yields the response to `gix-protocol::handshake`). gix-protocol is activated with its `async-client` feature (all transitively-pinned crates share the manifest's MSRV 1.88 — verified). `gix-transport`'s async client traits are `#[async_trait(?Send)]` — a session drives one operation at a time on one task; the runtime bound is stated as the design shape, matching how a replicator uses a remote (an operation per remote at a time). The manifest's two unconditional deps become honest with this ADR: this is what they were for. 3. **Push is hand-rolled** (gitoxide has no send-pack): a mirror of ADR-013's server shapes at the same honesty bar — parse the V0 ref advertisement (`capabilities^{}` sentinel for empty repos), send command lines (old/new/ref with the correct zero-id create/delete shapes), optional push-options section, pack stream, then parse the status report in both framings (sideband band-1-wrapped and bare — the same distinction ADR-013 §8 pins server-side). Pack **generation** is not re-implemented: the caller supplies the pack (see 5) — for a replicator using the `gix` feature, pack generation is the same `GitPackGen`-class primitive the server half uses. Recorded as a candidate for upstream `gix-protocol` contribution later; not a dependency of this crate's design on it. 4. **Storage-agnostic: packs stream both ways; `GitSession` never touches an object store.** `fetch` delivers the sideband-unwrapped pack bytes to a caller-supplied consumer (the replicator/applies it to its own odb — with the `gix` feature, `Bundle::write_to_directory`-class ingestion is available, but alkgit does not couple to it). `push` takes a caller-supplied pack stream + the caller-computed commands. Client-side want/have computation, ref bookkeeping, and local-merge policy are the application's; `GitSession` is the transport + grammar layer, not a git client policy engine (the app-level cases — remote-tracking refs, shallow policy UI, tag auto-follow — are the app's, on top). 5. **No credentials in the session.** Identity is established by the alkcall transport before the preamble (the session tuple's identity input, ADR-016); the preamble carries no credential material; no client-side credential helper exists (the no-secret-material invariant; alkvault handles any replication credentials at the app layer). The gix-protocol handshake's credentials callback is never wired — the alkcall paths authenticate at the transport, not at git's credentials-protocol layer. 6. **Bounded like every other session.** Client operations carry `Limits` (ADR-009): negotiation round budgets, wall-clock, max response pack size (a malicious or stalled peer server is the internet-facing threat; a hub must not stall a replicator unboundedly). Client-side breaches end the session with an error — the same fail-closed discipline as the server side. ## Consequences - **Positive:** the p2p replicator downstream has its primitive — a replicator task is `open_via_channels`/`connect_direct` + `fetch`/ `push` against its own storage, with no pkt-line exposure; ADR-010's producer/consumer claim becomes fully specified; the manifest deps carry purpose; client-vs-our-server integration tests are cheap (in-process, both halves in one test) — a verification surface neither the server-only design nor a real-git-only client would have; ADR-012 §4's deferred decision is resolved with the manifest unchanged. - **Negative:** v1's implementation surface grows — two client machines (fetch via gitoxide integration, push hand-rolled) each need integration tests against our server half and captures against real git (for the push client's grammar fidelity to upstream behavior); the `async-client` feature of gix-protocol pulls its client-side tree (futures-lite, gix-negotiate, gix-credentials — MSRV-verified); the push client is a surface we own and maintain upstream-independent. - **Neutral:** the consumer half is wire-layer (no feature flag — it compiles with `default-features = false`); ADR-012 §4's "recorded, not an architecture commitment" rider is superseded by this ADR (an architecture commitment is exactly what was missing); ADR-010's consumer-half bullet is amended in place. ## References - Review 001 A-5 (the trigger; its option (c) direction, grounded here in the two deployment use cases rather than the review's (a) recommendation), OQ-03 (the freeze inventory this ADR's public API enters) - ADR-010 (the producer/consumer structure — consumer half now specified), ADR-016 (the preamble `connect_direct`/`open_via_channels` send), ADR-013 (the push grammar the hand-rolled client mirrors), ADR-014 (the negotiation grammar the fetch client speaks), ADR-005 (the no-hand-rolled-framing principle this ADR applies client-side), ADR-009 (bounded sessions, client-side), ADR-012 §4 (the deferred `gix-protocol` call this resolves; the feature-split story unchanged) - gix-protocol 0.65 (`Command::{LsRefs,Fetch}`, `handshake`, `fetch`, `Arguments`, response parsing — `async-client` feature), gix-transport 0.59 (`client::Transport` trait + `Service`/`Protocol` — the transport-agnostic client seam; `#[async_trait(?Send)]` mode) - alk-stack context: the two named downstreams (self-hosted platform app; p2p replicators with donation-based on-chain ACL/naming, off-chain git data) — vision.md is the record of the platform use cases - doors.md §"The alkcall-native path", backend.md §public API, transport.md §public API, overview.md §crate map