From 8f73da5d12d4bd3cee9160f4472cd4af4629db54 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Mon, 21 Sep 2026 03:55:33 +0000 Subject: [PATCH] =?UTF-8?q?docs(architecture):=20phase=201=20bootstrap=20?= =?UTF-8?q?=E2=80=94=20specs,=209=20ADRs,=20OQ=20tracker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture documentation structure per sdd_process phase 1: - README index (doc table, ADR table, lifecycle), overview with crate map, dependency rules, and security invariants - Component specs: storage, transport, http, ssh, alkgitd (all draft) - ADRs 001-009: crate decomposition, front-door-blind core, V2-first protocol, pack pipeline (data::output generation / data::input ingestion), session substrate types, http adapter composition (proposed, OQ-01), ACL-before-advertisement, registry-resolved repo identity, bounded-resources budgets - open-questions.md: OQ-01..08 with two deferred(scope), one deferred(unclear), door-type definitions, blocker tracker tasks in tasks/architecture/ - v1 ssh-door decision recorded: russh terminates wire SSH in alkgitd; alkcall channels stay the internal substrate (OQ-03 partially resolved) Two review rounds (fresh-context subagent): 4 critical + 17 warnings fixed in round one; zero critical + 4 warnings + 5 suggestions fixed in round two. All ADR/OQ cross-references verified resolving. --- docs/architecture/README.md | 65 +++++++ docs/architecture/alkgitd.md | 96 ++++++++++ .../decisions/001-crate-decomposition.md | 59 ++++++ .../decisions/002-front-door-blind-core.md | 73 +++++++ .../decisions/003-protocol-v2-first.md | 82 ++++++++ .../decisions/004-pack-pipeline.md | 80 ++++++++ .../decisions/005-session-substrate-types.md | 71 +++++++ .../decisions/006-http-adapter-composition.md | 91 +++++++++ .../decisions/007-acl-before-advertisement.md | 64 +++++++ .../008-registry-resolved-repo-identity.md | 46 +++++ .../decisions/009-bounded-resources-budget.md | 64 +++++++ docs/architecture/http.md | 96 ++++++++++ docs/architecture/open-questions.md | 178 ++++++++++++++++++ docs/architecture/overview.md | 119 ++++++++++++ docs/architecture/ssh.md | 106 +++++++++++ docs/architecture/storage.md | 117 ++++++++++++ docs/architecture/transport.md | 114 +++++++++++ tasks/architecture/oq-03-embedder.md | 35 ++++ tasks/architecture/oq-04-receive-pack.md | 42 +++++ tasks/architecture/oq-05-sha256.md | 34 ++++ tasks/architecture/oq-06-metadata-backing.md | 37 ++++ 21 files changed, 1669 insertions(+) create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/alkgitd.md create mode 100644 docs/architecture/decisions/001-crate-decomposition.md create mode 100644 docs/architecture/decisions/002-front-door-blind-core.md create mode 100644 docs/architecture/decisions/003-protocol-v2-first.md create mode 100644 docs/architecture/decisions/004-pack-pipeline.md create mode 100644 docs/architecture/decisions/005-session-substrate-types.md create mode 100644 docs/architecture/decisions/006-http-adapter-composition.md create mode 100644 docs/architecture/decisions/007-acl-before-advertisement.md create mode 100644 docs/architecture/decisions/008-registry-resolved-repo-identity.md create mode 100644 docs/architecture/decisions/009-bounded-resources-budget.md create mode 100644 docs/architecture/http.md create mode 100644 docs/architecture/open-questions.md create mode 100644 docs/architecture/overview.md create mode 100644 docs/architecture/ssh.md create mode 100644 docs/architecture/storage.md create mode 100644 docs/architecture/transport.md create mode 100644 tasks/architecture/oq-03-embedder.md create mode 100644 tasks/architecture/oq-04-receive-pack.md create mode 100644 tasks/architecture/oq-05-sha256.md create mode 100644 tasks/architecture/oq-06-metadata-backing.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..90845f6 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,65 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# alkgit Architecture + +Phase 1 (SDD) output for alkgit — the self-hosted, single-binary git server +built on the alk stack (alkcall, alkhttp, alktls, alkvault) and gitoxide. +Phase 0 research lives in [docs/research/](../research/README.md); every +design claim here traces to a POC finding or research doc, or is flagged as +an open question. + +## Current State + +Phase 1 is starting. All architecture documents below are `draft` +(ADR-006 additionally carries a Proposed ADR status pending OQ-01). +POC-1/2/3 validated the git protocol half end-to-end against real git 2.43; +the remaining design work is shape work (adapter composability, metadata/ +registry backing, admin surface, receive-pack). + +## Architecture Documents + +| Doc | Area | Status | +|---|---|---| +| [overview.md](overview.md) | Cross-cutting: crate map, dependency rules, security invariants | draft | +| [storage.md](storage.md) | `alkgit-core`: registry, refs, odb, pack generate/ingest, ACL types | draft | +| [transport.md](transport.md) | `alkgit-transport`: pkt-line sessions, V2 state machine, upload/receive-pack | draft | +| [http.md](http.md) | `alkgit-http`: smart-http adapter over alkhttp | draft | +| [ssh.md](ssh.md) | `alkgit-ssh`: git-command dispatch (wire SSH terminated by russh in alkgitd) | draft | +| [alkgitd.md](alkgitd.md) | `alkgitd`: binary assembly, config, TLS/ACME, serving loops | draft | +| [open-questions.md](open-questions.md) | Centralized OQ tracker | — | + +## ADRs + +| ADR | Decision | Status | +|---|---|---| +| [001](decisions/001-crate-decomposition.md) | Workspace crate decomposition (5 crates) | Accepted | +| [002](decisions/002-front-door-blind-core.md) | Front-door-blind core: session boundary (identity, repo, stream, limits) | Accepted | +| [003](decisions/003-protocol-v2-first.md) | Protocol V2-first with honest capability advertisement | Accepted | +| [004](decisions/004-pack-pipeline.md) | Pack generation/ingestion pipeline (gitoxide `data::output`) | Accepted | +| [005](decisions/005-session-substrate-types.md) | Session substrate types (duplex + stateless APIs, request reader) | Accepted | +| [006](decisions/006-http-adapter-composition.md) | HTTP adapter composition (alkgit-owned router factory) | Proposed | +| [007](decisions/007-acl-before-advertisement.md) | ACL runs before any advertisement/ref line | Accepted | +| [008](decisions/008-registry-resolved-repo-identity.md) | Wire repo names are registry IDs, never paths | Accepted | +| [009](decisions/009-bounded-resources-budget.md) | Bounded-resources budget model (limits on every session) | Accepted | + +Note: ADR-001's crate table and ssh.md record the v1 ssh-door decision +(russh terminates wire SSH in alkgitd; OQ-03 covers embedder variants). + +## Open Questions + +All unresolved questions are tracked in [open-questions.md](open-questions.md) +with stable OQ-IDs, priorities, and cross-references. Highest-priority opens: +OQ-01 (http adapter home / composability, blocks ADR-006 finalization), +OQ-06 (metadata store backing), OQ-08 (identity sources per front door). + +## Document Lifecycle + +| Status | Meaning | Transitions | +|---|---|---| +| `draft` | Under active development; may change significantly | → `reviewed` when its OQs are resolved | +| `reviewed` | Architecture final; implementation may begin; changes need review | → `stable` when implementation verified | +| `stable` | Locked; changes require review, may warrant an ADR | → `deprecated` when superseded | +| `deprecated` | Superseded; kept for reference | Removed when no longer referenced | \ No newline at end of file diff --git a/docs/architecture/alkgitd.md b/docs/architecture/alkgitd.md new file mode 100644 index 0000000..d5a09a4 --- /dev/null +++ b/docs/architecture/alkgitd.md @@ -0,0 +1,96 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# alkgitd: Server Binary + +## What it is + +The assembly point: config parsing, listener setup (TLS/ACME), vault +wiring, the admin surface, and the glue that turns config into running +adapters. The only crate that knows the whole graph (ADR-001). No business +logic lives here. + +## Assembly shape + +For each listener (http and/or ssh), the binary: + +1. Builds the identity provider (per-door; **OQ-08** decides mechanics). +2. Builds the registry (core trait + chosen backing; **OQ-06** decides + backing) and the storage roots from config. +3. Builds the transport hooks + `Limits` from config (ADR-009 defaults + + overrides). +4. http door: constructs the alkgit-http router factory (ADR-006) and + merges via `HttpAdapter::with_extra_routes`; wraps in alktls per + config; `Arc` per accept task (POC-3 usage note). +5. ssh door: russh listener terminates SSH; alkgit-ssh dispatches the + exec requests (ssh.md §client compatibility). +6. Vault (alkvault) wired for any credential material; metadata/config + hold vault references only (convention 4/5). + +## Config schema (v1 shape) + +Server-facing values (registry backing location, storage roots, per-door +listeners/ports, TLS/ACME, limits overrides, vault path). The schema's +final shape depends on **OQ-06** (backing) and **OQ-08** (identity +config); the skeleton above is the stable frame. + +Feature flags (convention 10): `sha256` (passthrough), `acme` (alktls +wiring). Base binary compiles lean; both feature sets verified in CI +(AGENTS.md verification commands). + +## Admin API (scope: OQ-07) + +- alkcall ops with `Visibility::Internal`, served over an admin-only + listener — never on the git traffic surface (alk-stack.md §gitea + lesson; vision threat-model notes). +- Candidate v1 op set (repo create/delete, visibility, ACL grant/revoke) + and its exact shapes: **OQ-07** — a dedicated session once the identity + model (OQ-08) exists, since op shapes depend on what identities mean. +- A downstream app may replace this surface entirely (vision + composability): the ops are alkcall ops, not embedded web endpoints. + +## TLS/ACME + +- alktls composition: `Connection::from_bidi(TlsStream, alpn)` — the + POC-1/3-validated alkcall entry shape. The http door rides TLS with + ALPN `http/1.1`/`h2`. The ssh door is a plain TCP listener + (stock `git clone ssh://` clients do not do TLS) — russh terminates SSH + directly on it (ssh.md); alkcall `Connection::from_stream` wraps the + post-auth stream when the adapter needs alkcall types. +- `acme` feature: alktls ACME state machine wired to config for the http + listener (alk-stack.md lists this as "likely trivial"; risk note: if the + wiring surprises, that becomes a research task before alkgitd + implementation — not silently absorbed). + +## Serving loops + +- Per-connection accept → `ProtocolHandler::handle` (alkcall ADR-002) + → adapter dispatch → transport session. +- Server-wide blocking-pool budget for pack generation (ADR-009): + accepted/rejected at assembly time. +- Graceful shutdown: listener stop + session drain bounded by the + session wall clock (ADR-009). + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [001](decisions/001-crate-decomposition.md) | Crate decomposition | alkgitd = assembly only | +| [006](decisions/006-http-adapter-composition.md) | Router factory | alkgitd consumes the factory like any embedder | +| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | storage roots configured server-side here | +| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits overrides live in config | + +## Open Questions + +- **OQ-06**: registry backing (deferred(scope)). +- **OQ-07**: admin op set (open — after OQ-08). +- **OQ-08**: identity providers per door (open). + +## References + +- `docs/research/alk-stack.md` (integration surface, version pins) +- `docs/research/poc3-findings.md` (HttpAdapter usage, extra-routes behavior) +- alkcall ADRs: 002 (ProtocolHandler), 010 (capability injection), 017 + (privilege model) \ No newline at end of file diff --git a/docs/architecture/decisions/001-crate-decomposition.md b/docs/architecture/decisions/001-crate-decomposition.md new file mode 100644 index 0000000..020e0ce --- /dev/null +++ b/docs/architecture/decisions/001-crate-decomposition.md @@ -0,0 +1,59 @@ +# ADR-001: Workspace crate decomposition (5 crates) + +## Status +Accepted + +## Context + +The workspace skeleton (created before phase 1) proposed five crates: +`alkgit-core`, `alkgit-transport`, `alkgit-http`, `alkgit-ssh`, `alkgitd`. +The decomposition has to serve two masters: the "ALPN as a service" +composability rule from `docs/research/vision.md` (downstream apps embed +core + transport and bring their own front doors) and the POC findings that +fixed the actual layering between protocol, storage, and front doors +(POC-1/2/3). + +Alternatives considered: + +- Single crate — simplest, but forces front-door code into the same + dependency graph as embedders who only want core+transport, and blocks + independent evolution of the http adapter. +- Split core into registry/metadata vs storage — premature; the registry + storage backing is still an open question (OQ-06) and the split would + bake in a boundary we may want to move. +- http+ssh in one `alkgit-frontends` crate — reduces crate count but + couples alkhttp to alkcall-channels consumers and vice versa. + +## Decision + +Keep the five-crate decomposition from the skeleton: + +- `alkgit-core` — repository storage: registry, refs, odb wrappers, pack + generate/ingest, fsck, access-rule input types. Transport-agnostic. +- `alkgit-transport` — smart protocol: pkt-line sessions, V2 capability + advertisement, ls-refs, fetch, receive-pack state machines. +- `alkgit-http` — http front door (smart-http over alkhttp). +- `alkgit-ssh` — ssh front door (git command exec dispatch; wire SSH + terminated by russh in alkgitd, or alkcall channels in embedded + variants — both converge on the adapter's dispatch). +- `alkgitd` — the binary: config, assembly, TLS/ACME, serving loops. + +Dependency edges: core ← transport ← {http, ssh} ← alkgitd. http and ssh +have no edge between them. Core and transport depend on alkcall types only +(never alkhttp/alkgit-ssh); see ADR-002 for the exact boundary. + +## Consequences + +- Downstream embedders take `alkgit-core` + `alkgit-transport` and stop + there; front-door crates are replaceable adapters. +- The binary crate carries all assembly knowledge (config schema, listener + setup, vault wiring), keeping the library crates front-door-blind. +- Crate count stays at five; any future split (e.g. metadata store) is a + new ADR. + +## References +- `docs/research/vision.md` §"ALPN as a service", §"Sub-crate shape" +- `docs/research/alk-stack.md` §"Composability boundary" +- POC findings 1–3 (layering validated end-to-end) +- ADR-002 (the core boundary) +- overview.md §"Crate map" \ No newline at end of file diff --git a/docs/architecture/decisions/002-front-door-blind-core.md b/docs/architecture/decisions/002-front-door-blind-core.md new file mode 100644 index 0000000..7d42af2 --- /dev/null +++ b/docs/architecture/decisions/002-front-door-blind-core.md @@ -0,0 +1,73 @@ +# ADR-002: Front-door-blind core — the session boundary + +## Status +Accepted + +## Context + +The load-bearing composability rule ("ALPN as a service", +`docs/research/vision.md`): `alkgit-core` + `alkgit-transport` must never +know which front door is talking. POC-1 proved the exact shape survives the +wire: `Connection::accept_bi() → BiStream → tokio::io::split → +tokio-util compat → gix-packetline` ran a full V2 fetch against real git. +POC-3 proved the stateless-http variant: the same protocol state machines +ran per-POST over (request-reader, response-writer) instead of a duplex +stream. + +The question this ADR settles: what is the *interface* the core/transport +exposes to adapters? + +Alternatives considered: + +- Adapters implement an alkcall-style `ProtocolHandler` themselves and call + into transport with raw `BiStream` — pushes too much protocol + responsibility (advertisement timing, framing errors) into every adapter. +- Transport speaks alkcall `Connection` directly — forces stateless http + (one request per connection) through a session-shaped API; wrong shape + for http, and couples transport to alkcall connection lifecycle. +- HTTP-specific abstractions in transport — violates the blind-core rule. + +## Decision + +The transport exposes two session entry points, both consuming the same +tuple (peer identity, resolved repo, limits): + +1. **Duplex session** (ssh, git://, any stream door): transport consumes a + `BiStream`-shaped duplex byte stream (`AsyncRead + AsyncWrite + Unpin`, + the alkcall `BiStream` contract, alkcall ADR-005/009) and runs the + advertise-once → command-loop state machine. Adapters hand it over after + ACL and repo resolution (ADR-007, ADR-008). +2. **Stateless session** (smart-http): transport consumes a + (request-reader, response-writer) pair per http request and runs one + command per invocation, matching smart-http's stateless framing. The + same core state machines run under both entry points. + +Storage-facing side: transport calls `alkgit-core` for advertisement data, +pack generation (want/have set in → streaming pack out), and pack +ingestion + ref CAS (receive-pack). Storage never sees pkt-lines. + +The `futures-io` bridging detail (`tokio_util::compat`), the split/compat +glue, and the packetline stop-delimiter handling live inside transport — +adapters never see `futures_io` types (POC-1 follow-ups 1–2, POC-3 +follow-up 2). + +## Consequences + +- The core+transport pair is embeddable with any front door; POC-1/3 are + existence proofs of both shapes. +- State machines are written once; http statelessness is a substrate + property, not a protocol fork. +- Transport depends on alkcall types only (`BiStream` shape, identity, + limits) — no alkhttp, no channel types below the stream. +- One cost: the stateless entry point needs explicit per-request limits + (body budget, wall clock) since there is no session to amortize them + (ADR-009). + +## References +- `docs/research/vision.md` §"ALPN as a service" +- `docs/research/poc-1-findings.md` (duplex shape validated), `docs/research/poc3-findings.md` + (stateless shape validated) +- alkcall ADR-005 (`BiStream` type), ADR-009 (BiStream as handler leaf) +- ADR-005 (substrate types detail), ADR-007/008 (what adapters do before + calling transport) +- overview.md §"Interfaces" \ No newline at end of file diff --git a/docs/architecture/decisions/003-protocol-v2-first.md b/docs/architecture/decisions/003-protocol-v2-first.md new file mode 100644 index 0000000..e3022c9 --- /dev/null +++ b/docs/architecture/decisions/003-protocol-v2-first.md @@ -0,0 +1,82 @@ +# ADR-003: Protocol V2-first with honest capability advertisement + +## Status +Accepted + +## Context + +The git smart protocol has three versions in the wild: V0 (the original), +V1 (capability line prefixed), and V2 (command-based, default since git +2.18+). POC-1 and POC-2 validated the full V2 fetch path against real git +2.43 end-to-end (advertisement, ls-refs, fetch with sideband packs) over +both duplex and http substrates. V0/V1 stateless-http is a different +framing (per-round POSTs, stateless ack re-derivation). + +The research inventory (`git-protocol.md` §"Open items") left the V0/V1 +question open: support V0/V1 on http? On ssh? Which first? + +Decision drivers: + +- Our deployment anchor (vision §"Primary deployment target") is modern + git clients over http and ssh; every git client from the last six years + speaks V2. +- Honest capability advertisement (vision principle 5) is easiest to keep + honest with fewer served shapes. +- V0/V1 stateless-http needs multi-round negotiation state re-derivation — + meaningful complexity for a client population that is effectively + extinct. + +## Decision + +**Serve protocol V2 first.** alkgit v1 serves V2 on both doors: + +- Advertisement is once-per-session (duplex) / per-request-set (http), + exactly as observed in POC-1 (re-advertising between commands hangs real + git). +- Advertised capabilities are exactly what we serve: `ls-refs=unborn`, + `fetch=wait-for-done` (v1 fetch policy is full-closure pack on `done`; + multi-round negotiation, OQ-02, may add capability values when it + lands), `object-format=sha1` (+ sha256 under the feature flag, if and + when tested — OQ-05). Nothing unimplemented is advertised + (shallow, filter, packfile-uris, object-info, server-option are + *declined by omission*; POC-1 confirmed real git accepts this). + On the push side, `git-upload-archive` is not served at all — ssh + exec requests for it get a fixed refusal (ssh.md; ADR-008's + never-execute rule). +- HTTP requests protocol V2 only; ssh requests protocol V2 only (see + below). + +**V0/V1 policy: decided now as V2-only for v1.** Both doors +(http and ssh) speak V2; clients that cannot speak V2 get a clear pkt-line +error. Reversal is wire-visible (the advertised version set is a wire +format), so treat this as effectively one-way once published — the +decision is still made now, and revisiting it needs a new ADR plus a +deprecation window for clients. The motivation stands: a concrete consumer +needing V0/V1 (e.g. very old CI images) has not been identified. +The V0/V1 receive-pack shape (used by `git push` on http stateless framing +in older clients) is unaffected by this choice: receive-pack is mostly +version-independent (see transport.md). + +Sequencing note (fixes the scope contradiction the review found): the v1 +fetch policy that ships first is full-closure-on-`done` (POC-validated). +Multi-round V2 negotiation (haves/acks without `done`) is in v1's *scope* +but lands after the done-path works end-to-end; its ack logic is OQ-02's +open question, and any capability-advertisement change it requires +happens then. + +## Consequences + +- Smallest honest surface; POC-validated shapes only. +- Old-client support is declined explicitly (error, not silence). +- The POC's observed `done`-path shape (no acknowledgments section, no + `ready`, response ends at flush) is our normative wire behavior, not the + docs' grammar — `git-protocol.md` §V2 records this. +- If V0/V1 is ever added, the state machines in transport must be + re-shaped around a version-dispatch at session start (single point, by + design — see ADR-005). + +## References +- `docs/research/git-protocol.md` §"Protocol surface inventory", §"Negotiation policy" +- `docs/research/poc-1-findings.md` (advertisement-once, capability declination), POC-3 (http V2 framing) +- ADR-005 (substrate types), ADR-007 (ACL before advertisement) +- transport.md, http.md, ssh.md \ No newline at end of file diff --git a/docs/architecture/decisions/004-pack-pipeline.md b/docs/architecture/decisions/004-pack-pipeline.md new file mode 100644 index 0000000..2e714fa --- /dev/null +++ b/docs/architecture/decisions/004-pack-pipeline.md @@ -0,0 +1,80 @@ +# ADR-004: Pack pipeline — gitoxide `data::output` generation, `data::input` streaming ingestion + +## Status +Accepted + +## Context + +Pack generation and ingestion are the two halves of fetch and receive-pack. +POC-2 resolved the generation question empirically; the ingestion tool +turned out to be mislabeled in the original research plan (the plan called +it `bundle::write` — the correct tool is `data::input` streaming) and was +corrected there. + +Generation options were: + +a. `gix-pack::bundle::write` into a temp dir, read back — **wrong tool**: + it is the index-from-stream machinery (consumes an *existing* pack + stream, mmaps to resolve deltas, always writes files). Not a generation + path. +b. `gix-pack::data::output::bytes` fed by an odb walk — validated live: + streams to any `io::Write` with O(counts) memory (60k-object pack → + 5.3 MB out, ~11 MB extra RSS), real git 2.43 clones fsck-clean. + +Ingestion (receive-pack): client sends a possibly-thin pack stream; server +must index it, fsck it, and apply ref updates via CAS. + +## Decision + +**Fetch-side generation** is gitoxide's own pipeline, composed exactly as +`gitoxide-core/src/pack/create.rs` does (POC-2's validated composition): + +``` +peel wants to commit tips + → commit-ancestry walk (gix_traverse::commit::Simple, Parents::All) + [+ non-commit tips as-is] + → count::objects(_unthreaded, TreeContents) [per-commit tree expansion] + → entry::iter_from_counts [chunked deflate + pack-delta copy] + → InOrderIter → bytes::FromEntriesIter (V2, sha1) + → io::Write sink (sideband on the wire, or http response body) +``` + +Two-stage closure is mandatory: `TreeContents` does **not** follow commit +parents; feeding tip commits alone produces incomplete packs (POC-2's +course correction). The odb handle needs `prevent_pack_unload()` + +`ignore_replacements = true`. `gix_odb::Cache` is not `Sync`: share +`Arc`, build a handle per session, generate on blocking threads. + +Missing objects mid-generation must **abort** the fetch (sideband error +band), never emit a broken pack — check entry statistics +(`missing_objects > 0`). + +**Receive-side ingestion** parses the client pack stream +(`gix-pack::data::input` with `streaming-input`), fscks it +(`gix-fsck` connectivity), and applies ref updates via `gix-ref` +transaction CAS. The exact push state machine (capability advertisement +set, CAS timing, status report, http framing) is OQ-04's investigation +target; the ingestion-tool choice above is decided. + +**Delta synthesis** for loose objects is an optimization backlog item, not +a v1 commitment: existing pack deltas copy through for free; loose objects +ship as compressed bases. No upstream delta-encode API exists +(`gix-delta` is apply-only). + +## Consequences + +- Fresh clones of loose-ish repos ship uncompressed bases (fine for v1; + clients re-pack at rest); repos kept packed get pack-copy efficiency. +- Memory stays O(counts); streaming under back pressure is proven on the + http path (POC-3: flat RSS under a 650 KB/s reader). +- Determinism: `objects_unthreaded` gives deterministic order; threaded + count + `InOrderIter` is the scale path later. +- `alkgit-core` exposes this as a type taking (odb handle, wants, haves) → + streaming pack; negotiation-agnostic (POC-2 follow-up 1). + +## References +- `docs/research/poc2-findings.md` (the whole basis), `docs/research/poc3-findings.md` (streaming proof) +- `docs/research/gitoxide.md` §"Storage" (generation-pipeline notes) +- `docs/research/git-protocol.md` §"Server-side pack generation" +- ADR-005 (how the sink reaches the wire), ADR-009 (memory/time budgets) +- transport.md §fetch, storage.md §packs \ No newline at end of file diff --git a/docs/architecture/decisions/005-session-substrate-types.md b/docs/architecture/decisions/005-session-substrate-types.md new file mode 100644 index 0000000..7f50779 --- /dev/null +++ b/docs/architecture/decisions/005-session-substrate-types.md @@ -0,0 +1,71 @@ +# ADR-005: Session substrate types — duplex + stateless over one state machine + +## Status +Accepted + +## Context + +POC-1 and POC-3 found recurring glue that must not be re-implemented per +adapter or per handler: + +- The split → tokio-util compat → packetline bridge (POC-1). +- `StreamingPeekableIter`'s stop-delimiter contract: flush returns `None`, + the iterator is inert until `.reset()`; forgetting reset silently + swallows the next command (POC-1's second hang). +- The three-layer return type `Option>>`. +- The EOF-mid-parse error loop: an error-shaped read that resets-and-retries + spins a tokio worker at 100% (POC-3 recorded bug class) — error branches + must break unconditionally. +- The http-side request-body adapter (axum `BodyDataStream` → + `futures_io::AsyncRead`, ~40 lines, POC-3). +- The smart-http framing facts: first POST carries the capability dump + (skip two flush-delimited sections); capabilities before `0001` delim, + args after; responses end at flush, never `0002` (remote-curl dies on + it); the flush is one-shot per response (idempotent-flush guard is + mandatory); a flush-only POST is a probe answered 200-empty. + +If adapters or handlers hand-roll any of this, each gets it subtly wrong. + +## Decision + +`alkgit-transport` owns the substrate layer; adapters and handlers see +friendly types: + +1. **`Session` (duplex)** — wraps (stream, limits) into the pkt-line + session: owns the split/compat bridge, the request reader (command line + + pre-delim lines + args + flush, with the `reset()` discipline and the + unconditional-error-break rule), and the writer (sideband sink with + 65000-byte chunks, idempotent flush). Seed: POC-1's `serve_v2`. +2. **`StatelessRequest` (http)** — wraps (request-reader, response-writer) + per POST: performs the capability-dump skip, the delim-aware arg split, + and the flush-only response rule (no `0002`, idempotent flush, + 200-empty for flush-only probes). Seed: POC-3's `httpservice.rs` + + `BodyReader`. +3. **One protocol core** — the V2 state machines (ADR-003) run under both + substrates; the stateless substrate presents each POST as a command + invocation. Version dispatch happens at session start, in one place. +4. **Error taxonomy** — substrate errors are a transport-level + `thiserror` enum; io errors are terminal (session ends), protocol + errors map to pkt-line error bands / http status responses per the + respective substrate's convention. + +The sideband sink (band-1 chunks ≤ 65000 bytes, blocking-side `io::Write` +that parks on a bounded channel on the http path) is part of the substrate: +exactly one task drives a given sink (POC-2's `SidebandSink` contract). + +## Consequences + +- Adapters (http, ssh, downstream) cannot get the framing wrong; the + POC-hang and spin-bug classes are structurally prevented. +- The packetline raw API is confined to transport internals. +- State machines are substrate-agnostic, which is what makes smart-http's + statelessness cheap (per-request state, no session to restore). + +## References +- `docs/research/poc-1-findings.md` §"Answered hypotheses" 3 (packetline contracts), + follow-ups 1–2 +- `docs/research/poc3-findings.md` §"Course corrections" 1, follow-ups 2, 4 +- `docs/research/gitoxide.md` §"Wire format" +- ADR-002 (substrate shapes), ADR-003 (V2-first), ADR-004 (sink = io::Write) +- transport.md §substrate \ No newline at end of file diff --git a/docs/architecture/decisions/006-http-adapter-composition.md b/docs/architecture/decisions/006-http-adapter-composition.md new file mode 100644 index 0000000..ccf1645 --- /dev/null +++ b/docs/architecture/decisions/006-http-adapter-composition.md @@ -0,0 +1,91 @@ +# ADR-006: HTTP adapter composition — router factory in alkgit-http + +## Status +Proposed (recommendation recorded; final call pending user review — OQ-01) + +## Context + +POC-3 proved that git smart-http runs through alkhttp's +`HttpAdapter::with_extra_routes` surface with zero alkhttp changes: an axum +`Router` with internal state merges under the gateway's auth layer, request +bodies stream, responses stream under back pressure. The question is where +that router lives so downstream users can stack git onto their own alkhttp +deployment. + +Two candidate shapes: + +**Option A — router factory in `alkgit-http` (this crate).** +`alkgit-http` exports a builder that takes (registry, transport hooks, +identity-extractor callback, limits) and returns an axum `Router` ready to +merge via `with_extra_routes`. Downstream: depend on `alkhttp` + +`alkgit-http`, merge one router, wire their own auth into the extractor. + +**Option B — `git` feature on alkhttp with alkgit as an optional +dependency.** Downstream enables `alkhttp = { features = ["git"] }` and +gets git routes directly. + +Evaluation of Option B: + +- Dependency direction: alkhttp is a published generic sibling (0.5); the + git adapter is alkgit's domain. Making alkhttp depend on alkgit inverts + the layering — the generic layer would know about the git member of the + family, and every alkgit adapter change would require an alkhttp + release. +- The alkcall ADR-027 precedent (`from-jsonschema-as-http-adapter`) puts + call-protocol adapters inside alkhttp, but those are alkcall-op + adapters — shared machinery for the protocol alkhttp exists to serve. + Git smart-http is a foreign wire protocol (its own content types, + framing, streaming shape), not a call adapter. +- Composability rule (vision): front doors are replaceable adapters; + "ALPN as a service" implies the service family member owns its adapter. +- One-dep ergonomics is real but buyable later: alkhttp could gain a + convenience feature *re-exporting or wiring alkgit-http* once alkgit is + published — that is an alkhttp-side decision that does not constrain + alkgit's shape now. + +## Decision (proposed) + +**Option A.** `alkgit-http` owns the smart-http adapter and exposes it as +an axum router factory; alkhttp stays git-agnostic and unchanged. + +The factory's seam is the composability surface: + +- Input: the adapter's dependencies as traits/callbacks — peer-identity + extraction (the downstream app decides *how* http requests authenticate, + OQ-08), the core registry (repo resolution + ACL inputs, ADR-007/008), + transport hooks (upload-pack/receive-pack entry points, ADR-002), and + `Limits` (ADR-009). +- Output: an axum `Router` (state finalized internally) serving exactly + `GET /{repo}/info/refs`, `POST /{repo}/git-upload-pack`, + `POST /{repo}/git-receive-pack` with the POC-3-validated framing, which + the downstream merges via `HttpAdapter::with_extra_routes`. +- The route set is small and stable; reserved-path collision checking + (POC-3 confirmed it passes for these shapes) stays with alkhttp. +- `alkgitd` is the first consumer of the factory (no special privileges); + downstream apps are second users of the same seam — this is what makes + the adapter genuinely composable rather than binary-only. + +If a concrete downstream later demonstrates that the two-dep + merge +ergonomics is a real friction point, the Option-B-style sugar can be added +*in alkhttp* without any change here (alkhttp would gain an optional +alkgit-http feature re-exporting the factory). Deciding that now is not +necessary and would couple the release cadences; recording the escape +hatch here is enough. + +## Consequences + +- alkgit controls its http adapter's cadence; alkhttp is untouched. +- Downstream embedding is: two deps + one merge + one auth callback. +- The identity-extractor callback is the one place downstream auth + semantics enter; ACL itself stays in core (ADR-007) — adapters never + hand-roll authorization. +- This ADR stays Proposed until OQ-01 is discussed (user flagged the + alkhttp-feature alternative; the escape hatch above is the recorded + reconciliation path). + +## References +- `docs/research/poc3-findings.md` §"alkhttp fit" (with_extra_routes surface), follow-up 1 +- `docs/research/vision.md` §"ALPN as a service", §"Sub-crate shape" +- alkcall ADR-027 (precedent and its limits) +- ADR-001 (crate decomposition), ADR-002 (session boundary), ADR-007/008/009 +- http.md, OQ-01, OQ-08 \ No newline at end of file diff --git a/docs/architecture/decisions/007-acl-before-advertisement.md b/docs/architecture/decisions/007-acl-before-advertisement.md new file mode 100644 index 0000000..8d85a74 --- /dev/null +++ b/docs/architecture/decisions/007-acl-before-advertisement.md @@ -0,0 +1,64 @@ +# ADR-007: ACL runs before any advertisement or ref line + +## Status +Accepted + +## Context + +Ref names leak repository existence (and structure). The advertisement is +the first thing a client sees; if ACL runs after advertisement begins, a +denied caller has already learned that a repo (or a ref within it) exists. +The vision's visible-surface = authorized-surface invariant requires the +check to run before the first byte of git protocol content. + +POC-1 observed the natural enforcement point on the git:// path: the repo +name arrives in-band in the first request line, *before* any ref data — so +the resolve-then-authorize step sits structurally ahead of the first +emitted line. POC-3 noted the http extra-routes are registered permissive +by default (the gateway bearer layer does not cover them) — making the +http-side check an explicit alkgit-http responsibility, not an inherited +one. + +## Decision + +Every session/request performs, in order, before any protocol output: + +1. Extract the wire repo name (git:// first-request line; ssh exec command + string; http path segment). +2. Resolve it against the registry to (repo id, storage root) — reject + unknown repos with the same error as unauthorized ones (no existence + oracle) (ADR-008). +3. Run alkcall `AccessControl::check(peer_identity)` against the repo's + required access. Read access covers the *whole fetch surface*: + advertisement, ref listing, and pack transfer alike — anonymous access + on explicitly-public repos grants the same full read path + (vision §"Primary deployment target" makes anonymous clone first-class; + the "advertisement is the only anonymous surface" line in the same + doc's principle 1 is read as the *minimum* boundary, and this decision + sets the operative rule). +4. Only then hand the session to transport. + +Adapters embed this sequence; transport asserts it (the session entry +points take an authorized-repo marker — a type the adapter constructs only +after step 3 passes — so skipping the check is a type error, not a runtime +log) but does not re-check — authorization evaluation lives in one place, +alkcall, and invocation/wiring lives in one place, the adapter. + +Push is always authenticated on every repo, no exceptions (vision § +"Primary deployment target"). + +## Consequences + +- No ref/capability line is ever emitted for a repo a caller cannot see; + the gitea-class "enforcement elsewhere" bug is structurally excluded. +- Unknown-repo and unauthorized errors are indistinguishable to callers. +- The check is cheap (registry lookup + ACL check) and runs before any + expensive protocol work. +- http adapters must wire ACL explicitly for their routes (POC-3 showed + alkhttp extra routes default permissive) — http.md encodes this. + +## References +- `docs/research/vision.md` §"Guiding principles" 1–2, §"Immediate threat-model notes" +- `docs/research/poc-1-findings.md` follow-up 4; `docs/research/poc3-findings.md` §"does NOT settle" (auth) +- alkcall ADR-017 (privilege model) +- ADR-008 (repo identity), http.md §auth, ssh.md §auth \ No newline at end of file diff --git a/docs/architecture/decisions/008-registry-resolved-repo-identity.md b/docs/architecture/decisions/008-registry-resolved-repo-identity.md new file mode 100644 index 0000000..f1c5e73 --- /dev/null +++ b/docs/architecture/decisions/008-registry-resolved-repo-identity.md @@ -0,0 +1,46 @@ +# ADR-008: Wire repo names are registry IDs, never paths + +## Status +Accepted + +## Context + +Repo names arrive on the wire in three shapes: the git:// first-request +line (`git-upload-pack ''\0host=…`), the ssh exec command string +(`git-upload-pack ''`), and the http path segment +(`//info/refs`). All three are attacker-controlled. Treating them as +filesystem paths (even with ad-hoc sanitization) is the classic git-server +traversal bug — and `docs/research/git-protocol.md` §"Security-relevant +protocol notes" flags exactly this. + +## Decision + +Wire-supplied repo names are **IDs**: opaque registry keys resolved +server-side to configured storage roots. + +- The registry is alkgit's authoritative (repo id → storage root + + visibility + ACL scope) mapping, owned by `alkgit-core`. +- Resolution failure and authorization failure are indistinguishable to + the caller (ADR-007 step 2's no-existence-oracle rule). +- Wire names are never joined, normalized, or canonicalized into paths. + Path construction happens only from registry-resolved roots. +- The same never-execute rule covers the ssh exec command string: it is + *parsed* (fixed grammar: service name + quoted repo argument), never + interpreted by a shell and never passed to a subprocess (convention 6's + no-shelling-out invariant). Unknown commands get a fixed refusal. +- The registry's own backing store is a separate decision (OQ-06). + +## Consequences + +- Path traversal is structurally impossible on the serving path. +- Repo renames/migrations are registry edits, not filesystem moves (the + storage root indirection absorbs them). +- The registry must be available (or cached) at session start; its + unavailability is a config/ops error surfaced as session failure, never + a path-guessing fallback. + +## References +- `docs/research/git-protocol.md` §"Security-relevant protocol notes" +- `docs/research/vision.md` §"Immediate threat-model notes" +- ADR-007 (the resolve-then-authorize order) +- storage.md §registry, OQ-06 (registry backing) \ No newline at end of file diff --git a/docs/architecture/decisions/009-bounded-resources-budget.md b/docs/architecture/decisions/009-bounded-resources-budget.md new file mode 100644 index 0000000..456783b --- /dev/null +++ b/docs/architecture/decisions/009-bounded-resources-budget.md @@ -0,0 +1,64 @@ +# ADR-009: Bounded-resources budget model + +## Status +Accepted + +## Context + +Git servers are internet-facing by definition; unbounded loops and buffers +are bugs. Each POC surfaced specific unbounded surfaces that need budgets: + +- Negotiation rounds / haves count (fetch can loop forever without + `done`). +- receive-pack POST body size (push can be arbitrarily large; alkhttp + custom routes get hyper's unbounded stream — POC-3). +- Session wall-clock (long-lived ssh/git sessions). +- Blocking-pool usage: pack generation runs on `spawn_blocking`; a + thundering herd of fetches can starve the pool (POC-2 follow-up 2). +- Sideband chunk size is bounded (65000) but max pack size per fetch is + still unbounded above it. +- alkcall channels carry their own backpressure limits (alkcall ADR-040); + git sessions ride raw duplex streams, so those limits do not + automatically apply. + +## Decision + +Every session carries a `Limits` value, constructed by the adapter from +server config and handed to transport as part of the session tuple +(ADR-002). Defaults are per-crate constants; overrides are server config +in `alkgitd`. + +| Budget | Applies to | Default direction | +|---|---|---| +| max negotiation rounds | fetch (V2, no `done`) | tens | +| max haves per round | fetch | thousands | +| max pack size | receive-pack | config-bound (tens of MB v1) | +| max request body | http POSTs (receive-pack especially) | same as max pack size | +| session wall clock | all sessions (enforced by transport's session loop on every door — it is the one component all doors hand the session to; alkcall channel caps add a second bound where channels exist) | tens of minutes | +| max concurrent pack generations | server-wide (blocking-pool budget) | small count | +| sideband chunk size | fetch streaming | 65000 (fixed, per protocol) | +| max advertisement refs | ls-refs response | config-bound | + +On breach: the session ends with a substrate-appropriate error — pkt-line +error band + close on duplex; on http, *client-fault* budgets (request +body size) map to 413, *server/session* budgets (wall clock, rounds, +generation concurrency exhaustion) map to 503. Budgets are fail-closed. + +Max pack size on fetch is *not* budgeted in v1 (the pack is a function of +the repo, not the request); receive-pack is the untrusted-input path and +gets the hard cap. + +## Consequences + +- No adapter can forget a budget: transport refuses to start a session + without `Limits` (part of the tuple, ADR-002). +- Streaming stays O(counts) regardless of budgets; budgets bound + *aggregate* work, not internal buffering. +- The blocking-pool budget is enforced at assembly/acceptance time + (reject/slow-path excess concurrent generations), not per-byte. + +## References +- `docs/research/vision.md` §"Guiding principles" 7 +- `docs/research/poc2-findings.md` follow-ups 2–3; `docs/research/poc3-findings.md` follow-up 3 +- alkcall ADR-040 (channel backpressure — the thing git sessions bypass) +- ADR-002 (session tuple), transport.md §limits, http.md §budgets \ No newline at end of file diff --git a/docs/architecture/http.md b/docs/architecture/http.md new file mode 100644 index 0000000..3594044 --- /dev/null +++ b/docs/architecture/http.md @@ -0,0 +1,96 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# alkgit-http: HTTP Front Door + +## What it is + +The smart-http adapter: git's http protocol served over alkhttp. Owns the +smart-http endpoints, the http-framing composition (streaming both ways), +and the ACL wiring that alkhttp's extra-routes surface does not provide by +default. **Shape partially pending OQ-01/ADR-006** (router factory vs +alkhttp feature). + +## Routes (POC-3-validated shapes) + +| Route | Service | Notes | +|---|---|---| +| `GET /{repo}/info/refs?service=git-{upload,receive}-pack` | advertisement | smart prefix + capability dump + flush; `application/x-git-*-pack-advertisement`; no-cache | +| `GET /{repo}/info/refs?service=git-upload-archive` | — | **not served** — honest refusal (404/403 class), same rule as ssh (ADR-003) | +| `POST /{repo}/git-upload-pack` | V2 fetch commands | one command per POST | +| `POST /{repo}/git-receive-pack` | push | streaming ingestion, budgeted body (OQ-04) | + +- `{repo}` is the registry ID (ADR-008) — path segment → registry lookup, + never a path. +- Reserved-path collision checks stay with alkhttp + (`HttpAdapter::with_extra_routes` verifies; POC-3 confirmed passing). + +## Session flow (per request) + +1. Extract repo ID from the path; resolve + authorize **before any + response bytes** (ADR-007/008). Unknown == unauthorized (no existence + oracle). Identity comes from the downstream auth model (**OQ-08**). +2. `GET info/refs`: emit advertisement (via transport stateless + substrate); ACL checked before the smart prefix line. +3. `POST git-upload-pack`: build `StatelessRequest` (ADR-005) — the + substrate handles capability-dump skipping, delim-aware parsing, + flush-only responses, probe handling. +4. `POST git-receive-pack`: streaming body → transport receive-pack; + body budgeted (ADR-009; alkhttp extra routes are unbounded by default + — the budget is *ours*, POC-3 follow-up 3). + +## Streaming composition (POC-3-proven) + +- Request bodies: axum `BodyDataStream` → chunk-at-a-time reader → + packetline parser; no accumulation (dribble-probe verified). The + `futures-io` reader adapter lives in transport (ADR-005). +- Responses: sideband/protocol output → bounded mpsc (8 slots) → + `Body::from_stream`; back pressure parks the blocking-side generator; + first chunk leaves in milliseconds; RSS flat under slow readers. +- One sideband chunk = one HTTP chunk (65000-byte chunks carry over). + +## Composition seam (ADR-006 — proposed, OQ-01) + +The crate exposes the adapter as a factory (shape per ADR-006): +dependencies in (identity extractor, registry, transport hooks, limits), +an axum `Router` out, ready for `HttpAdapter::with_extra_routes`. +`alkgitd` is the first consumer; a downstream alkhttp deployment is the +second. If adopted, this section becomes the seam spec. + +`HttpAdapter` is not `Clone` (POC-3): assembly holds `Arc` +per accept task — the intended usage. + +## Auth + +- The gateway bearer layer does not cover extra routes' handlers unless + registered before it (POC-3): ACL wiring here is explicit, not + inherited. +- Identity extraction mechanics (bearer/basic/…) are **OQ-08**; the + factory seam takes it as a callback so downstream apps plug theirs in. +- Anonymous fetch: per-repo opt-in; push always authenticated (ADR-007). + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [002](decisions/002-front-door-blind-core.md) | Session boundary | stateless substrate per POST | +| [005](decisions/005-session-substrate-types.md) | Substrate types | http framing rules live in transport | +| [006](decisions/006-http-adapter-composition.md) | Router factory | **proposed** — OQ-01 open | +| [007](decisions/007-acl-before-advertisement.md) | ACL first | explicit wiring, not inherited | +| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | path segment = registry ID | +| [009](decisions/009-bounded-resources-budget.md) | Budgets | request-body cap on POSTs | + +## Open Questions + +- **OQ-01**: adapter home/composability (partially resolved — ADR-006 + proposed). +- **OQ-08**: identity sources (open — blocks the auth section). +- **OQ-04**: receive-pack over http (deferred(unclear)). + +## References + +- `docs/research/poc3-findings.md` (route shapes, framing facts, streaming measurements + — normative) +- `docs/research/alk-stack.md` §"Interfaces in alkcall terms" \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md new file mode 100644 index 0000000..bf45b91 --- /dev/null +++ b/docs/architecture/open-questions.md @@ -0,0 +1,178 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# Open Questions + +All unresolved architecture questions, centrally tracked. Status values: +`open` (needs resolution now), `partially resolved` (decision made but a +narrower question remains — named in the entry), `resolved` (decision +made, ADR recorded), `deferred(scope)` (waiting on external information — +blocked-on condition stated), `deferred(unclear)` (pieces exist, shape +needs investigation). See `docs/sdd_process.md` for the deferral protocol +(blocker tasks in `tasks/architecture/`). + +**Door type** classifies reversal cost: `one-way` decisions are +expensive/impossible to reverse once published (wire formats, public API +shapes); `two-way` decisions can be revisited while nothing is published. +Door type does not change urgency — all decisions here need resolution +when their impacts say so; it records how careful the resolution must be. + +## Deferred / Blocked summary + +| OQ | Status | Blocked on / investigation | +|---|---|---| +| OQ-04 | deferred(unclear) | receive-pack walkthrough (capabilities, shallow, thin-pack, CAS timing) + push POC | +| OQ-06 | deferred(scope) | concrete metadata-scale requirements (feeders: OQ-07, OQ-08 outputs) | +| OQ-05 | deferred(scope) | ecosystem need for sha256 | + +## Theme: composition / crate shapes + +### OQ-01: HTTP adapter home and composability (alkhttp `git` feature vs alkgit-owned factory) + +- **Origin**: [overview.md], [http.md], user session question +- **Status**: partially resolved — ADR-006 written **Proposed** with + Option A (alkgit-owned router factory) as the recommendation and the + alkhttp-side sugar as a recorded escape hatch. Needs user review. +- **Door type**: two-way (adapter shape can change before anything is + published) +- **Priority**: high +- **Impacts**: blocks finalizing http.md and ADR-006; small effect on + downstream ergonomics. +- **Resolution path**: user reviews ADR-006; accept → ADR becomes + Accepted; or choose Option B (alkhttp feature) → ADR reworked. +- **Cross-references**: ADR-001, ADR-006, http.md + +### OQ-03: Downstream embedding surface (what "embeds core + transport" means concretely) + +- **Origin**: [overview.md], [transport.md], [ssh.md], vision §ALPN as a + service +- **Status**: partially resolved — v1 ssh-door decision made: alkgitd + terminates SSH via russh for stock git clients (ssh.md); what remains + deferred is whether a *pure-alkcall-channels* ssh variant and a + russh-flavored adapter are exported for embedders. +- **Door type**: two-way +- **Priority**: medium +- **Impacts**: blocks nothing in v1 (alkgitd is the only consumer); shapes + the crates' public API freeze before publish. +- **Blocked on**: a concrete downstream embedder use case (e.g. a real + gitea-like app or test harness wanting to serve git) — until one + exists, the embedding seam is designed by example (alkgitd) only. + Tracker task: `tasks/architecture/oq-03-embedder.md`. +- **Cross-references**: ADR-001, ADR-002, transport.md §public API, + ssh.md §russh + +## Theme: transport / protocol + +### OQ-02: V2 multi-round negotiation (ack/NAK logic, `wait-for-done` retirement) + +- **Origin**: [transport.md], poc2-findings §"does NOT settle" +- **Status**: open +- **Priority**: medium (full-closure-on-`done` works; multi-round is an + efficiency feature, not correctness) +- **Impacts**: fetch efficiency on repos with large shared history; + capability advertisement text (`fetch=` value). +- **Resolution path**: design the ack loop (rounds budget per ADR-009) + when transport implementation begins; POC-2's generator is + negotiation-agnostic already. +- **Cross-references**: ADR-003, ADR-004, ADR-009, transport.md §fetch + +### OQ-04: receive-pack (push) — validation gap + +- **Origin**: [transport.md], poc3-findings §"does NOT settle" +- **Status**: deferred(unclear) +- **Door type**: two-way +- **Priority**: high +- **Impacts**: blocks receive-pack implementation tasks; push is the + always-authenticated half of the wire surface. +- **Investigation**: the pieces are decided (POC-2: pack ingestion via + `gix-pack::data::input` with `streaming-input` (ADR-004); `gix-ref` + transaction CAS; fsck via `gix-fsck`; POC-3: request bodies stream). The shape to + work through: the full push state machine — + (a) the receive-pack **capability advertisement set** + (`report-status`/`report-status-v2`, `delete-refs`, `push-options`, + `atomic`, `side-band-64k`, `object-format`) under the honest-advertisement + invariant (ADR-003); (b) request-line parsing + (` ` + shallow lines policy — expected resolution: + **reject shallow on push for v1**, mirroring fetch's decline-by-omission + in ADR-003, so depth semantics stay symmetric; confirm against real + `git push` behavior); + (c) thin-pack acceptance on push (client packs may be thin; accepting + implies base-object availability requirements); (d) pack ingestion + mid-stream; (e) CAS validation timing (before vs after pack index); + (f) status report (`unpack ok|ng` + per-ref lines); (g) the + receive-pack version/framing surface over http (which framing `git push` + uses against us; ADR-003's V2 decision covers fetch only). Method: + walkthrough against real `git push` captures, then a small POC if the + ingestion composition is not obvious from POC-2's findings. Tracker + task: `tasks/architecture/oq-04-receive-pack.md`. +- **Cross-references**: ADR-003, ADR-004, ADR-009, transport.md + §receive-pack, storage.md §ref transactions, http.md + +### OQ-05: sha256 support policy + +- **Origin**: [transport.md], git-protocol.md §"Open items" +- **Status**: deferred(scope) +- **Door type**: two-way +- **Priority**: low +- **Impacts**: none for v1 (sha1 pinned); feature-flag passthrough + compiles but is untested end-to-end. +- **Blocked on**: ecosystem need (a real client/repo requiring sha256) or + upstream gix sha256 maturity; POC-2 left the pipeline hash-generic but + untested. Tracker task: `tasks/architecture/oq-05-sha256.md`. +- **Cross-references**: ADR-003, ADR-004 + +## Theme: identity / auth + +### OQ-08: Identity sources per front door (v1 auth mechanics) + +- **Origin**: [overview.md], [http.md], [ssh.md], [alkgitd.md] +- **Status**: open +- **Priority**: high +- **Impacts**: blocks http.md and ssh.md auth sections finalizing; blocks + the identity-extractor callback design in ADR-006's seam; blocks + alkgitd config schema and the OQ-07 admin op shapes. +- **Resolution path**: decide per door — http (bearer token? basic? via + alkvault-stored credentials), ssh (russh-terminated public-key auth → + alkgit identity mapping per ssh.md), and which identities exist in + v1's registry — including whether identity records live in the same + metadata store as repo records (OQ-06's field list may grow for this). +- **Cross-references**: ADR-006, ADR-007, OQ-06 (metadata backing where + identity records live), + http.md §auth, ssh.md §identity, alkgitd.md, OQ-07 + +## Theme: storage / metadata + +### OQ-06: Registry/metadata backing store + +- **Origin**: [storage.md], ADR-008 +- **Status**: deferred(scope) +- **Door type**: two-way (backing choice is swappable behind the core + trait) +- **Priority**: high for v1 config story, but choice deferrable because + the trait boundary is what matters +- **Impacts**: blocks storage.md's registry section finalizing and + alkgitd's config schema; does NOT block core/transport work (they code + against the trait). +- **Blocked on**: concrete metadata-scale requirements (how many repos, + what metadata fields beyond id/root/visibility/ACL scope, whether + alkcall-hub integration lands in v1). A config-file or embedded-store + decision without those inputs would be a guess. Tracker task: + `tasks/architecture/oq-06-metadata-backing.md`. +- **Cross-references**: ADR-008, storage.md §registry, alkgitd.md §config, + OQ-08 (whose identity-records question may extend this store's field + list) + +### OQ-07: Admin API operation set (v1 scope) + +- **Origin**: [overview.md], [alkgitd.md], alk-stack.md §"The gitea lesson" +- **Status**: open +- **Priority**: medium +- **Impacts**: blocks the admin-ops inventory (repo create/delete, + visibility set, ACL grant/revoke, user/identity management — if v1 has + users at all, which is OQ-08 territory). Deliberately small: everything + is `Visibility::Internal` alkcall ops over the admin surface. +- **Resolution path**: one dedicated session once OQ-08's identity model + exists (the ops' shapes depend on what identities/credentials mean). +- **Cross-references**: ADR-007, OQ-08, alkgitd.md §admin API \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..09f1b45 --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,119 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# Overview: alkgit + +## Purpose + +alkgit is a self-hosted git server: repository storage, the git smart +protocol served over http and ssh interfaces, and the `alkgitd` binary that +assembles it all. It exists to provide a small, security-first git platform +whose exposure model inverts the mainstream pattern (visible-surface = +authorized-surface, authenticated-by-default, no plaintext secrets, no +plugin execution). See `docs/research/vision.md` for the full WHY. + +## The one-line architecture + +**Storage + ACL + protocol adapters.** `alkgit-core` owns repository storage +and access-rule types; `alkgit-transport` owns the git protocol state +machines; `alkgit-http` and `alkgit-ssh` are thin front doors that +authenticate, resolve the repo, and hand a session to the transport; +`alkgitd` assembles everything with config, TLS/ACME, and the vault. + +## Crate map + +| Crate | Owns | Depends on | Never depends on | +|---|---|---|---| +| `alkgit-core` | repo registry/storage roots, odb wrappers, ref store, pack generate/ingest, fsck, ACL input types | gix crates, alkcall (ACL types only) | any front-door crate | +| `alkgit-transport` | pkt-line sessions, V2 advertisement/state machine, ls-refs, fetch, receive-pack | `alkgit-core`, alkcall, gix-packetline | alkhttp, alkgit-ssh | +| `alkgit-http` | smart-http endpoints over alkhttp | transport, core, alkhttp | alkgit-ssh, alkgitd | +| `alkgit-ssh` | git-command dispatch for exec requests over alkcall channels | transport, core, alkcall | alkhttp, alkgitd | +| `alkgitd` | binary: config, assembly, TLS/ACME, listeners, vault wiring | everything | — | + +Dependency rules (ADR-001, ADR-002): + +1. `alkgit-core` + `alkgit-transport` are **front-door-blind**: they consume + (identity, repo id, duplex stream, limits) and depend on alkcall types + only. No http types, no ssh channel types below the stream. +2. `alkgit-http` and `alkgit-ssh` never depend on each other. +3. `alkgitd` is the only crate allowed to know the whole graph. +4. A downstream application (gitea-like) embeds core + transport and brings + its own front doors; the admin API is a set of alkcall ops it may use or + replace. + +## Security invariants (spec-level, all components honor) + +These are the load-bearing rules from `docs/research/vision.md` and +`docs/research/alk-stack.md`; each component doc references them where +concrete: + +1. **Authenticated by default** — anonymous fetch exists only on + explicitly-public repos; push is always authenticated. +2. **Visible-surface = authorized-surface** — via alkcall ACL; ops with + `Visibility::Internal` are never wire-callable; admin ops ride an admin + surface, never the git traffic surface. +3. **ACL before advertisement** — access is checked before any ref line or + capability line is emitted (ref names leak repo existence) (ADR-007). +4. **Registry-resolved repo identity** — wire-supplied repo names are IDs + resolved to server-configured storage roots; never used as paths (ADR-008). +5. **No secret material on the wire or at rest outside alkvault** — metadata + holds vault references only; outbound credentials flow through alkcall + `Capabilities` (alkcall ADR-010), and no handler reads credentials from + env or files (the no-env-vars invariant). +6. **No shelling out to `git`** — serving path is pure Rust on gix + primitives (GPL hygiene + no process-injection surface). +7. **Bounded resources** — every session carries wall-clock, size, and round + limits (ADR-009); unbounded loops/buffers are bugs. +8. **Honest capability advertisement** — the protocol advertises exactly + what we serve (ADR-003). + +## Interfaces (the boundary shape) + +The core boundary, from POC-1 (`docs/research/poc-1-findings.md`, follow-up 4) and the +"ALPN as a service" rule in `docs/research/vision.md`: + +- **Transport input**: one session = (peer identity from alkcall + `AuthContext`, repo id resolved against the registry, a duplex byte + stream, session limits). For the stateless http door this becomes + (identity, repo, request-reader, response-writer, limits) — the same state + machines, a different substrate (ADR-005). +- **Storage input**: the transport asks core for (a) ref advertisement data, + (b) pack generation for a want/have set, (c) pack ingestion + ref + transactions for receive-pack. Storage never sees pkt-lines. + +## What is already validated (POC-backed) + +- pkt-line over alkcall `BiStream` end-to-end (POC-1). +- Pack generation pipeline streaming with O(counts) memory (POC-2). +- Smart-http streaming both ways through alkhttp custom routes (POC-3). +The full V2 fetch path against real git 2.43 is proven; receive-pack is +designed but not yet exercised (OQ-04 tracks the POC/validation gap). + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [001](decisions/001-crate-decomposition.md) | Crate decomposition | 5 crates: core, transport, http, ssh, alkgitd | +| [002](decisions/002-front-door-blind-core.md) | Front-door-blind core | Session boundary = (identity, repo, stream, limits) | +| [003](decisions/003-protocol-v2-first.md) | V2-first protocol | V2-only fetch on both doors; honest advertisement; multi-round negotiation sequenced (OQ-02); push surface OQ-04 | +| [004](decisions/004-pack-pipeline.md) | Pack pipeline | gitoxide `data::output` generation, streaming-input ingestion | +| [005](decisions/005-session-substrate-types.md) | Substrate types | Duplex + stateless session APIs over one state machine | +| [006](decisions/006-http-adapter-composition.md) | HTTP adapter composition | Router factory in alkgit-http (**proposed**, OQ-01) | +| [007](decisions/007-acl-before-advertisement.md) | ACL first | No ref/capability line before ACL passes | +| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | Wire names are registry IDs | +| [009](decisions/009-bounded-resources-budget.md) | Budgets | Every session carries limits | + +## Open Questions + +Key cross-cutting questions tracked in [open-questions.md](open-questions.md): + +- **OQ-01**: http adapter composability (where the smart-http routes live + for downstream embedding) — affects http.md and ADR-006. +- **OQ-04**: receive-pack (push) validation gap (high — the + always-authenticated half of the wire surface). +- **OQ-08**: identity sources per front door (how http and ssh authenticate + peers in v1). +- **OQ-06**: metadata store backing for the registry (config-file vs + embedded store vs alkcall-hosted). \ No newline at end of file diff --git a/docs/architecture/ssh.md b/docs/architecture/ssh.md new file mode 100644 index 0000000..65d7c4a --- /dev/null +++ b/docs/architecture/ssh.md @@ -0,0 +1,106 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# alkgit-ssh: SSH Front Door + +## What it is + +The ssh adapter: serves the git command surface over ssh. Unlike http, +ssh is session-shaped, so it maps directly onto the transport duplex +session (ADR-002). + +**Client compatibility (v1 decision):** stock git clients +(`git clone ssh://git@host/repo`) speak the SSH wire protocol — an exec +request over an SSH connection. alkcall channels are *not* that protocol; +they are alkgit's internal session layer. v1 therefore terminates real SSH +wire protocol (russh is the candidate); the vision's "not a general sshd" +non-goal is untouched — only the git command surface is served. Concretely: + +- `alkgitd` terminates SSH (russh under the `ssh` door) and hands the + adapter the post-auth exec request + stream. +- alkcall channels remain the *internal* substrate identity/ACL flows + through (alk-stack.md §"Interfaces in alkcall terms"); the adapter's + dispatch output is the same duplex session either way. +- A pure-alkcall-channels ssh door (no wire SSH) is a downstream-embedding + variant only — OQ-03 territory, not v1. +- Manifest note: the pre-phase-1 workspace skeleton pins only `alkcall` in + `crates/alkgit-ssh/Cargo.toml`; the russh dependency (and where it sits — + alkgitd vs alkgit-ssh) is set when this spec implements. + +## What we serve + +Exactly the git command surface, not a general sshd (vision non-goals): + +| Command | Serves | +|---|---| +| `git-upload-pack ''` | fetch/clone (duplex V2 session) | +| `git-receive-pack ''` | push (duplex receive-pack; OQ-04) | +| `git-upload-archive` | **not served** (declined explicitly — + `docs/research/git-protocol.md` §security notes; honest refusal, not + silence) | + +## Session flow + +1. **Command dispatch**: parse the exec request string (`git-upload-pack` + / `git-receive-pack` + quoted repo argument). Reject other commands + with a clear error on stderr (no shell interpretation — the command + string is parsed, never executed). +2. **Repo resolution + ACL** (ADR-007/008): the quoted repo argument is a + registry ID; resolve, authorize (read for upload-pack, write for + receive-pack — push always authenticated), all before any protocol + byte. +3. **V2 negotiation**: `GIT_PROTOCOL=version=2` arrives as an ssh env + line (`git-protocol.md` §framing). v1 policy: expect/require V2 per + ADR-003 (V0/V1 declined with a clear error). +4. **Handoff**: the post-auth stream (from the russh-terminated session) + is the duplex substrate; transport runs the duplex session + (advertise-once → command loop, sideband packs). The adapter's job + ends here — no protocol logic lives in this crate. + +## Identity + +- With russh terminating SSH (see the v1 decision), peer identity comes + from the ssh authentication the terminator performs (public key, + typically). How that identity becomes an alkgit/alkcall identity — and + where authorized keys live (vault involvement) — is **OQ-08**, shared + with http; one auth session settles both doors. + +## Relationship to russh + +russh terminates the SSH wire protocol for stock git clients (see the v1 +decision above). The dispatch + ACL + handoff sequence (§Session flow) is +door-agnostic: alkcall-channel and russh inputs converge on the same +transport duplex session. Whether alkgit-ssh exports a russh-flavored +adapter separately from the alkcall one is an OQ-03 (embedding surface) +question — v1 ships the path alkgitd needs (russh termination → this +adapter's dispatch). + +## Limits + +The duplex session carries `Limits` (ADR-009). Channel-level caps +(alkcall ADR-041 per-identity channel cap) still apply above us; git +sessions consume one channel each (alk-stack.md §"Interfaces in alkcall +terms"). + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [001](decisions/001-crate-decomposition.md) | Crate decomposition | ssh is a separate replaceable adapter | +| [002](decisions/002-front-door-blind-core.md) | Session boundary | duplex session = native ssh shape | +| [003](decisions/003-protocol-v2-first.md) | V2-first | V0/V1 declined via env-line check | +| [007](decisions/007-acl-before-advertisement.md) | ACL first | before advertisement, push always authed | +| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | exec argument = registry ID | + +## Open Questions + +- **OQ-08**: ssh identity mechanics (open). +- **OQ-03**: russh-flavored embedder adapter (deferred(scope)). + +## References + +- `docs/research/poc-1-findings.md` (duplex session shape, git:// in-band framing + analog — ssh exec is the same resolve-before-emit order) +- `docs/research/alk-stack.md` §"Interfaces in alkcall terms" \ No newline at end of file diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md new file mode 100644 index 0000000..e149667 --- /dev/null +++ b/docs/architecture/storage.md @@ -0,0 +1,117 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# alkgit-core: Storage + +## What it is + +The repository storage layer: repo registry, object database wrappers, ref +store, pack generation/ingestion, and access-rule input types. It is +transport-agnostic and front-door-blind (ADR-001): it knows nothing about +pkt-lines, http, or ssh. + +## Why this shape + +gitoxide provides the primitives (odb, packs, refs, fsck, objects); the +generation-pipeline decision is ADR-004 (POC-2-validated). The crate +exists to (a) wrap gix with a coherent session-safe API (`gix_odb::Cache` +is not `Sync` — the sharing rules are enforced by the API in this crate), +(b) own the registry (ADR-008), and (c) hold the ACL input types the +adapters and alkcall share. + +## Components + +### Repository registry + +- Authoritative (repo id → storage root, visibility, ACL scope) mapping. +- Wire repo names are registry IDs, never paths (ADR-008); resolution + failure is indistinguishable from authorization failure (ADR-007). +- Backing store: **open** — OQ-06. Core defines the trait; alkgitd picks + the backing. Metadata holds no secrets (vault references at most). +- Storage roots are server-configured; the registry maps IDs onto them. + +### Repository access (`Repo`) + +- Opens a (storage root, hash algo) into an object database + ref store. +- Wraps `gix_odb::Store` sharing: `Arc` shared across sessions, + per-session handles (`to_handle_arc()` + `prevent_pack_unload()` + + `ignore_replacements = true` — the POC-2 prerequisites). The public API + hands out handles, never the raw `Cache`. +- Ref listing for advertisement: refs + peeled tags + symref targets, the + ls-refs response data. +- Ref transactions: CAS apply for receive-pack (`gix-ref` transaction + module), with name validation (git ref rules + reserved-namespace + deny-list; `docs/research/git-protocol.md` §security notes). + +### Pack generation (fetch side) + +- Type: (odb handle, wants, haves, limits) → streaming pack (`io::Write` + consumer). Negotiation-agnostic: negotiation produces the boundary sets, + the writer streams them (ADR-004). +- Encodes the POC-2 composition: tip peeling → commit-ancestry walk → + `TreeContents` count → entries → bytes; missing objects abort (never + emit a broken pack); entry statistics surface as metrics. +- Memory profile is O(counts) — the budget model (ADR-009) bounds + aggregate work, not internal buffering. + +### Pack ingestion (receive side) + +- Client pack stream → indexed pack + fsck/connectivity report + per-ref + CAS application. Uses `gix-pack` streaming-input + `gix-fsck` + + `gix-ref` transactions. Shape is designed (ADR-004); validation against + real `git push` is pending — OQ-04. +- Received pack size is budgeted (ADR-009 max pack size); ingestion runs + on blocking threads like generation. + +### Access-rule input types + +- The types alkcall `AccessControl::check` consumes for repos: visibility + (public/private), identity-based read/write. Deliberately *input types*: + the evaluation function lives in alkcall (single source of authorization + logic); invocation/wiring happens in the adapters (ADR-007). Core + defines what a repo's rule set looks like. +- v1 scope: repo visibility + identity read/write, nothing richer + (vision §"Primary deployment target"). + +## Concurrency model + +- `Store`/registry structures: `parking_lot` short-held locks (project + convention 3). +- odb handles: per-session, owned, moved into `spawn_blocking` tasks. +- Ref transactions: serialized per-ref by gix-ref's lock files; the + registry mapping's read-mostly synchronization (ArcSwap-class or + equivalent) is part of OQ-06's backing decision. +- Poisoned locks: `unwrap_or_else(|e| e.into_inner())` per convention 2. + +## Public API surface (v1) + +`lib.rs` re-exports (module structure per convention 14): registry trait + +types, `Repo`/handle types, pack generate/ingest types, ref-transaction +API, ACL input types, error enums (`thiserror`, no panics, no unwrap +outside tests). The embedder-facing freeze point is tracked in OQ-03. + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [001](decisions/001-crate-decomposition.md) | Crate decomposition | core = storage, transport-blind | +| [004](decisions/004-pack-pipeline.md) | Pack pipeline | `data::output` generation, `data::input` streaming ingestion | +| [007](decisions/007-acl-before-advertisement.md) | ACL before advertisement | core supplies rule inputs, adapters enforce | +| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry IDs | +| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits flow into generation/ingestion | + +## Open Questions + +- **OQ-06**: registry backing store (deferred(scope) — blocked on + metadata-scale requirements). +- **OQ-04**: receive-pack ingestion validation (deferred(unclear) — pieces + decided, push state machine needs a walkthrough/POC). +- **OQ-03**: embedder-facing API freeze (deferred(scope)). + +## References + +- `docs/research/gitoxide.md` (the API contract notes are normative here) +- `docs/research/poc2-findings.md` (generation pipeline + prerequisites) +- `docs/research/git-protocol.md` §"Server-side pack generation" \ No newline at end of file diff --git a/docs/architecture/transport.md b/docs/architecture/transport.md new file mode 100644 index 0000000..d2f4cc2 --- /dev/null +++ b/docs/architecture/transport.md @@ -0,0 +1,114 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# alkgit-transport: Git Smart Protocol + +## What it is + +The git protocol layer: pkt-line session substrates, protocol V2 state +machines (advertisement, ls-refs, fetch, receive-pack), and the glue that +keeps adapters from touching `futures_io` or raw packetline APIs. Depends +on `alkgit-core` and alkcall types only (ADR-001/002). + +## 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** (ssh, git://, embedded stream doors) — input: + (peer identity, resolved repo id, duplex stream, `Limits`). 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) — input: (peer identity, resolved + repo id, request-reader, response-writer, `Limits`) per http POST; adds + the http-framing rules (capability-dump skip, flush-only responses, + probe handling). + +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` for the v1 done-path policy — ADR-003 pins the + values; OQ-02 may extend them when multi-round lands, + `object-format=sha1`). Unimplemented features are declined by omission + (validated against real git, POC-1). + +### ls-refs + +- Parse `command=ls-refs` (peel, symrefs, ref-prefix), stream ref lines + from core's listing, flush. `ref-prefix` filtering is client-driven. + +### fetch + +- Parse wants/haves/done/args; object-format check (reject mismatches — + POC-1 to-do). +- Negotiation policy (v1 initial): full-closure pack on `done` + (POC-validated). Multi-round ack/NAK negotiation: **OQ-02**. +- Pack generation via core (ADR-004), streamed over sideband on duplex / + sideband-in-response on http; generation runs on `spawn_blocking` with + the owned odb handle moved in (POC-2's shape: store shared, handle per + session, generation on blocking threads). +- Round/haves budgets enforced here (ADR-009). + +### receive-pack (push) + +- Parse update requests (` ` + shallow lines), ingest the + pack stream (POC-3-verified: request bodies stream), apply CAS + transactions, emit status report. The capability set, shallow policy, + CAS timing, and exact status-report shape are OQ-04's investigation + target (design intent: shape per OQ-04's resolution). +- Validation pending — **OQ-04** (includes the receive-pack version + surface, which ADR-003's fetch-V2 decision does not cover). + +### 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). + +## 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. + +## Public API surface (v1) + +`lib.rs` re-exports: `Session` (duplex) + `StatelessRequest` (http) +entry points, `Limits`, hook trait(s) connecting to core +(advertisement data, pack generation, ingestion), protocol error enums. +The exact embedder-facing 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 | +| [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 | + +## Open Questions + +- **OQ-02**: V2 multi-round negotiation (open — efficiency, not + correctness). +- **OQ-04**: receive-pack state machine + validation (deferred(unclear)). +- **OQ-03**: embedder API freeze (deferred(scope)). +- **OQ-05**: sha256 policy (deferred(scope)). + +## 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/git-protocol.md` (inventory + observed corrections) +- `docs/research/gitoxide.md` §"Wire format" (packetline contracts) \ No newline at end of file diff --git a/tasks/architecture/oq-03-embedder.md b/tasks/architecture/oq-03-embedder.md new file mode 100644 index 0000000..7eaf0e9 --- /dev/null +++ b/tasks/architecture/oq-03-embedder.md @@ -0,0 +1,35 @@ +--- +id: architecture/oq-03-embedder +name: OQ-03 unblock — downstream embedder use case arrives +status: pending +depends_on: [] +scope: narrow +risk: trivial +impact: component +level: research +tags: [external-trigger, deferred-oq] +--- + +## Description + +Tracker for OQ-03 (`deferred(scope)`): downstream embedding surface. Not +actionable work — tracks whether a concrete downstream embedder use case +has arrived (a real app or harness wanting to embed core + transport, or +ask for a russh-flavored ssh adapter). On trigger: mark completed, OQ-03 +transitions to `open`. + +## Work + +None by default. + +## Verification + +- OQ-03 status matches `docs/architecture/open-questions.md`. + +## Out of scope + +- Designing the embedding seam. + +## Summary + +> Filled on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-04-receive-pack.md b/tasks/architecture/oq-04-receive-pack.md new file mode 100644 index 0000000..9599743 --- /dev/null +++ b/tasks/architecture/oq-04-receive-pack.md @@ -0,0 +1,42 @@ +--- +id: architecture/oq-04-receive-pack +name: OQ-04 unblock — receive-pack walkthrough/POC arrives +status: pending +depends_on: [] +scope: narrow +risk: trivial +impact: component +level: research +tags: [external-trigger, deferred-oq] +--- + +## Description + +Tracker for OQ-04 (`deferred(unclear)`): receive-pack (push) design +validation. This task is not actionable work — it tracks whether the +investigation has arrived. When a receive-pack design walkthrough (against +real `git push` captures) or a validation push POC is scheduled, mark this +completed; OQ-04 transitions to `open`. The investigation scope is in +`docs/architecture/open-questions.md` (OQ-04): capability advertisement +set, request-line parsing + shallow policy, thin-pack acceptance, CAS +timing, status report, http framing. + +## Work + +None by default. On trigger: set OQ-04 to `open` in +`docs/architecture/open-questions.md` and spawn the focused session that +works through the push state machine. + +## Verification + +- OQ-04 status matches this task's state in + `docs/architecture/open-questions.md`. + +## Out of scope + +- Actually designing/implementing receive-pack (that is the follow-on + session's work). + +## Summary + +> Filled on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-05-sha256.md b/tasks/architecture/oq-05-sha256.md new file mode 100644 index 0000000..59e53f0 --- /dev/null +++ b/tasks/architecture/oq-05-sha256.md @@ -0,0 +1,34 @@ +--- +id: architecture/oq-05-sha256 +name: OQ-05 unblock — sha256 ecosystem need arrives +status: pending +depends_on: [] +scope: narrow +risk: trivial +impact: component +level: research +tags: [external-trigger, deferred-oq] +--- + +## Description + +Tracker for OQ-05 (`deferred(scope)`): sha256 support policy. Not +actionable work — tracks whether ecosystem need arrives (a real +client/repo requiring sha256, or upstream gix sha256 maturity worth +testing). On trigger: mark completed, OQ-05 transitions to `open`. + +## Work + +None by default. + +## Verification + +- OQ-05 status matches `docs/architecture/open-questions.md`. + +## Out of scope + +- sha256 implementation. + +## Summary + +> Filled on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-06-metadata-backing.md b/tasks/architecture/oq-06-metadata-backing.md new file mode 100644 index 0000000..822f235 --- /dev/null +++ b/tasks/architecture/oq-06-metadata-backing.md @@ -0,0 +1,37 @@ +--- +id: architecture/oq-06-metadata-backing +name: OQ-06 unblock — metadata-scale requirements arrive +status: pending +depends_on: [] +scope: narrow +risk: trivial +impact: component +level: research +tags: [external-trigger, deferred-oq] +--- + +## Description + +Tracker for OQ-06 (`deferred(scope)`): registry/metadata backing store. +This task is not actionable work — it tracks whether the blocking +condition has arrived: concrete metadata-scale requirements (repo count, +metadata fields beyond id/root/visibility/ACL scope, alkcall-hub +integration scope). When they exist, mark completed; OQ-06 transitions to +`open`. + +## Work + +None by default. On trigger: set OQ-06 to `open` and schedule the backing +decision session. + +## Verification + +- OQ-06 status matches `docs/architecture/open-questions.md`. + +## Out of scope + +- Choosing the backing store. + +## Summary + +> Filled on completion. \ No newline at end of file