docs(architecture): phase 1 bootstrap — specs, 9 ADRs, OQ tracker

Architecture documentation structure per sdd_process phase 1:

- README index (doc table, ADR table, lifecycle), overview with crate
  map, dependency rules, and security invariants
- Component specs: storage, transport, http, ssh, alkgitd (all draft)
- ADRs 001-009: crate decomposition, front-door-blind core, V2-first
  protocol, pack pipeline (data::output generation / data::input
  ingestion), session substrate types, http adapter composition
  (proposed, OQ-01), ACL-before-advertisement, registry-resolved repo
  identity, bounded-resources budgets
- open-questions.md: OQ-01..08 with two deferred(scope), one
  deferred(unclear), door-type definitions, blocker tracker tasks in
  tasks/architecture/
- v1 ssh-door decision recorded: russh terminates wire SSH in alkgitd;
  alkcall channels stay the internal substrate (OQ-03 partially
  resolved)

Two review rounds (fresh-context subagent): 4 critical + 17 warnings
fixed in round one; zero critical + 4 warnings + 5 suggestions fixed in
round two. All ADR/OQ cross-references verified resolving.
This commit is contained in:
glm-5.3-flash committed 2026-09-21 03:55:33 +00:00
1 parent 70815e22a0
commit 8f73da5d12
21 files changed
+1669

No files matched your search

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