Files
alkgit/docs/architecture/decisions/017-consumer-half-git-session.md
T
glm-5.3-flash 82653c31b6 docs(architecture): ADR-017 — consumer half, GitSession as typed client (review 001 A-5)
- new ADR-017: GitSession is a real typed client in v1 (ls_refs/fetch/
  push), grounded in the two deployment use cases — the p2p replicator
  is the named downstream and needs the client protocol layer; the
  thin-wrapper reading is superseded
- fetch client reuses gix-protocol (async-client) over a custom
  alkcall gix_transport::client::Transport impl (handshake writes the
  ADR-016 request line on the direct path; the channels open-op params
  carry it otherwise); push hand-rolled to ADR-013's shapes (gitoxide
  has no send-pack client)
- storage-agnostic: packs stream both ways to caller-owned consumers;
  no in-session credentials (alkcall transport authenticates); client
  sessions carry ADR-009 Limits (client is also internet-facing)
- manifest: gix-protocol/gix-transport gain async-client features
  (verified against published tree, MSRV 1.88)
- amend ADR-010 (consumer-half bullet) and ADR-012 §4 (the deferred
  gix-protocol call — resolved; rider superseded); backend/transport/
  overview/doors wording; review 001 A-5 marked resolved

verification: cargo check (default, --all-features, --no-default-features,
--features sha256), cargo +1.88 check, cargo test, clippy
-D warnings, fmt --check — clean across the matrix
2026-09-29 09:24:47 +00:00

10 KiB

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