diff --git a/Cargo.toml b/Cargo.toml index 73212ce..033b5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,13 @@ gix-object = { version = "0.64", optional = true, features = ["sha1"] } gix-fsck = { version = "0.25", optional = true, features = ["sha1"] } gix-hash = { version = "0.26", features = ["sha1"] } gix-packetline = "0.22" -gix-protocol = "0.65" -gix-transport = "0.59" +# Consumer half (ADR-017): gix-protocol with its async client (handshake + +# ls-refs + fetch machinery) driven by a custom alkcall Transport impl +# (gix-transport's client::Transport is the transport-agnostic seam); +# the push client is hand-rolled to ADR-013's shapes (gitoxide has no +# send-pack client). +gix-protocol = { version = "0.65", features = ["async-client"] } +gix-transport = { version = "0.59", features = ["async-client"] } tokio = { version = "1", default-features = false, features = ["rt", "sync", "io-util", "macros", "time"] } tokio-util = { version = "0.7", features = ["compat"] } futures = "0.3" diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 7a942aa..cc92b8f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -27,7 +27,9 @@ blocks nothing in v1). ADR-015 resolved review 001's A-1 (the repo-op gate) with the `manage` grant tier; ADR-016 resolved its A-3 (the native session preamble — `{repo, service}` open-op params, the git-daemon request line on the direct path, and the -service dimension in the session tuple). +service dimension in the session tuple); ADR-017 resolved its A-5 (the +consumer half: `GitSession` is a real typed fetch/push client in v1 — +the direct-connection primitive for the p2p replicator deployment). ## Architecture Documents @@ -59,6 +61,7 @@ service dimension in the session tuple). | [014](decisions/014-v2-negotiation-ack-loop.md) | V2 negotiation ack loop (no `ready`, wait-for-done stays) | Accepted | | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant tier + repo-op gate (admin scope OR manage grant) | Accepted | | [016](decisions/016-native-session-preamble.md) | Native session preamble (`{repo, service}` params, request line, service in tuple) | Accepted | +| [017](decisions/017-consumer-half-git-session.md) | Consumer half — `GitSession` typed fetch/push client (custom alkcall Transport + gix-protocol, hand-rolled push) | Accepted | ## Open Questions diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index 868a108..4e7f63f 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -130,8 +130,8 @@ Crate-root re-exports (the alktty pattern): backend traits + types function, the authorized-repo marker type (the door constructs it only after the ADR-007 check passes — session tuples carry it, transport.md substrate layer), and the `git/repo/*` op spec+handler pairs), -`GitAdapter`/ -`register_openable` (producer), `GitSession` (consumer), substrate +`GitAdapter`/`register_openable` (producer), `GitSession` (consumer — +the ADR-017 typed client: `ls_refs`/`fetch`/`push`), substrate types, `Limits`, protocol error enums; feature types (`GixBackend` family under `gix`, the file registry under `registry-file`) exported under their features. The embedder-facing freeze point remains OQ-03 diff --git a/docs/architecture/decisions/010-pure-protocol-crate.md b/docs/architecture/decisions/010-pure-protocol-crate.md index 170e2fe..432e891 100644 --- a/docs/architecture/decisions/010-pure-protocol-crate.md +++ b/docs/architecture/decisions/010-pure-protocol-crate.md @@ -64,7 +64,10 @@ template: what lets the gate run the correct `authorize` action per ADR-011/015). - **Consumer half**: `GitSession` typed client with `connect_direct` and `open_via_channels` constructors — both send the ADR-016 preamble - (request line / open-op params) including the service. + (request line / open-op params) including the service. Specified by + ADR-017: real typed client in v1 (`ls_refs`/`fetch`/`push`; fetch via + gitoxide's client machinery over a custom alkcall transport, push + hand-rolled to ADR-013's shapes, storage-agnostic). - **Backend traits** (registry, refs, pack-gen, pack-ingest) in-crate; `gix` implementation behind the default-on `gix` feature (disable it to embed your own storage). diff --git a/docs/architecture/decisions/012-registry-backing-and-ops.md b/docs/architecture/decisions/012-registry-backing-and-ops.md index 7cfc1bb..e22136a 100644 --- a/docs/architecture/decisions/012-registry-backing-and-ops.md +++ b/docs/architecture/decisions/012-registry-backing-and-ops.md @@ -152,11 +152,13 @@ Ops (names indicative, schema-stable once published): not facade machinery. Verified at implementation; the facade comes back only if a concrete need appears. - `gix-protocol`/`gix-transport` (client-side) remain in the wire - layer's dependency set only pending the consumer-half decision - (`GitSession` may reuse `gix-protocol`'s response parsing or - hand-roll the small client surface; `gix-transport` is almost - certainly droppable). Implementation-time call, recorded, not an - architecture commitment. + layer's dependency set only pending the consumer-half decision. + *(Resolved by ADR-017: `GitSession` is a real typed client in v1 — + fetch reuses `gix-protocol`'s client machinery over a custom alkcall + transport (`async-client` feature), push is hand-rolled to ADR-013's + shapes; `gix-transport` is kept as the custom-transport seam the + fetch client plugs into. ADR-012 §4's "not an architecture + commitment" rider is superseded.)* ### 5. The recorded future trigger for a crate split diff --git a/docs/architecture/decisions/017-consumer-half-git-session.md b/docs/architecture/decisions/017-consumer-half-git-session.md new file mode 100644 index 0000000..e81bc8c --- /dev/null +++ b/docs/architecture/decisions/017-consumer-half-git-session.md @@ -0,0 +1,181 @@ +# 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 \ No newline at end of file diff --git a/docs/architecture/doors.md b/docs/architecture/doors.md index da893c7..30a5f7d 100644 --- a/docs/architecture/doors.md +++ b/docs/architecture/doors.md @@ -96,8 +96,10 @@ zero door code — POC-1 is that shape verbatim. On the direct-ALPN path (`git-upload-pack \0host=…\0\0version=2\0` / `git-receive-pack \0host=…\0`) as the in-band preamble (ADR-016). Any alkcall-speaking client (including alkgit's own consumer half, -`GitSession`) can use either path. This is the baseline path; the -http/ssh doors are conveniences layered on top for stock git clients. +`GitSession` — ADR-017's typed fetch/push client) can use either path. +This is the baseline path; the http/ssh doors are conveniences layered +on top for stock git clients, and the replicator path of the p2p +deployment (direct push/pull between replicators) rides it directly. ## Assembly (downstream responsibility) @@ -124,6 +126,7 @@ deployment's docs, not here. | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | one round per POST; ack section is per-round, stateless | | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage op gate | | [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 — the direct-connection push/pull primitive | ## Open Questions diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index cb926ca..7952679 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -30,7 +30,7 @@ Single crate `alkgit`: | Half | Contents | POC evidence | |---|---|---| | Producer | `GitAdapter` (`alk/git` ALPN via alkcall `ProtocolHandler`; parses the ADR-016 request-line preamble), channels `register_openable` (`channels/git/sub` — open-op params `{repo, service}`, the negotiation + service selector + ACL point; ADR-016) | POC-1 verbatim | -| Consumer | `GitSession` typed client (`connect_direct`, `open_via_channels`) — sends the same ADR-016 preamble shapes | new, small (TtySession analog) | +| Consumer | `GitSession` typed client (`connect_direct`, `open_via_channels`) — ADR-017: `ls_refs`/`fetch`/`push`; fetch via gitoxide client machinery over a custom alkcall Transport, push hand-rolled to ADR-013's shapes; the replication/mirroring primitive for alknet | new, specified (grammar capture-grounded: ADR-013/014 + captures) | | Substrate | duplex session + stateless request/response layer (ADR-005); wire framing, V2 state machines (ADR-003) | POC-1, POC-3 | | Backends | `GitRegistry` (+ write supertrait), `GitRefs`, `GitPackGen`, `GitPackIngest` traits; impls behind the default-on `gix` (engine) and `registry-file` (records) features | POC-2 (gix impl) | | Management ops | `git/repo/*` call ops over `GitRegistryStore` (ADR-012 §3) — the JSON half alongside the `alk/git` open op (first dual-kind payload; ADR-012 §5) | thin over the store trait | @@ -94,6 +94,7 @@ multi-round negotiation are design-complete against real-client captures | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` | | [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage gate, create seeds manage | | [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, custom alkcall Transport + gix-protocol fetch, hand-rolled push | ## Open Questions @@ -103,7 +104,8 @@ Key questions tracked in [open-questions.md](open-questions.md): and the trait family enter it; ADR-012, ADR-013/014's trait additions; the ADR-015 three-action grant shape must land in it; the ADR-016 native preamble wire shapes — `{repo, service}` params schema and the - request-line grammar — entered it). + request-line grammar — entered it; ADR-017's `GitSession` public API + enters it). - **OQ-05**: sha256 policy (deferred(scope), low). - **OQ-16**: grant-key identity namespace (deferred(scope); blocks nothing in v1 — ADR-015 §7). diff --git a/docs/architecture/transport.md b/docs/architecture/transport.md index 1f2726e..daa6f3a 100644 --- a/docs/architecture/transport.md +++ b/docs/architecture/transport.md @@ -154,10 +154,13 @@ a partial ref list is the worst failure mode an advertisement can have 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**. +`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 @@ -172,6 +175,7 @@ Publish-freeze point: **OQ-03**. | [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 | ## Open Questions diff --git a/docs/reviews/001-architecture-pre-decomposition-review.md b/docs/reviews/001-architecture-pre-decomposition-review.md index 9a806d3..e583a68 100644 --- a/docs/reviews/001-architecture-pre-decomposition-review.md +++ b/docs/reviews/001-architecture-pre-decomposition-review.md @@ -762,7 +762,7 @@ criticals are ADR-writing work, not code): | A-2 | `async fn` traits not dyn-compatible | ADR-012 §1 + backend.md amendment: `#[async_trait]`; add `async-trait = "0.1"` to manifest | small | none | **resolved** — all five traits `#[async_trait]`, desugared boxed form pinned in the freeze inventory (OQ-03), dep in manifest | | A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | **resolved (ADR-016)** — `{repo, service}` open-op params (`channels/git/sub`, `additionalProperties: false`), git-daemon request line on the direct path (POC-1 verbatim, freeze inventory), service in both substrate tuples, `GitSession` mirrors the shapes | | A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = `common_haves`-filtered haves | trivial | none | **resolved** — boundary set is the recognized subset (`common_haves`-filtered), amendment clause in ADR-014 §2 + transport.md §fetch | -| A-5 | consumer half unspecified | user scope decision, then amendment or small ADR (recommended: thin wrapper, deps carried with purpose) | small | scope | open | +| A-5 | consumer half unspecified | user scope decision, then amendment or small ADR (recommended: thin wrapper, deps carried with purpose) | small | scope | **resolved (ADR-017)** — superseding the thin-wrapper recommendation: `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); fetch reuses gix-protocol over a custom alkcall `Transport`, push is hand-rolled to ADR-013's shapes (gitoxide has no send-pack), storage-agnostic | | A-6 | trait execution model unspecified | backend.md paragraph + transport.md rephrase: async traits, wire-layer permit, impl-internal spawn_blocking | small | none | **resolved** — backend.md concurrency model: wire layer enforces the ADR-009 permit around gen/ingest trait calls; impls own internal `spawn_blocking` (ADR-009/ADR-013 aligned) | | D-1 | stale Internal-ops + OQ-list text | supersession notes (vision, alk-stack, AGENTS) | trivial | none | **resolved** — supersession notes in vision.md (with ADR-015) and alk-stack.md item 2; AGENTS.md active-OQ list updated to OQ-03/05/16 | | D-2 | ADR-007 step-3 mechanism superseded | amendment note on ADR-007 | trivial | none | **resolved** — amendment note on ADR-007 step 3 (mechanism → ADR-011 `authorize`; step order unchanged) |