From 86bf5a0cf05ac5307a730de25bc5ff0184c8f835 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Mon, 21 Sep 2026 10:54:03 +0000 Subject: [PATCH] =?UTF-8?q?refactor(architecture):=20ADR-010=20=E2=80=94?= =?UTF-8?q?=20pure=20protocol=20crate=20(alktty=20template)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Structural decision (OQ-09 resolved): alkgit follows the alktty/ alktunnels template — a single published protocol crate on alkcall channels, no binary, no front doors. - ADR-010 supersedes ADR-001 (crate decomposition) and ADR-006 (http router factory); both marked Superseded - Single crate at repo root: Cargo.toml with gix feature (default-on backend implementations; wire layer compiles without it — gix-hash always-on with sha1 per the compile-time-rejected invariant), crates/ workspace deleted, src/lib.rs stub in place - doors.md replaces http.md/ssh.md/alkgitd.md: alkhttp git-feature sequencing (after first publish), alkssh requirement (fixed-grammar exec dispatch), native alk/git path, downstream assembly - backend.md replaces storage.md: GitRegistry/GitRefs/GitPackGen/ GitPackIngest traits (ingest validates, refs commits — single CAS home), gix feature encodes POC-2 prerequisites - transport.md reframed for the single crate; backend traits replace hook traits in the public API - OQ-09 resolved (all five sub-decisions in ADR-010), OQ-01 resolved (subsumed), OQ-03 narrowed to publish-freeze, OQ-08 narrowed to registry identity + vault placement, OQ-07 rescoped to the gix feature's registry impl - vision.md v2: single-binary/monorepo framing corrected as init-agent artifact; POC checklist marked complete - AGENTS.md + .opencode agent specs updated to the new shape Verification: cargo build (default + no-default-features), cargo test --all-features, clippy --all-features -D warnings, fmt --check all pass. Third review round: zero critical, all warnings/suggestions addressed (GitPackGen signature amended in ADR-004, stale anchors fixed, ADR-006 body tense normalized, CAS split stated, vision residuals cleaned). --- .opencode/agents/architect.md | 2 +- .opencode/agents/coordinator.md | 5 +- AGENTS.md | 72 ++++--- Cargo.toml | 68 +++--- crates/alkgit-core/Cargo.toml | 22 -- crates/alkgit-core/src/lib.rs | 1 - crates/alkgit-http/Cargo.toml | 25 --- crates/alkgit-http/src/lib.rs | 1 - crates/alkgit-ssh/Cargo.toml | 21 -- crates/alkgit-ssh/src/lib.rs | 1 - crates/alkgit-transport/Cargo.toml | 25 --- crates/alkgit-transport/src/lib.rs | 1 - crates/alkgitd/Cargo.toml | 31 --- crates/alkgitd/src/main.rs | 3 - docs/architecture/README.md | 51 ++--- docs/architecture/alkgitd.md | 96 --------- docs/architecture/backend.md | 103 +++++++++ .../decisions/001-crate-decomposition.md | 3 +- .../decisions/002-front-door-blind-core.md | 6 +- .../decisions/003-protocol-v2-first.md | 4 +- .../decisions/004-pack-pipeline.md | 11 +- .../decisions/005-session-substrate-types.md | 2 +- .../decisions/006-http-adapter-composition.md | 30 +-- .../decisions/007-acl-before-advertisement.md | 6 +- .../008-registry-resolved-repo-identity.md | 4 +- .../decisions/009-bounded-resources-budget.md | 12 +- .../decisions/010-pure-protocol-crate.md | 107 ++++++++++ docs/architecture/doors.md | 115 ++++++++++ docs/architecture/http.md | 96 --------- docs/architecture/open-questions.md | 199 +++++++----------- docs/architecture/overview.md | 146 ++++++------- docs/architecture/ssh.md | 106 ---------- docs/architecture/storage.md | 117 ---------- docs/architecture/transport.md | 73 ++++--- docs/research/README.md | 12 +- docs/research/vision.md | 91 ++++---- src/lib.rs | 9 + 37 files changed, 730 insertions(+), 947 deletions(-) delete mode 100644 crates/alkgit-core/Cargo.toml delete mode 100644 crates/alkgit-core/src/lib.rs delete mode 100644 crates/alkgit-http/Cargo.toml delete mode 100644 crates/alkgit-http/src/lib.rs delete mode 100644 crates/alkgit-ssh/Cargo.toml delete mode 100644 crates/alkgit-ssh/src/lib.rs delete mode 100644 crates/alkgit-transport/Cargo.toml delete mode 100644 crates/alkgit-transport/src/lib.rs delete mode 100644 crates/alkgitd/Cargo.toml delete mode 100644 crates/alkgitd/src/main.rs delete mode 100644 docs/architecture/alkgitd.md create mode 100644 docs/architecture/backend.md create mode 100644 docs/architecture/decisions/010-pure-protocol-crate.md create mode 100644 docs/architecture/doors.md delete mode 100644 docs/architecture/http.md delete mode 100644 docs/architecture/ssh.md delete mode 100644 docs/architecture/storage.md create mode 100644 src/lib.rs diff --git a/.opencode/agents/architect.md b/.opencode/agents/architect.md index 213c999..240fe34 100644 --- a/.opencode/agents/architect.md +++ b/.opencode/agents/architect.md @@ -329,7 +329,7 @@ A decision should be `deferred(scope)` when: - The use case isn't concrete (e.g., "we don't know what the admin API will need from the call protocol") - The options depend on something that doesn't exist yet (e.g., - "depends on the alkgit-http streaming adapter shape") + "depends on the alkhttp git-feature shape") - The trade-off requires data that can only come from implementation (e.g., "need performance benchmarks to choose between X and Y") - The decision is genuinely not needed for the current scope (e.g., "the diff --git a/.opencode/agents/coordinator.md b/.opencode/agents/coordinator.md index a3437e3..d1d18e1 100644 --- a/.opencode/agents/coordinator.md +++ b/.opencode/agents/coordinator.md @@ -206,11 +206,12 @@ Your task: {{task}} 8. Notify: worktree({action: "notify", args: {message: "Task completed: {{task}}. ", level: "info"}}) Key project constraints (@alkdev/alkgit): -- Rust workspace: crates/alkgit-core, -transport, -http, -ssh, alkgitd +- Pure protocol crate: single `alkgit` at repo root (ADR-010); no + workspace, no binary, no front-door crates - Rust: use cargo build, cargo clippy, cargo fmt, cargo test - No comments in code - thiserror for library error types; no panics in library code -- Feature flags for optional surface (sha256, acme) +- Feature flags for optional surface (gix default-on, sha256) - Async via tokio runtime - gitoxide (gix) for git storage/wire primitives; never shell out to git ``` diff --git a/AGENTS.md b/AGENTS.md index efe24d6..8ca0aee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,14 +50,16 @@ failed one. Git identity is preconfigured (`glm-5.3-flash `). Do not change `git config`, skip hooks, or use `git commit -i`. -## Project Conventions (Rust / git-server workspace) +## Project Conventions (Rust / protocol crate) -This is alkgit — a self-hosted git server: repository storage, the git -smart protocol served over HTTP and SSH interfaces, and the `alkgitd` -binary that assembles it on the alk stack (alkcall, alkhttp, alktls, -alkvault) and gitoxide (gix). The project is a cargo workspace of -sub-crates under `crates/`. The conventions below apply to all work in -`crates/*/src` and `tests/`. They mirror +This is alkgit — a pure protocol crate (ADR-010, the alktty/alktunnels +template): the git smart protocol as producer/consumer halves on alkcall +channels (the `alk/git` ALPN), backend traits with a feature-gated +gitoxide (gix) implementation, no binary, no front doors (doors — alkhttp +`git` feature, alkssh — are family infrastructure; see +`docs/architecture/doors.md`). Single crate at the repo root (`src/`, +`tests/`). The conventions below apply to all work in `src/` and +`tests/`. They mirror `.opencode/agents/implementation-specialist.md` §Project Conventions and are repeated here so they apply to every session, not just spawned implementation agents. @@ -105,7 +107,7 @@ implementation agents. is always authenticated. This is a spec-level invariant. 7. **gitoxide for git primitives, no shelling out** — storage and pkt-line - come from the gix crates (pinned in the workspace manifest). The + come from the gix crates (pinned in the crate manifest). The serving path never spawns the `git` binary (no GPL dependency, no process-injection surface). The server half of the smart protocol is ours; see `docs/research/git-protocol.md` for the inventory. @@ -122,10 +124,11 @@ implementation agents. exists), its shape must not change. Protocol capability advertisement is honest: never advertise what we don't serve. -10. **Feature flags** — optional surface is feature-gated: `sha256` (hash - algorithm passthrough; `sha1` is the default and pinned in the - workspace manifest) and `acme` (alktls ACME wiring in the binary). - Base crates compile lean. Verify both `cargo test` (default) and +10. **Feature flags** — optional surface is feature-gated: `gix` + (default-on; the backend implementations) and `sha256` (hash + algorithm passthrough; `sha1` is the default and pinned in the crate + manifest). The wire layer compiles without gix + (`default-features = false`). Verify both `cargo test` (default) and `cargo test --all-features` pass if features are added. 11. **Bounded resources** — every protocol session carries wall-clock, @@ -144,25 +147,25 @@ implementation agents. constants. 14. **Module structure** — one module per file under `src/`, re-exported - from `src/lib.rs`. Public API surface is `lib.rs` re-exports. - Workspace members under `crates/`: `alkgit-core` (storage), - `alkgit-transport` (smart protocol), `alkgit-http` (http front door), - `alkgit-ssh` (ssh front door), `alkgitd` (binary). + from `src/lib.rs` (the alktty crate-root pattern). Public API surface + is `lib.rs` re-exports. Single crate at the repo root (ADR-010); the + gix backend implementation lives under the default-on `gix` feature; + `default-features = false` gives the wire/protocol layer only. -15. **Composability boundary ("ALPN as a service")** — `alkgit-core` + - `alkgit-transport` are front-door-blind: they consume (identity, repo - id, duplex stream, limits) and depend on alkcall types only, never on - alkhttp/alkgit-ssh, and carry no http/channel-specific types below the - stream. The http and ssh crates are replaceable adapters; a downstream - app embeds core + transport and brings its own front doors. v1 is the - reduction to storage + ACL + protocol adapters. +15. **Composability boundary ("ALPN as a service")** — alkgit is + front-door-blind: it consumes (identity, repo id, duplex stream, + limits) and depends on alkcall types only, never on alkhttp/alkssh, + and carries no http/channel-specific types below the stream. Doors + (alkhttp `git` feature, alkssh) are family infrastructure; a + downstream app embeds alkgit and brings its own doors. v1 is the + reduction to wire layer + backend traits + one gix implementation. ## Verification Commands Run these before committing. All must pass. ```bash -cargo test # full suite (workspace) +cargo test # full suite cargo clippy --all-targets -- -D warnings cargo fmt --check cargo doc --no-deps # if docs changed @@ -174,17 +177,18 @@ If feature flags are added, also run `cargo test --all-features` and ## Lifecycle Status -The project is in **SDD phase 0** (exploration) — see -`docs/sdd_process.md` and `docs/research/README.md`. There is no -architecture yet (`docs/architecture/` is empty; ADR numbering starts at -001 when phase 1 begins). POC code never merges to main — two modes: -branch mode (POC builds on repo code; git branch, findings merged into -research docs, branch dropped) or standalone mode (POC independent of -repo code; scratch project at `/workspace/`). POCs have relaxed +The project is in **SDD phase 1** (architecture committed, implementation +not yet begun). Phase 0 research is in `docs/research/`; the committed +architecture is `docs/architecture/` (ADR-010 is the structural +decision: pure protocol crate, `alk/git` ALPN, backend traits, no +binary/doors). POC code never merges to main — two modes: branch mode +(POC builds on repo code; git branch, findings merged into research +docs, branch dropped) or standalone mode (POC independent of repo code; +scratch project at `/workspace/`). POCs have relaxed constraints (comments/unwrap acceptable) — they are exploration tools, -not production code. Until phase 1 -produces ADRs, "the architecture says" has no referent — cite -`docs/research/` docs instead. +not production code. Open architecture questions live in +`docs/architecture/open-questions.md` (OQ-04 receive-pack, OQ-06 +registry backing, OQ-08 identity model are the active ones). ## Architecture Context diff --git a/Cargo.toml b/Cargo.toml index 92aa9df..9395d85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,46 +1,58 @@ -[workspace] -resolver = "2" -members = [ - "crates/alkgit-core", - "crates/alkgit-transport", - "crates/alkgit-http", - "crates/alkgit-ssh", - "crates/alkgitd", -] - -[workspace.package] +[package] +name = "alkgit" version = "0.0.1" edition = "2021" rust-version = "1.88" license = "MIT OR Apache-2.0" repository = "https://git.alk.dev/alkdev/alkgit" +description = "Git smart protocol for the alk family: pkt-line substrates, protocol V2 serving (upload-pack/receive-pack), producer/consumer halves on alkcall channels, and backend traits with a feature-gated gitoxide implementation" +keywords = ["git", "protocol", "alkcall", "vcs", "network"] +categories = ["network-programming", "asynchronous"] -[workspace.dependencies] -alkgit-core = { path = "crates/alkgit-core", version = "0.0.1" } -alkgit-transport = { path = "crates/alkgit-transport", version = "0.0.1" } -alkgit-http = { path = "crates/alkgit-http", version = "0.0.1" } -alkgit-ssh = { path = "crates/alkgit-ssh", version = "0.0.1" } +[lib] +name = "alkgit" +path = "src/lib.rs" + +[features] +default = ["gix"] +# gitoxide-backed backend implementations (registry, refs, pack gen/ingest). +# Disable for a wire/protocol-only embed with your own backends. +gix = [ + "dep:gix", + "dep:gix-odb", + "dep:gix-pack", + "dep:gix-ref", + "dep:gix-object", + "dep:gix-fsck", +] +# sha256 passthrough (untested end-to-end; sha1 is pinned by default — OQ-05). +# gix-hash stays always-on with sha1: the wire layer needs it, and gix-hash +# fails to compile with neither hash feature selected (compile-time-rejected +# invariant, see docs/research/gitoxide.md). +sha256 = ["gix-hash/sha256"] + +[dependencies] alkcall = "0.8" -alkhttp = "0.5" -alktls = "0.1" -alkvault = "0.1" -gix = { version = "0.87", default-features = false, features = ["sha1"] } +gix = { version = "0.87", optional = true, default-features = false, features = ["sha1"] } +gix-odb = { version = "0.84", optional = true, default-features = false, features = ["sha1"] } +gix-pack = { version = "0.74", optional = true, features = ["sha1"] } +gix-ref = { version = "0.67", optional = true } +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" -gix-pack = { version = "0.74", features = ["sha1"] } -gix-odb = { version = "0.84", default-features = false, features = ["sha1"] } -gix-ref = "0.67" -gix-object = { version = "0.64", features = ["sha1"] } -gix-hash = { version = "0.26", features = ["sha1"] } -tokio = { version = "1", features = ["rt-multi-thread", "io-util", "net", "fs", "time", "sync", "macros"] } +tokio = { version = "1", default-features = false, features = ["rt", "sync", "io-util", "macros", "time"] } +tokio-util = { version = "0.7", features = ["compat"] } futures = "0.3" bytes = "1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" thiserror = "2" tracing = "0.1" parking_lot = "0.12" +[dev-dependencies] +tokio = { version = "1", features = ["full", "test-util", "macros"] } + [profile.release] lto = "thin" \ No newline at end of file diff --git a/crates/alkgit-core/Cargo.toml b/crates/alkgit-core/Cargo.toml deleted file mode 100644 index 1f499ef..0000000 --- a/crates/alkgit-core/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "alkgit-core" -description = "Core git plumbing for alkgit: repository storage, ref management, pack handling, and access rules — transport-agnostic" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -gix = { workspace = true } -gix-odb = { workspace = true } -gix-pack = { workspace = true } -gix-ref = { workspace = true } -gix-object = { workspace = true } -gix-hash = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } - -[features] -default = [] -sha256 = ["gix/sha256"] \ No newline at end of file diff --git a/crates/alkgit-core/src/lib.rs b/crates/alkgit-core/src/lib.rs deleted file mode 100644 index 45278f2..0000000 --- a/crates/alkgit-core/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -#![forbid(unsafe_code)] diff --git a/crates/alkgit-http/Cargo.toml b/crates/alkgit-http/Cargo.toml deleted file mode 100644 index 043c8e8..0000000 --- a/crates/alkgit-http/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "alkgit-http" -description = "HTTP interface for alkgit: git smart-http (info/refs, git-upload-pack, git-receive-pack) and the admin API over alkhttp" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -alkgit-core = { workspace = true } -alkgit-transport = { workspace = true } -alkhttp = { workspace = true } -alkcall = { workspace = true } -bytes = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } - -[features] -default = [] -sha256 = ["alkgit-transport/sha256"] \ No newline at end of file diff --git a/crates/alkgit-http/src/lib.rs b/crates/alkgit-http/src/lib.rs deleted file mode 100644 index 45278f2..0000000 --- a/crates/alkgit-http/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -#![forbid(unsafe_code)] diff --git a/crates/alkgit-ssh/Cargo.toml b/crates/alkgit-ssh/Cargo.toml deleted file mode 100644 index b2308ab..0000000 --- a/crates/alkgit-ssh/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "alkgit-ssh" -description = "SSH interface for alkgit: git-over-ssh command serving (git-upload-pack / git-receive-pack) on alkcall channels" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -alkgit-core = { workspace = true } -alkgit-transport = { workspace = true } -alkcall = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } - -[features] -default = [] -sha256 = ["alkgit-transport/sha256"] \ No newline at end of file diff --git a/crates/alkgit-ssh/src/lib.rs b/crates/alkgit-ssh/src/lib.rs deleted file mode 100644 index 45278f2..0000000 --- a/crates/alkgit-ssh/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -#![forbid(unsafe_code)] diff --git a/crates/alkgit-transport/Cargo.toml b/crates/alkgit-transport/Cargo.toml deleted file mode 100644 index 7332124..0000000 --- a/crates/alkgit-transport/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "alkgit-transport" -description = "Git smart-protocol transport for alkgit: packetline framing, capability advertisement, upload-pack and receive-pack serving over alkcall BiStreams" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -alkgit-core = { workspace = true } -alkcall = { workspace = true } -gix-packetline = { workspace = true } -gix-protocol = { workspace = true } -gix-transport = { workspace = true } -gix-hash = { workspace = true } -bytes = { workspace = true } -tokio = { workspace = true } -futures = { workspace = true } -thiserror = { workspace = true } -tracing = { workspace = true } - -[features] -default = [] -sha256 = ["alkgit-core/sha256"] \ No newline at end of file diff --git a/crates/alkgit-transport/src/lib.rs b/crates/alkgit-transport/src/lib.rs deleted file mode 100644 index 45278f2..0000000 --- a/crates/alkgit-transport/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -#![forbid(unsafe_code)] diff --git a/crates/alkgitd/Cargo.toml b/crates/alkgitd/Cargo.toml deleted file mode 100644 index eb7b291..0000000 --- a/crates/alkgitd/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "alkgitd" -description = "The alkgit server binary: assembles storage, smart protocol, HTTP, SSH, and TLS into one self-hosted git server" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true - -[[bin]] -name = "alkgitd" -path = "src/main.rs" - -[dependencies] -alkgit-core = { workspace = true } -alkgit-transport = { workspace = true } -alkgit-http = { workspace = true } -alkgit-ssh = { workspace = true } -alkhttp = { workspace = true } -alktls = { workspace = true } -alkvault = { workspace = true } -tokio = { workspace = true } -serde = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { version = "0.3", features = ["env-filter"] } -thiserror = { workspace = true } - -[features] -default = [] -sha256 = ["alkgit-http/sha256"] -acme = ["alktls/acme"] \ No newline at end of file diff --git a/crates/alkgitd/src/main.rs b/crates/alkgitd/src/main.rs deleted file mode 100644 index 96497ac..0000000 --- a/crates/alkgitd/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("alkgitd: skeleton — server assembly lands after architecture (SDD phase 1)"); -} diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9c236f3..9b51ab4 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -5,56 +5,51 @@ 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. +Phase 1 (SDD) output for alkgit — the git payload service of the alk +family: a pure protocol crate on alkcall channels (the `alk/git` ALPN), +following the alktty/alktunnels template (ADR-010). 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). +Phase 1, architecture committed to the pure-protocol-crate shape (ADR-010; +OQ-09 resolved). All docs below are `draft` except the superseded ADRs. +POC-1/2/3 validated the git protocol half end-to-end against real git +2.43; the remaining design work is the receive-pack state machine (OQ-04) +and backend/identity decisions (OQ-06, OQ-08). ## 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 | +| [overview.md](overview.md) | Cross-cutting: crate shape, halves, security invariants | draft | +| [transport.md](transport.md) | Wire layer: substrates, V2 state machines, upload/receive-pack | draft | +| [backend.md](backend.md) | Backend traits + feature-gated gix implementation | draft | +| [doors.md](doors.md) | Door mappings: alkhttp `git` feature, alkssh requirement, native path | 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 | +| [001](decisions/001-crate-decomposition.md) | Workspace crate decomposition (5 crates) | Superseded (ADR-010) | +| [002](decisions/002-front-door-blind-core.md) | 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 | +| [004](decisions/004-pack-pipeline.md) | Pack pipeline (`data::output` gen / `data::input` ingestion) | Accepted | +| [005](decisions/005-session-substrate-types.md) | Session substrate types (duplex + stateless APIs) | Accepted | +| [006](decisions/006-http-adapter-composition.md) | HTTP adapter composition (alkgit-owned router factory) | Superseded (ADR-010) | | [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). +| [009](decisions/009-bounded-resources-budget.md) | Bounded-resources budget model | Accepted | +| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate (alktty/alktunnels template) | Accepted | ## 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-09 (slim-crate model: doors as family infrastructure — may supersede -ADR-006/001 shape), OQ-04 (receive-pack validation), OQ-06 (metadata store -backing), OQ-08 (identity sources per front door). +OQ-04 (receive-pack validation), OQ-06 (registry backing), OQ-08 (registry +identity space + vault placement). ## Document Lifecycle diff --git a/docs/architecture/alkgitd.md b/docs/architecture/alkgitd.md deleted file mode 100644 index d5a09a4..0000000 --- a/docs/architecture/alkgitd.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -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/backend.md b/docs/architecture/backend.md new file mode 100644 index 0000000..b1584ac --- /dev/null +++ b/docs/architecture/backend.md @@ -0,0 +1,103 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# Backend traits: the storage seam + +## What this is + +The payload side of the protocol crate: the traits a service deployer +implements (or consumes via the feature-gated gix implementation) so the +git protocol can talk to storage. Per ADR-010 this mirrors alktty's +`TtyBackend` pattern — traits in-crate, real implementation behind a +feature. Unlike alktunnels (no backend trait), git needs the seam: pack +generation/ingestion is too heavy to hard-wire. + +## The trait family (ADR-010 sub-decision 4) + +Four traits, kept small and orthogonal — the protocol crate never sees +gix types: + +1. **`GitRegistry`** — repo id → (storage root, visibility, ACL scope). + The authoritative mapping (ADR-008); resolution failure is + indistinguishable from authorization failure (ADR-007). Metadata holds + vault references, never secrets. +2. **`GitRefs`** — listing for advertisement (refs + peeled tags + symref + targets, the ls-refs response data) and ref transactions (CAS apply + for receive-pack, name validation per git ref rules + reserved- + namespace deny-list). +3. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack + (`io::Write` consumer). Negotiation-agnostic. Missing objects abort + with an error, never a broken pack (ADR-004). +4. **`GitPackIngest`** — client pack stream → indexed pack + fsck/ + connectivity report. It *prepares* the validated ref updates; the + transaction itself is applied by `GitRefs` (single CAS home — ingest + validates, refs commits). Budgeted (ADR-009 max pack size); + blocking-thread friendly. + +Minimal-vs-full was the open sub-question; resolved as **full family** — +the four traits are each one or two methods plus types, and collapsing +them (e.g. refs into the registry) would force one impl block per +downstream where independent seams are cheaper to satisfy. The `gix` +feature implements all four; a downstream with its own object store +implements 3–4 and reuses 1–2, or none of it. + +## The gix feature (default on) + +- `alkgit = { default-features = true }` — wire layer + gix backend; + `default-features = false` — wire/protocol layer only (an embedder + brings its own backend). Hash: `sha1` pinned (the compile-time-rejected + invariant from `docs/research/gitoxide.md`); `sha256` passthrough + feature (OQ-05 policy unchanged). +- Encodes the POC-2 prerequisites by construction: odb handle sharing + (`Arc` shared, per-session handles, `prevent_pack_unload()` + + `ignore_replacements = true`), generation on blocking threads, + O(counts) memory, missing-objects abort. +- Received-pack ingestion via `gix-pack::data::input` (`streaming-input`) + + `gix-fsck` + `gix-ref` transactions (ADR-004). Validation against + real `git push` is OQ-04. + +## Concurrency model + +- `gix` structures: `parking_lot` short-held locks; per-session handles + moved into `spawn_blocking` tasks (POC-2's shape: store shared, handle + per session, generation on blocking threads). +- Poisoned locks: `unwrap_or_else(|e| e.into_inner())` (convention 2). +- The traits are `Send + Sync` object-safe; impls run under the + adapter's tokio context. + +## Public API surface + +Crate-root re-exports (the alktty pattern): backend traits + types, +`GitAdapter`/`register_openable` (producer), `GitSession` (consumer), +substrate types, `Limits`, protocol error enums; `gix`-feature types +(`GixBackend`-family) exported under the feature. The embedder-facing +freeze point remains OQ-03 (narrowed: it is now this crate's own publish, +not a multi-crate freeze). + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [004](decisions/004-pack-pipeline.md) | Pack pipeline | `data::output` gen / `data::input` ingestion | +| [007](decisions/007-acl-before-advertisement.md) | ACL first | registry returns rule inputs | +| [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 gen/ingest | +| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, gix behind a feature | + +## Open Questions + +- **OQ-06**: registry backing store (deferred(scope) — the trait is what + matters; the gix feature can ship a config-file/classic-on-disk impl, + and richer backing is downstream's choice). +- **OQ-04**: pack ingestion validation (deferred(unclear)). +- **OQ-05**: sha256 policy (deferred(scope)). + +## References + +- `docs/research/gitoxide.md` (API contract notes — normative for the + gix impl) +- `docs/research/poc2-findings.md` (generation pipeline + prerequisites) +- alktty `backend.rs`/`local` module (the trait + feature template) +- ADR-010 (the structural decision) \ No newline at end of file diff --git a/docs/architecture/decisions/001-crate-decomposition.md b/docs/architecture/decisions/001-crate-decomposition.md index 020e0ce..5fba80b 100644 --- a/docs/architecture/decisions/001-crate-decomposition.md +++ b/docs/architecture/decisions/001-crate-decomposition.md @@ -1,7 +1,8 @@ # ADR-001: Workspace crate decomposition (5 crates) ## Status -Accepted +Superseded by ADR-010 (pure protocol crate — single `alkgit`, no workspace, +no front-door crates, no binary) ## Context diff --git a/docs/architecture/decisions/002-front-door-blind-core.md b/docs/architecture/decisions/002-front-door-blind-core.md index 7d42af2..2f47ce6 100644 --- a/docs/architecture/decisions/002-front-door-blind-core.md +++ b/docs/architecture/decisions/002-front-door-blind-core.md @@ -6,7 +6,7 @@ Accepted ## Context The load-bearing composability rule ("ALPN as a service", -`docs/research/vision.md`): `alkgit-core` + `alkgit-transport` must never +`docs/research/vision.md`): alkgit (the single crate) 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. @@ -42,7 +42,7 @@ tuple (peer identity, resolved repo, limits): 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, +Storage-facing side: the wire layer calls the backend traits for advertisement data, pack generation (want/have set in → streaming pack out), and pack ingestion + ref CAS (receive-pack). Storage never sees pkt-lines. @@ -70,4 +70,4 @@ follow-up 2). - 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 +- overview.md §"Crate map" \ 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 index e3022c9..8761937 100644 --- a/docs/architecture/decisions/003-protocol-v2-first.md +++ b/docs/architecture/decisions/003-protocol-v2-first.md @@ -41,7 +41,7 @@ Decision drivers: (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 + exec requests for it get a fixed refusal (doors.md; ADR-008's never-execute rule). - HTTP requests protocol V2 only; ssh requests protocol V2 only (see below). @@ -79,4 +79,4 @@ happens then. - `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 +- transport.md, doors.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 index 2e714fa..288576f 100644 --- a/docs/architecture/decisions/004-pack-pipeline.md +++ b/docs/architecture/decisions/004-pack-pipeline.md @@ -63,18 +63,23 @@ ship as compressed bases. No upstream delta-encode API exists ## Consequences +(2026-09-21 amendment, ADR-010: the crate layout changed — the type +below is now the `GitPackGen` backend trait in the single crate, +gix-free by signature `(repo, wants, haves, limits)`; backend.md is +authoritative for the trait shape. The pipeline itself is unchanged.) + - 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). +- The generation seam is negotiation-agnostic: boundary sets in → pack + out (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 +- transport.md §fetch, backend.md §"The trait family" (GitPackGen) \ 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 index 7f50779..9ce51cb 100644 --- a/docs/architecture/decisions/005-session-substrate-types.md +++ b/docs/architecture/decisions/005-session-substrate-types.md @@ -29,7 +29,7 @@ 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 +`alkgit` owns the substrate layer; doors and handlers see friendly types: 1. **`Session` (duplex)** — wraps (stream, limits) into the pkt-line diff --git a/docs/architecture/decisions/006-http-adapter-composition.md b/docs/architecture/decisions/006-http-adapter-composition.md index c939544..3d1126f 100644 --- a/docs/architecture/decisions/006-http-adapter-composition.md +++ b/docs/architecture/decisions/006-http-adapter-composition.md @@ -1,9 +1,8 @@ # ADR-006: HTTP adapter composition — router factory in alkgit-http ## Status -Proposed (recommendation recorded; final call pending user review — OQ-01, -and now also OQ-09, whose slim-crate model may supersede this ADR's -shape entirely) +Superseded by ADR-010 (pure protocol crate — the http mounting moves to an +alkhttp `git` feature; the stateless substrate stays in `alkgit`) ## Context @@ -45,7 +44,12 @@ Evaluation of Option B: published — that is an alkhttp-side decision that does not constrain alkgit's shape now. -## Decision (proposed) +## Decision + +(Historical: this ADR was written as a proposal with the router factory +recommended; OQ-09/OQ-01 later resolved in favor of the alkhttp feature +instead, and ADR-010 superseded this ADR. Text below preserved as +written.) **Option A.** `alkgit-http` owns the smart-http adapter and exposes it as an axum router factory; alkhttp stays git-agnostic and unchanged. @@ -67,12 +71,10 @@ The factory's seam is the composability surface: 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. +(Historical) A concrete downstream later confirmed the ergonomics +friction; the Option-B sugar — an alkhttp `git` feature — was chosen as +the outcome (OQ-01/OQ-09), and ADR-010 adopted it as the structural +decision, superseding this ADR. ## Consequences @@ -81,13 +83,13 @@ hatch here is enough. - 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). +- (Historical) This ADR was Proposed pending OQ-01; OQ-01/OQ-09 resolved + in favor of the alkhttp-feature alternative recorded below the + escape-hatch note, and ADR-010 superseded this ADR outright. ## References - `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, OQ-09 (may supersede this ADR) \ No newline at end of file +- OQ-01 (resolved), OQ-08, OQ-09; superseded by ADR-010 \ 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 index 8d85a74..a612a0e 100644 --- a/docs/architecture/decisions/007-acl-before-advertisement.md +++ b/docs/architecture/decisions/007-acl-before-advertisement.md @@ -16,7 +16,7 @@ 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 +http-side check an explicit door responsibility (alkhttp `git` feature), not an inherited one. ## Decision @@ -55,10 +55,10 @@ Push is always authenticated on every repo, no exceptions (vision § - 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. + alkhttp extra routes default permissive) — doors.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 +- ADR-008 (repo identity), doors.md (door auth mechanics) \ 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 index f1c5e73..930cbe3 100644 --- a/docs/architecture/decisions/008-registry-resolved-repo-identity.md +++ b/docs/architecture/decisions/008-registry-resolved-repo-identity.md @@ -19,7 +19,7 @@ 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`. + visibility + ACL scope) mapping, owned by the `GitRegistry` backend trait. - 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. @@ -43,4 +43,4 @@ server-side to configured storage roots. - `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 +- backend.md §"The trait family" (GitRegistry), 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 index 456783b..58ef8ea 100644 --- a/docs/architecture/decisions/009-bounded-resources-budget.md +++ b/docs/architecture/decisions/009-bounded-resources-budget.md @@ -23,10 +23,12 @@ are bugs. Each POC surfaced specific unbounded surfaces that need budgets: ## 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`. +Every session carries a `Limits` value, constructed by the door/adapter +from assembler config and handed to the wire layer as part of the session +tuple (ADR-002). On the native path the producer adapter is in-crate +(`GitAdapter`), but `Limits` still originates from assembler config — +the adapter's constructor takes it. Defaults are crate constants; +overrides are assembler config. | Budget | Applies to | Default direction | |---|---|---| @@ -61,4 +63,4 @@ gets the hard cap. - `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 +- ADR-002 (session tuple), transport.md §Limits \ No newline at end of file diff --git a/docs/architecture/decisions/010-pure-protocol-crate.md b/docs/architecture/decisions/010-pure-protocol-crate.md new file mode 100644 index 0000000..5a5f56d --- /dev/null +++ b/docs/architecture/decisions/010-pure-protocol-crate.md @@ -0,0 +1,107 @@ +# ADR-010: Pure protocol crate — alkgit follows the alktty/alktunnels template + +## Status +Accepted (supersedes ADR-001, ADR-006) + +## Context + +OQ-09 captured a structural correction to how alkgit was initially framed. +The monorepo + binary setup (`alkgit-core`/`alkgit-transport`/`alkgit-http`/ +`alkgit-ssh`/`alkgitd`) came from the repo-initialization agent's rendering +of loosely-scoped use-case discussions — not a deliberate architecture +decision. It diverged from the established family pattern: the alk +protocol crates (alktty, alktunnels, alksocks in progress) are single +published crates implementing a service as a producer/consumer protocol on +top of alkcall channels, with doors (http, ssh, net) owned by separate +family crates that downstream consumers assemble. + +The git service fits that pattern exactly: + +- **Producer half** = POC-1's substrate verbatim: an adapter implementing + alkcall `ProtocolHandler` for the `alk/git` ALPN → `accept_bi()` → + duplex V2 session, plus a `register_openable` helper so a git session is + openable through the `alk/channels` multiplexer (alktty ADR-007/009 + pattern: the channels open-op carries the negotiation — here, the repo + id as params, which is also the natural ACL enforcement point). +- **Consumer half** = a typed `GitSession` client (alktty's `TtySession` + analog) driving fetch/push against a remote alkgit service — the + alkcall-native primitive for replication/mirroring in the alknet + rewrite. +- **Backend seam** = traits for registry, ref listing, pack generation, + and pack ingestion; the gix implementation ships behind a feature flag + (alktty's `local`-feature analog). Unlike alktunnels (no backend trait, + their ADR-004), git needs the storage seam — pack generation/ingestion + is too heavy to hard-wire — and unlike alktty, wasm-cleanliness is NOT a + goal: it falls out of the wire layer being gix-free, but nothing targets + wasm. +- **Smart-http** = the stateless substrate (ADR-005) stays in the protocol + crate; the http *mounting* (routes, content types, + `HttpAdapter::with_extra_routes` wiring) becomes an alkhttp `git` + feature — published after alkgit (see doors.md). POC-3 proved that + mapping is thin (its route layer was a thin wrapper over the stateless + substrate). +- **git-over-ssh** = alkssh's job when that crate exists (after alksocks). + The temporary russh-in-alkgitd decision (ssh.md) is dropped, not + shipped. The requirement alkgit places on alkssh is small and recorded + in doors.md. + +Why now: nothing is implemented (the crates/ tree is an empty skeleton), +so the entire migration is documentation and manifest surgery. POC work +maps 1:1 onto the new shape, so no validation is lost. + +## Decision + +**alkgit is a single protocol crate** following the alktty/alktunnels +template: + +- One crate, one repo (no cargo workspace, no sub-crates). Published to + crates.io as `alkgit`. +- **Producer half**: `GitAdapter` (direct `alk/git` ALPN via + `ProtocolHandler`) + channels `register_openable` (open-op params carry + the repo id — the negotiation, and the ACL enforcement point). +- **Consumer half**: `GitSession` typed client with `connect_direct` and + `open_via_channels` constructors. +- **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). +- **Substrate layer in-crate**: duplex session (ADR-002 boundary) + + stateless request/response substrate (ADR-005) — the stateless side is + IO-abstract (request-reader/response-writer), so alkhttp's feature maps + routes onto it without alkgit knowing http exists. +- **No binary. No front doors.** Assembly is downstream's job (the + platform deployment, or a future tiny assembly crate once alkssh/alknet + exist to assemble against). +- ALPN: `alk/git` (matches `alk/tty`, `alk/tunnel`, `alk/socks5`). +- Session tuple, security invariants (ADR-007/008/009), V2-first protocol + (ADR-003), pack pipeline (ADR-004), substrate types (ADR-005) all carry + over unchanged — they are pattern-independent. + +## Consequences + +- Positive: the embedder seam is *stronger* than ADR-001's shape (backend + traits let a downstream use its own storage instead of dragging gix + in); release surface shrinks to one crate; git becomes the first + payload service proven across three doors (alkhttp, alkssh, alknet) + with one protocol core; auth semantics collapse to "the door's auth" + (OQ-08 narrows sharply); POC-validated shapes are preserved verbatim. +- Negative: `vision.md`'s "single-binary git server" framing is amended — + the binary was never the user's intent (OQ-09 context); git-over-http + now ships on alkhttp's release cadence (feature lands in alkhttp 0.6 + after `alkgit` is published); git-over-ssh waits for alkssh (no ssh + path in the interim unless a downstream adds its own wire-ssh + termination via the duplex session — that is a legitimate embedder + path, not alkgit scope). +- Neutral: crate granularity sub-decision resolved as "merge" (the + storage-only embedder concern is served by feature-gating, not + splitting: `default-features = false` gives the wire/protocol layer + without gix). +- ADR-001 (crate decomposition) and ADR-006 (http router factory) are + superseded; ssh.md/http.md/alkgitd.md are replaced by doors.md. + +## References +- OQ-09 (the discussion this resolves), OQ-01 (subsumed), OQ-03 (narrowed) +- alktty docs/architecture (template), alktunnels docs/architecture (the + no-binary, feature-gated `local` precedent) +- POC-1/2/3 findings (the 1:1 mapping evidence) +- ADR-002 (unchanged, load-bearing), ADR-003/004/005/007/008/009 + (carry over), doors.md, backend.md, overview.md \ No newline at end of file diff --git a/docs/architecture/doors.md b/docs/architecture/doors.md new file mode 100644 index 0000000..e8ed7ff --- /dev/null +++ b/docs/architecture/doors.md @@ -0,0 +1,115 @@ +--- +status: draft +last_updated: 2026-09-21 +--- + +# Doors: how alkgit is exposed + +## What this is + +alkgit (per ADR-010) owns no front doors. This document records the two +door mappings that exist or are planned in the family, and the requirements +alkgit places on each. The protocol crate itself is door-blind (ADR-002): +every door converges on the same two substrates. + +## The door pattern + +Doors are family infrastructure: alkhttp (exists), alkssh (planned, after +alksocks), the alknet rewrite (coming). A door wraps alkcall's +producer/consumer in its wire protocol; services like git, tty, tunnels, +and socks5 are payloads doors optionally expose. Downstream consumers +(our platform deployment, a future gitea-like app) assemble the doors they +want with the payloads they want. + +Auth semantics are the door's auth (http: the door's token mechanism; ssh: +the door's key-based identity). alkgit consumes the resulting alkcall +identity — the identity-extractor seam that ADR-006 needed exists only in +the door, where it belongs. + +## alkhttp `git` feature (http mounting) + +**Scope**: an alkhttp feature that maps two route shapes onto alkgit's +stateless substrate (ADR-005): + +| Route | Service | +|---|---| +| `GET /{repo}/info/refs?service=git-{upload,receive}-pack` | advertisement (smart prefix + capability dump + flush) | +| `POST /{repo}/git-upload-pack` | V2 fetch commands (one command per POST) | +| `POST /{repo}/git-receive-pack` | push (streaming ingestion, budgeted body) | +| `GET /{repo}/info/refs?service=git-upload-archive` | honest refusal (not served) | + +`{repo}` is the registry id (ADR-008); resolution + ACL run before any +response byte (ADR-007). + +**Sequencing**: the feature requires `alkgit` on crates.io (optional +dependencies must resolve), so it lands in alkhttp 0.6 after alkgit's +first publish. Until then, git-over-http is served by any downstream that +mounts the stateless substrate directly — POC-3's `httpservice.rs` is the +reference implementation of exactly that mapping. + +**Framing facts the feature must honor** (all POC-3-validated, encoded in +the substrate, not re-decided): responses end at flush (never `0002`); +flush is one-shot per response; flush-only POSTs are probes answered +200-empty; request bodies stream (no accumulation); response bodies +stream under back pressure (bounded mpsc → `Body::from_stream`); request +bodies carry a budget (ADR-009 — alkhttp custom routes are unbounded by +default). + +## alkssh (git-over-ssh, future) + +**alkgit's requirement on alkssh** (to record in alkssh's spec when it +exists): parse the exec-request string with a fixed grammar — +`git-upload-pack ''` / `git-receive-pack ''` — never shell- +interpret it (ADR-008's never-execute rule), map the door's key-based +identity to the alkcall identity space, resolve the repo id against the +registry, run ACL, and hand (identity, repo, post-auth stream, limits) +to alkgit's duplex session. `git-upload-archive` gets a fixed refusal. +V2 is expected (ADR-003); `GIT_PROTOCOL=version=2` rides the ssh env +mechanism. + +**Interim**: no git-over-ssh path ships with alkgit. A downstream that +needs it before alkssh lands can terminate wire-ssh itself (russh or +otherwise) and consume the duplex session — that is exactly the embedder +path ADR-002 defines, and it works today against the POC-1 shape. It is +an embedder assembly concern, not alkgit scope. + +## The alkcall-native path (no adapter at all) + +The `alk/git` ALPN producer and the channels open-op (repo id in the +open-op params) need zero door code — POC-1 is that shape verbatim. Any +alkcall-speaking client (including alkgit's own consumer half, +`GitSession`) can use it. This is the baseline path; the http/ssh doors +are conveniences layered on top for stock git clients. + +## Assembly (downstream responsibility) + +There is no alkgit binary (ADR-010). A deployment assembles: alkcall +connection sources (alkhttp/alktls for http, alkssh later, raw ALPN for +the native path) + `GitAdapter` + a backend implementation (the `gix` +feature's, or its own) + `Limits` from config + vault for any credential +material. Reference sequence for our platform deployment lives in that +deployment's docs, not here. + +## Design Decisions + +| ADR | Decision | Summary | +|---|---|---| +| [002](decisions/002-front-door-blind-core.md) | Session boundary | every door consumes the same tuple | +| [003](decisions/003-protocol-v2-first.md) | V2-first | honest advertisement per door | +| [007](decisions/007-acl-before-advertisement.md) | ACL first | before any protocol byte | +| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids | +| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits on every session | +| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | doors are family infrastructure | + +## Open Questions + +- **OQ-08**: registry identity space + vault placement (narrowed by + ADR-010; door auth mechanics belong to the door crates). + +## References + +- `docs/research/poc-1-findings.md` (duplex shape), `docs/research/poc3-findings.md` + (http mounting evidence, framing facts) +- alktty/alktunnels architecture docs (the template this follows) +- ADR-010 (supersedes ADR-001/006; this file replaces http.md, ssh.md, + alkgitd.md) \ No newline at end of file diff --git a/docs/architecture/http.md b/docs/architecture/http.md deleted file mode 100644 index 3594044..0000000 --- a/docs/architecture/http.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -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 index 8afe01b..5438ac4 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -23,46 +23,54 @@ when their impacts say so; it records how careful the resolution must be. | OQ | Status | Blocked on / investigation | |---|---|---| -| OQ-09 | open (discussion) | slim-crate model: doors as family infrastructure; subsumes OQ-01 if Slim-B | | 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-09: Slim-crate model — doors as family infrastructure, git as a payload service + +- **Origin**: user session (2026-09-21); ADR-006, ADR-001, ssh.md +- **Status**: **resolved** — ADR-010 (pure protocol crate, the alktty/ + alktunnels template): single `alkgit` crate, producer/consumer halves, + backend traits with feature-gated gix, no doors, no binary; ALPN + `alk/git`; http mounting → alkhttp `git` feature; git-over-ssh → + alkssh (russh scaffolding dropped); the monorepo/binary framing was an + init-agent artifact (vision amended). +- **Resolution**: [decisions/010-pure-protocol-crate.md]. All five + sub-decisions recorded there (ssh deletion, http home, crate + granularity, backend-trait surface, binary fate). +- **Cross-references**: ADR-001/006 (superseded), OQ-01, OQ-03, OQ-08, + doors.md, backend.md, overview.md + ### 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 +- **Origin**: user session question (OQ-01, resolved 2026-09-21) +- **Status**: **resolved** (subsumed by OQ-09/ADR-010) — the smart-http + stateless substrate stays in `alkgit` (IO-abstract); the http mounting + (routes, content types, `with_extra_routes` wiring) becomes an alkhttp + `git` feature published after alkgit's first publish. The ADR-006 + router-factory shape is superseded; the alkhttp-side feature is the + outcome. +- **Cross-references**: ADR-006 (superseded), ADR-010, doors.md -### OQ-03: Downstream embedding surface (what "embeds core + transport" means concretely) +### OQ-03: Downstream embedding surface (what "embeds alkgit" 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 +- **Origin**: [overview.md], [transport.md], vision §"ALPN as a service" +- **Status**: **partially resolved** — the shape is settled (ADR-010): + embedding = one crate + backend traits (own storage via + `default-features = false`, or the gix feature) + optional door + features. What remains deferred is the publish-time API freeze itself: + which type/feature names are pinned at first crates.io publish. +- **Door type**: one-way (API freeze is registry-visible to dependents) - **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 +- **Impacts**: blocks the first publish only, not implementation. +- **Blocked on**: first-publish timing (a release decision, not an + architecture question). The API surface inventory lives in + [backend.md](backend.md) §public API and [transport.md](transport.md) + §public API. +- **Cross-references**: ADR-010, ADR-002, backend.md, transport.md ## Theme: transport / protocol @@ -109,7 +117,7 @@ when their impacts say so; it records how careful the resolution must be. 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 + §receive-pack, backend.md §"The trait family" (GitRefs), doors.md ### OQ-05: sha256 support policy @@ -126,106 +134,59 @@ when their impacts say so; it records how careful the resolution must be. ## Theme: identity / auth -### OQ-08: Identity sources per front door (v1 auth mechanics) +### OQ-08: Registry identity space + vault placement (narrowed) -- **Origin**: [overview.md], [http.md], [ssh.md], [alkgitd.md] -- **Status**: open +- **Origin**: [overview.md], [doors.md], [backend.md]; originally "identity + sources per front door" +- **Status**: open — narrowed by ADR-010. Door auth mechanics (http token + handling, ssh keys) belong to the door crates; what remains for alkgit + is: the identity model the `GitRegistry` knows (what an identity is, + what identity records exist, whether they live in the same metadata + store as repo records — OQ-06's field list may grow), and where + credential material lives (alkvault; metadata holds references only). +- **Door type**: two-way - **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 +- **Impacts**: blocks backend.md's registry trait field list finalizing; + blocks the admin-ops shapes (OQ-07). +- **Resolution path**: one focused session on the registry identity model + + alkvault placement. +- **Cross-references**: ADR-007, ADR-010, OQ-06, OQ-07, backend.md + §GitRegistry, doors.md ## Theme: storage / metadata -### OQ-06: Registry/metadata backing store +### OQ-06: Registry/metadata backing store (gix feature) -- **Origin**: [storage.md], ADR-008 +- **Origin**: [backend.md] (was 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). +- **Door type**: two-way (backing choice is swappable behind the + `GitRegistry` trait) +- **Priority**: high for the gix feature's default story, but choice + deferrable because the trait boundary is what matters +- **Impacts**: blocks backend.md's gix-feature registry impl finalizing; + does NOT block the wire layer (it codes 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: + identity records join — OQ-08's output, whether 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) +- **Cross-references**: ADR-008, ADR-010, backend.md §"The trait family" (GitRegistry), 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 +- **Origin**: [overview.md], alk-stack.md §"The gitea lesson" +- **Status**: open — rescope note (ADR-010): there is no alkgit binary, so + there is no alkgit-owned admin surface. The question narrows to whether + the gix feature's `GitRegistry` implementation ships with any management + ops (repo create/delete, visibility, ACL grant) as reusable alkcall ops, + or whether registry mutation is entirely downstream assembly work. If + shipped, they are `Visibility::Internal` alkcall ops over the + assembler's admin listener — never the git traffic surface. - **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 - -### OQ-09: Slim-crate model — doors as family infrastructure, git as a payload service - -- **Origin**: user session (2026-09-21); ADR-006, ADR-001, ssh.md -- **Status**: open — under discussion, no commitment -- **Door type**: one-way once anything is published (crate deletion and - feature-promotion are wire/registry-visible to dependents) -- **Priority**: high -- **Impacts**: blocks ADR-006 finalization (OQ-01 is subsumed by this if - Slim-B is chosen); reshapes ADR-001 (crate set), ssh.md (delete or - defer), OQ-08 (narrows: git consumes door identity), alkgitd's assembly - surface, and publish sequencing (alkhttp `git` feature requires - alkgit-transport on crates.io first). -- **Context**: the alk family shares doors — alkhttp exists, alkssh is - planned (after alksocks), alknet rewrite coming. Doors wrap alkcall - producer/consumer in their wire protocol; services (git, tty, tunnels, - socks) are payloads doors optionally expose. git should be a service - exposed by downstream consumers rather than owning its own doors. - Candidate end-states: **Slim-A** — keep alkgit-http factory, 5-crate - layout, ADR-006 Option A as written; **Slim-B** — protocol crates + - alkhttp `git` feature for smart-http (requires alkgit published first); - **Option C (pure protocol crate)** — follow the alktty/alktunnels - template exactly: single `alkgit` crate, producer half (`GitAdapter` - for `alk/git` ALPN + channels `register_openable`, POC-1 substrate), - consumer half (typed `GitSession` — the replication/mirroring - primitive), backend traits (registry/refs/pack-gen/pack-ingest) with - the gix implementation feature-gated (mirrors alktty's `local`; NOT a - wasm goal, the cleanliness just falls out), smart-http stateless - substrate stays in-crate while the http mounting becomes alkhttp's - `git` feature. No binary; assembly is downstream's. Consequences to - record on commitment: vision.md's "single-binary git server" framing - is amended (assembly belongs to downstream consumers); ADR-001/006 - superseded by a new ADR; ssh.md defers entirely to alkssh (the - temporary russh scaffolding decision is dropped, not shipped); OQ-08 - narrows to the registry identity space + vault placement. -- **Sub-decisions**: (1) delete `alkgit-ssh` now and defer git-over-ssh to - alkssh, or keep temporary russh scaffolding in alkgitd until then - (hinges on whether our deployment needs git-over-ssh before alkssh - lands); (2) http adapter home — Slim-A (keep alkgit-http, ADR-006 - Option A as written) vs Slim-B (alkhttp 0.6 `git` feature; requires - alkgit published first); (3) crate granularity — keep core+transport - split (embedders wanting storage-only) vs merge into one `alkgit` - (Option C's shape); (4) under Option C, backend-trait surface: full - family (registry, refs, pack-gen, pack-ingest) vs minimal (pack + - registry, refs ride the registry trait); (5) alkgitd's fate — deleted, - or kept as a thin reference assembly once doors exist to assemble - against. -- **Resolution path**: dedicated discussion session; decide the three - sub-decisions, then rewrite ADR-001/006 and ssh.md (a new ADR superseding - the affected parts, per ADR stability rules), and re-scope OQ-08. -- **Cross-references**: OQ-01, OQ-03, OQ-08, ADR-001, ADR-006, ssh.md, - alkgitd.md \ No newline at end of file +- **Impacts**: blocks the gix feature's registry impl scope; OQ-08's + identity model determines the op shapes. +- **Resolution path**: decide alongside OQ-08 (one session can settle + both). +- **Cross-references**: ADR-007, ADR-010, OQ-08, backend.md §"The trait family" (GitRegistry) \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 09f1b45..a2d5758 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -7,113 +7,89 @@ last_updated: 2026-09-21 ## 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. +alkgit is the git payload service of the alk family: a pure protocol crate +(per ADR-010, following the alktty/alktunnels template) implementing the +git smart protocol over alkcall channels — the `alk/git` ALPN. It provides +repository storage as backend traits (with a feature-gated gitoxide +implementation), the git smart protocol as producer/consumer halves, and +nothing else: no binary, no front doors. The original framing of this repo +(a monorepo with an `alkgitd` binary and its own http/ssh crates) was an +init-agent artifact corrected by OQ-09/ADR-010; `docs/research/vision.md` +is amended accordingly. ## 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. +**One protocol crate: producer + consumer + backend traits, gix behind a +feature.** Doors (alkhttp, alkssh, alknet) expose it; assembly belongs to +downstream consumers. ## 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 | — | +Single crate `alkgit`: -Dependency rules (ADR-001, ADR-002): +| Half | Contents | POC evidence | +|---|---|---| +| Producer | `GitAdapter` (`alk/git` ALPN via alkcall `ProtocolHandler`), channels `register_openable` (repo id in open-op params — the negotiation + ACL point) | POC-1 verbatim | +| Consumer | `GitSession` typed client (`connect_direct`, `open_via_channels`) — the replication/mirroring primitive for alknet | new, small (TtySession analog) | +| Substrate | duplex session + stateless request/response layer (ADR-005); wire framing, V2 state machines (ADR-003) | POC-1, POC-3 | +| Backends | `GitRegistry`, `GitRefs`, `GitPackGen`, `GitPackIngest` traits; gix impl behind the default-on `gix` feature | POC-2 (gix impl) | -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. +Feature model: `default-features = false` gives the wire/protocol layer +without gix (wasm-clean as a side effect, not a goal); the `sha256` +passthrough and (eventually, in alkhttp) the `git` door feature ride the +same pattern. Doors live in the door crates — see [doors.md](doors.md). -## Security invariants (spec-level, all components honor) +## Security invariants (spec-level, carried from vision/principles) -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. +1. **Authenticated by default** — anonymous fetch only on explicitly- + public repos; push always authenticated. +2. **Visible-surface = authorized-surface** — alkcall ACL end-to-end; + internal ops never wire-callable. +3. **ACL before advertisement** — nothing is emitted before the check + (ADR-007); the channels open-op carrying the repo id is the natural + enforcement point on the native path. +4. **Registry-resolved repo identity** — wire names are ids, never paths + (ADR-008). +5. **No secret material on the wire or at rest outside alkvault** — + metadata holds vault references; no env-var credential reads. +6. **No shelling out to `git`** — pure Rust on gix primitives. +7. **Bounded resources** — every session carries `Limits` (ADR-009). +8. **Honest capability advertisement** — advertise exactly what we serve + (ADR-003). ## 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). +- pkt-line over alkcall `BiStream` end-to-end (POC-1 → producer half). +- Pack generation pipeline streaming with O(counts) memory (POC-2 → gix + backend impl). +- Smart-http streaming both ways (POC-3 → the stateless substrate that + alkhttp's future `git` feature maps onto; `docs/research/poc3-findings.md` + §alkhttp fit is the mounting reference). 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). +designed but not yet exercised (OQ-04). ## 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 | +| [001](decisions/001-crate-decomposition.md) | Crate decomposition | **superseded by ADR-010** | +| [002](decisions/002-front-door-blind-core.md) | Session boundary | (identity, repo, stream, limits) — unchanged, load-bearing | +| [003](decisions/003-protocol-v2-first.md) | V2-first protocol | V2-only both doors; honest advertisement | +| [004](decisions/004-pack-pipeline.md) | Pack pipeline | `data::output` gen / `data::input` ingestion | +| [005](decisions/005-session-substrate-types.md) | Substrate types | duplex + stateless APIs over one state machine | +| [006](decisions/006-http-adapter-composition.md) | HTTP adapter composition | **superseded by ADR-010** (mounting → alkhttp feature) | +| [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 | +| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | single crate, producer/consumer halves, no doors/binary | ## Open Questions -Key cross-cutting questions tracked in [open-questions.md](open-questions.md): +Key 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 +- **OQ-06**: registry backing store for the gix feature (deferred on + scale requirements). +- **OQ-08**: registry identity space + vault placement (narrowed by + ADR-010). \ No newline at end of file diff --git a/docs/architecture/ssh.md b/docs/architecture/ssh.md deleted file mode 100644 index 65d7c4a..0000000 --- a/docs/architecture/ssh.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -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 deleted file mode 100644 index e149667..0000000 --- a/docs/architecture/storage.md +++ /dev/null @@ -1,117 +0,0 @@ ---- -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 index d2f4cc2..ca25d17 100644 --- a/docs/architecture/transport.md +++ b/docs/architecture/transport.md @@ -3,28 +3,34 @@ status: draft last_updated: 2026-09-21 --- -# alkgit-transport: Git Smart Protocol +# alkgit: Git Smart Protocol (wire layer) ## 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). +The wire half of the `alkgit` protocol crate: pkt-line session substrates, +protocol V2 state machines (advertisement, ls-refs, fetch, receive-pack), +and the substrate types that keep doors and handlers from touching +`futures_io` or raw packetline APIs. Depends on alkcall and — only under +the `gix` feature — the backend implementations (ADR-010). The wire layer +itself is backend-trait-only, which is what makes `default-features = +false` compile without gix. ## Substrate layer (ADR-005) Two session entry points over one state-machine core. ADR-005 owns the full decision (what each substrate owns and why); the surface is: -- **Duplex session** (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). +- **Duplex session** (the `alk/git` ALPN producer, channels-opened git + sessions, embedder 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 doors, e.g. alkhttp's future `git` + feature) — 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). IO-abstract: + the door supplies reader/writer; see [doors.md](doors.md) for the + mounting. Both substrates feed the same V2 state machines; statelessness is a substrate property (per-request state), not a protocol fork. @@ -40,12 +46,15 @@ substrate property (per-request state), not a protocol fork. `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). + (validated against real git, POC-1). `git-upload-archive` is not + served (fixed refusal — ADR-008's never-execute rule, ssh analog in + doors.md). ### ls-refs - Parse `command=ls-refs` (peel, symrefs, ref-prefix), stream ref lines - from core's listing, flush. `ref-prefix` filtering is client-driven. + from the backend's listing, flush. `ref-prefix` filtering is + client-driven. ### fetch @@ -53,10 +62,10 @@ substrate property (per-request state), not a protocol fork. 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). +- Pack generation via `GitPackGen` (ADR-004), streamed over sideband on + duplex / sideband-in-response on http; generation runs on + `spawn_blocking` with the owned 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) @@ -66,8 +75,9 @@ substrate property (per-request state), not a protocol fork. 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). +- Ingestion via `GitPackIngest` (ADR-004); validation pending — **OQ-04** + (includes the receive-pack version surface, which ADR-003's fetch-V2 + decision does not cover). ### Error taxonomy @@ -83,10 +93,11 @@ round, receive-pack max size, wall clock, sideband chunk size (fixed ## 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**. +Crate-root re-exports (alktty pattern; the full list in +[backend.md](backend.md) §public API): `GitAdapter` + `register_openable`, +`GitSession`, substrate types, `Limits`, the backend traits (ADR-010's +seam), protocol error enums; gix-feature types under the feature. +Publish-freeze point: **OQ-03**. ## Design Decisions @@ -97,18 +108,22 @@ The exact embedder-facing freeze point: **OQ-03**. | [004](decisions/004-pack-pipeline.md) | Pack pipeline | generation on blocking threads, O(counts) | | [005](decisions/005-session-substrate-types.md) | Substrate types | request reader, sideband sink, http framing rules | | [009](decisions/009-bounded-resources-budget.md) | Budgets | `Limits` in every session tuple | +| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | wire layer is backend-trait-only | ## 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-03**: publish/API freeze (partially resolved — single-crate shape + settled by ADR-010). - **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/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 +- `docs/research/gitoxide.md` §"Wire format" (packetline contracts) +- alktty `wire.rs`/`session.rs`/`adapter.rs` (the template's half shapes) \ No newline at end of file diff --git a/docs/research/README.md b/docs/research/README.md index 164fba2..4d23fe5 100644 --- a/docs/research/README.md +++ b/docs/research/README.md @@ -4,7 +4,7 @@ Phase 0 (exploration) research. Feeds phase 1 (architecture). | Doc | Topic | Status | |---|---|---| -| [vision.md](vision.md) | Vision, guiding principles, non-goals, phase-0 checklist | draft v1 | +| [vision.md](vision.md) | Vision, guiding principles, non-goals, phase-0 checklist | draft v2 (amended 2026-09-21) | [gitoxide.md](gitoxide.md) | gitoxide (gix) capability + version alignment | initial pass complete | | [alk-stack.md](alk-stack.md) | alk stack fit, integration surface, gitea-lesson constraints | initial pass complete | | [git-protocol.md](git-protocol.md) | Server-side git smart protocol inventory (what we own) | initial pass complete | @@ -27,4 +27,12 @@ Phase 0 (exploration) research. Feeds phase 1 (architecture). Phase 0 is done when POC-1..3 have results and a recommended-approach summary is written here (append below). Then the Architect produces -`docs/architecture/` per sdd_process. \ No newline at end of file +`docs/architecture/` per sdd_process. + +**Convergence (2026-09-21)**: phase 0 gates all pass (POC-1/2/3 proceed). +Phase 1 opened with `docs/architecture/`; the structural decision is +ADR-010 — pure protocol crate following the alktty/alktunnels template +(the v1 "monorepo + alkgitd binary" framing in these research docs was an +init-agent artifact, amended in vision.md v2). POC-1 maps to the producer +half, POC-2 to the gix backend implementation, POC-3 to the stateless +substrate consumed by alkhttp's future `git` feature. \ No newline at end of file diff --git a/docs/research/vision.md b/docs/research/vision.md index 07d3777..1ef7ac6 100644 --- a/docs/research/vision.md +++ b/docs/research/vision.md @@ -1,18 +1,23 @@ # alkgit Phase 0: Vision and Guiding Principles -**Status**: draft v1 — 2026-09-19 +**Status**: draft v2 — 2026-09-21 (v1 2026-09-19; v2 amends the delivery +shape — see "Sub-crate shape") **Phase**: 0 (exploration) — this document captures WHAT we are building and WHY before architecture (phase 1) commits to HOW. ## Vision -A self-hosted, single-binary git server in Rust: `alkgitd` serves repositories -over **http** and **ssh** interfaces with an authenticated-by-default surface, -built on the alk stack (alkcall, alkhttp, alktls, alkvault) and gitoxide. -It exists because self-hosted git platforms (gitea/gitlab) are large -multi-component applications with a long-tail of exposed APIs, plaintext -secret storage, and web-UI attack surface — and because CVE-2026-59774 -(gitea) demonstrated that a single logic bug in an internally-exposed API is +A git payload service for the alk family, in Rust: alkgit implements the +git smart protocol as a **pure protocol crate** on alkcall channels (the +`alk/git` ALPN) — storage behind backend traits (gitoxide implementation +behind a feature), producer/consumer halves, no binary, no front doors. +Doors are family infrastructure: alkhttp exposes smart-http, alkssh +(planned) will expose git-over-ssh, the alknet rewrite carries the native +path; downstream consumers assemble what they want. It exists because +self-hosted git platforms (gitea/gitlab) are large multi-component +applications with a long-tail of exposed APIs, plaintext secret storage, +and web-UI attack surface — and because CVE-2026-59774 (gitea) +demonstrated that a single logic bug in an internally-exposed API is enough for full compromise of the host. Our blast-radius and exposure model is designed from day one, not retrofitted. @@ -27,10 +32,11 @@ is designed from day one, not retrofitted. 2. **Secret hygiene**: gitea stores secrets plaintext in its DB. alkgit uses alkvault for anything credential-shaped; the metadata store holds vault references, never plaintext secrets. -3. **Footprint**: a git server is a small program (storage + smart protocol + - two front doors). The platform features (issues, PRs, wikis, CI) are where - the CVEs live. alkgit v1 is deliberately just the git server; everything - else stays out. +3. **Footprint**: a git server is a small program (storage + smart + protocol + thin doors); the platform features (issues, PRs, CI) are where + the CVEs live. alkgit v1 is deliberately just the git service as a + protocol crate; everything else stays out (doors live in the door + crates — alkhttp, alkssh — per ADR-010). 4. **Language**: the whole serving path is Rust (memory-safe, no C deps in the packet path). @@ -62,42 +68,49 @@ is designed from day one, not retrofitted. - Issues/PR/review features. - Git LFS (later phase, separate decision). - federation/replication between alkgit instances (later phase). -- Serving as a general sshd (only the git command surface). +- Serving as a general sshd (alkgit carries no ssh surface at all; + alkssh is the door). - Windows as a serving platform (linux first; keep code portable-ish but don't test it). -## Sub-crate shape (provisional, matches workspace skeleton) +## Sub-crate shape (amended 2026-09-21 by ADR-010) -| crate | role | +The v1 draft's table below was an init-agent artifact (monorepo + binary + +own doors); the corrected shape is the alktty/alktunnels pattern — see +`docs/architecture/decisions/010-pure-protocol-crate.md`: + +| Piece | Role | |---|---| -| `alkgit-core` | storage: repos, refs, odb, pack read/write, access-rule types | -| `alkgit-transport` | smart protocol: pkt-line sessions, advertise, ls-refs, fetch, receive-pack | -| `alkgit-http` | http front door (smart-http endpoints + admin API via alkhttp) | -| `alkgit-ssh` | ssh front door (git commands over alkcall channels) | -| `alkgitd` | binary: config, assembly, TLS/ACME, serving loops | +| `alkgit` (single crate) | wire layer (substrates, V2 state machines) + producer/consumer halves + backend traits | +| `gix` feature (default on) | gitoxide-backed implementation of the backend traits | +| alkhttp `git` feature (future) | smart-http mounting of the stateless substrate | +| alkssh (future family crate) | git-over-ssh door | +| downstream assembly | the actual deployment (config, listeners, TLS/ACME, vault) | ## Composability: "ALPN as a service" alkgit is the git member of the alk "ALPN as a service" family — alktty, alktunnels, and alksocks (socks5) follow the same pattern, and the alknet mono-repo is being decomposed into exactly these pieces for rewrite. The -pattern's load-bearing rule: **`alkgit-core` + `alkgit-transport` never know -which front door is talking.** The transport layer consumes (identity, repo +pattern's load-bearing rule: **alkgit never knows which front door is +talking.** The protocol crate consumes (identity, repo id, duplex stream, limits) and speaks git; everything above the stream is -the adapter's problem. That is what keeps alkgit composable for downstream +the door's problem. That is what keeps alkgit composable for downstream use: -- A future gitea/gitlab-like application should be able to embed - `alkgit-core` + `alkgit-transport` (or talk to `alkgitd`) and add its own - UI/issues/PR layer without forking anything here. -- The admin API is a set of alkcall ops, not an embedded web framework — - a downstream app can either use it or replace it. -- Nothing in the core may reach upward into alkhttp/alkssh concerns +- A future gitea/gitlab-like application embeds `alkgit` (wire layer, with + its own backend implementation via the traits or the gix feature) and + adds its own UI/issues/PR layer without forking anything here. +- Registry management ops, if the gix feature ships any, are a set of + alkcall ops, not an embedded web framework — a downstream app can either + use them or replace them (OQ-07). +- Nothing in the crate may reach upward into alkhttp/alkssh concerns (no http types, no channel types below the transport boundary). -The v1 reduction follows from this: **storage + ACL + protocol adapters**. -A simple static/template web UI for the public repo listing is explicitly -out of scope here (would be a separate downstream thing on top). +The v1 reduction follows from this: **wire layer + backend traits + one +gix implementation**. A simple static/template web UI for the public repo +listing is explicitly out of scope here (would be a separate downstream +thing on top). ## Primary deployment target @@ -117,19 +130,19 @@ in use**. So: - [x] alk stack fit + integration surface (`alk-stack.md`) - [x] server-side protocol inventory (`git-protocol.md`) - [x] license/reference policy (`reference-policy.md`) -- [ ] POC-1: real `git clone` over alkcall BiStream using gix-packetline +- [x] POC-1: real `git clone` over alkcall BiStream using gix-packetline (validates BiStream fit + packetline async codec end-to-end) -- [ ] POC-2: server-side pack generation composition (bundle-write vs +- [x] POC-2: server-side pack generation composition (bundle-write vs entries-to-bytes) for a small want/have set -- [ ] POC-3: http smart-endpoint streaming shape through alkhttp +- [x] POC-3: http smart-endpoint streaming shape through alkhttp (chunked pack response, unbuffered receive-pack POST ingestion) -- [ ] Convergence: recommended approach summary feeding phase 1 architecture +- [x] Convergence: recommended approach summary feeding phase 1 architecture ## Immediate threat-model notes carried into architecture -- The admin API (repo/user/permission management) is alkcall - `Visibility::Internal` ops over an admin-only listener; it is never part - of the git traffic surface. +- Registry management ops, if the gix feature ships any, are alkcall + `Visibility::Internal` ops over an admin-only listener; they are never + part of the git traffic surface (OQ-07). - Repo names arriving on the wire are registry IDs, never paths; storage roots are configured server-side only. - The advertisement phase runs ACL before the first ref line is emitted. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d199991 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +//! alkgit: git smart protocol for the alk family. +//! +//! Producer/consumer protocol crate on alkcall channels — the `alk/git` +//! ALPN (ADR-010, the alktty/alktunnels template). Phase 1 (SDD) has just +//! begun: this crate currently contains only the module skeleton markers; +//! the substrate layer, V2 state machines, and backend traits land per +//! `docs/architecture/` (see transport.md, backend.md, doors.md). + +#![forbid(unsafe_code)]