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:
1 parent
70815e22a0
commit
8f73da5d12
21 files changed
+1669
No files matched your search
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user