From a3cdef909b3d4067b24ddde428eccdbafe6181c3 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 19 Sep 2026 15:36:21 +0000 Subject: [PATCH] feat: workspace skeleton + phase 0 research docs - Cargo workspace with 5 sub-crates: alkgit-core (storage), alkgit-transport (smart protocol), alkgit-http, alkgit-ssh, alkgitd (binary); sha1 pinned through the gix stack, sha256 passthrough feature - docs/research/: vision, gitoxide alignment, alk stack fit, server-side protocol inventory, license/reference policy, POC plan - AGENTS.md: conventions mirroring alkcall (no comments, thiserror, tokio, no secrets on wire/at rest, visible-surface=authorized-surface, gitoxide-only serving path, bounded resources, registry-resolved repos) Verified: cargo build, clippy -D warnings, fmt, test, check --all-features --- .gitignore | 3 + AGENTS.md | 199 +++++++++++++++++++++++++++++ Cargo.toml | 46 +++++++ crates/alkgit-core/Cargo.toml | 22 ++++ crates/alkgit-core/src/lib.rs | 1 + crates/alkgit-http/Cargo.toml | 25 ++++ crates/alkgit-http/src/lib.rs | 1 + crates/alkgit-ssh/Cargo.toml | 21 +++ crates/alkgit-ssh/src/lib.rs | 1 + crates/alkgit-transport/Cargo.toml | 25 ++++ crates/alkgit-transport/src/lib.rs | 1 + crates/alkgitd/Cargo.toml | 31 +++++ crates/alkgitd/src/main.rs | 3 + docs/research/README.md | 30 +++++ docs/research/alk-stack.md | 94 ++++++++++++++ docs/research/git-protocol.md | 118 +++++++++++++++++ docs/research/gitoxide.md | 105 +++++++++++++++ docs/research/pocs.md | 71 ++++++++++ docs/research/reference-policy.md | 67 ++++++++++ docs/research/vision.md | 101 +++++++++++++++ 20 files changed, 965 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Cargo.toml create mode 100644 crates/alkgit-core/Cargo.toml create mode 100644 crates/alkgit-core/src/lib.rs create mode 100644 crates/alkgit-http/Cargo.toml create mode 100644 crates/alkgit-http/src/lib.rs create mode 100644 crates/alkgit-ssh/Cargo.toml create mode 100644 crates/alkgit-ssh/src/lib.rs create mode 100644 crates/alkgit-transport/Cargo.toml create mode 100644 crates/alkgit-transport/src/lib.rs create mode 100644 crates/alkgitd/Cargo.toml create mode 100644 crates/alkgitd/src/main.rs create mode 100644 docs/research/README.md create mode 100644 docs/research/alk-stack.md create mode 100644 docs/research/git-protocol.md create mode 100644 docs/research/gitoxide.md create mode 100644 docs/research/pocs.md create mode 100644 docs/research/reference-policy.md create mode 100644 docs/research/vision.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1fc0d6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.worktrees/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f305e7f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,199 @@ +# AGENTS.md + +Operating instructions for opencode agents working in this repo. opencode +auto-loads this file as instructions, overriding the built-in defaults for +this project. Custom agents in `.opencode/agents/` inherit these rules +unless their own prompts say otherwise. + +## Git Workflow + +**Commit and push when reasonable.** When a change is complete and +verified (build + lint + tests pass), commit and push to `origin/main` +without asking. This overrides the built-in default of "only commit when +explicitly asked." + +Commit in small, focused units — one commit per unit of work (a fix, a +feature, a doc change), not one large commit covering many topics. Push +regularly so work is never stranded locally. + +The workflow: + +1. Make the change +2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`, + `cargo fmt --check`, `cargo doc --no-deps` if docs changed +3. Inspect `git status` and `git diff` before staging — stage only the + intended files, never secrets +4. Write a concise commit message in **conventional commits** style: + `(): ` — types are `feat`, `fix`, `docs`, + `chore`, `refactor`, `test`, `perf`, `build`, `ci`; the scope is + optional. For multi-point changes, use a summary line plus a body with + bullet points and a verification block. +5. `git push origin main` +6. Report the commit hash and the verification summary + +Exceptions — **do not** commit or push without asking: + +- The change is exploratory / speculative (you're not sure the user wants + it kept) +- The user is actively reviewing the diff and may ask for changes +- The change touches published wire formats or semver-relevant public API + (crates will be published to crates.io once architecture lands; see the + ADRs in `docs/architecture/decisions/` for the stable-contract list once + it exists) +- You'd be force-pushing, amending a published commit, creating an empty + commit, or skipping hooks + +Never commit secrets, keys, or credentials. If a commit fails or hooks +reject it, fix the issue and create a new commit — do not amend the +failed one. + +Git identity is preconfigured (`glm-5.3-flash `). +Do not change `git config`, skip hooks, or use `git commit -i`. + +## Project Conventions (Rust / git-server workspace) + +This is alkgit — a self-hosted git server: repository storage, the git +smart protocol served over HTTP and SSH interfaces, and the `alkgitd` +binary that assembles it on the alk stack (alkcall, alkhttp, alktls, +alkvault) and gitoxide (gix). The project is a cargo workspace of +sub-crates under `crates/`. The conventions below apply to all work in +`crates/*/src` and `tests/`. They mirror +`.opencode/agents/implementation-specialist.md` §Project Conventions and +are repeated here so they apply to every session, not just spawned +implementation agents. + +1. **No comments in code** unless the user explicitly asks. This is a + project-wide convention. Doc comments (`///`, `//!`) are fine and + expected on public API. Inline `//` comments only when the user asks + or when a non-obvious safety/correctness constraint would otherwise be + missed (e.g., "ACL must run before the first advertised ref line — ref + names leak repository existence"). + +2. **Error handling** — `thiserror` for library error types. No panics + in library code. No `unwrap()` or `expect()` outside tests. If you + reach for `unwrap`, the error path wasn't specified — stop and decide + what should actually happen. For poisoned `RwLock`/`Mutex`, use + `unwrap_or_else(|e| e.into_inner())` so a panic in one operation does + not cascade to other operations. + +3. **`tokio` is the async runtime** — all I/O is async. Git protocol + sessions are long-lived streaming conversations; do not introduce + blocking I/O on the async path. Use `tokio::sync` primitives + (`oneshot`, `mpsc`) for correlation; `parking_lot` for short-held + internal locks. + +4. **No secret material on the wire or in plaintext at rest** — the git + protocol, the call protocol payloads, and the metadata store carry no + private keys, API keys, tokens, or decrypted credentials. Stored + credential material lives in alkvault; metadata holds vault references. + Outbound credentials flow through `Capabilities` injected at the + assembly layer → `OperationContext.capabilities` → handler. See the + no-env-vars invariant below and the alkcall ADRs (ADR-010, ADR-017). + +5. **No-env-vars invariant** — no handler reads outbound credentials from + any source other than `OperationContext.capabilities`. The credential + injection path is vault → assembly layer → `Capabilities` → handler. + This is a spec-level invariant, not a runtime convention. + +6. **Visible-surface = authorized-surface** — the reason this project + exists (a self-hosted gitea got pwned via an internal API reachable + over the internet). Every operation exposed on the wire is gated by + alkcall's `AccessControl::check(peer_identity)`; ops with + `Visibility::Internal` are never wire-callable; no endpoint exists that + a caller cannot see themselves authorized for. There is no + unauthenticated endpoint unless a repo is explicitly public, and push + is always authenticated. This is a spec-level invariant. + +7. **gitoxide for git primitives, no shelling out** — storage and pkt-line + come from the gix crates (pinned in the workspace manifest). The + serving path never spawns the `git` binary (no GPL dependency, no + process-injection surface). The server half of the smart protocol is + ours; see `docs/research/git-protocol.md` for the inventory. + +8. **License hygiene** — this project is MIT OR Apache-2.0. Never copy or + derive code from MPL-2.0-licensed reference projects; reading gitoxide + (MIT OR Apache-2.0) is fine. The policy and the rationale live in + `docs/research/reference-policy.md` — do not name the + incompatible-licensed project in docs, README, or commit messages. + +9. **Wire formats are stable one-way doors** — once a wire surface is + published (the git smart protocol is defined upstream and not ours to + change; any alkgit-specific alkgit↔alkgit framing will get ADRs when it + exists), its shape must not change. Protocol capability advertisement + is honest: never advertise what we don't serve. + +10. **Feature flags** — optional surface is feature-gated: `sha256` (hash + algorithm passthrough; `sha1` is the default and pinned in the + workspace manifest) and `acme` (alktls ACME wiring in the binary). + Base crates compile lean. Verify both `cargo test` (default) and + `cargo test --all-features` pass if features are added. + +11. **Bounded resources** — every protocol session carries wall-clock, + size, and round limits (max negotiation rounds, max receive-pack size, + session timeout). Git servers are internet-facing; unbounded loops and + unbounded buffers are bugs. + +12. **Repo identity is registry-resolved** — repo names arriving on the + wire are IDs resolved against a server-side registry to configured + storage roots. Wire-supplied names are never used as filesystem paths + directly (path traversal is a protocol-level input, not a config + value). + +13. **Naming** — Rust standard: `snake_case` for functions/variables/ + modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for + constants. + +14. **Module structure** — one module per file under `src/`, re-exported + from `src/lib.rs`. Public API surface is `lib.rs` re-exports. + Workspace members under `crates/`: `alkgit-core` (storage), + `alkgit-transport` (smart protocol), `alkgit-http` (http front door), + `alkgit-ssh` (ssh front door), `alkgitd` (binary). + +## Verification Commands + +Run these before committing. All must pass. + +```bash +cargo test # full suite (workspace) +cargo clippy --all-targets -- -D warnings +cargo fmt --check +cargo doc --no-deps # if docs changed +cargo publish --dry-run --allow-dirty # before a release (per crate) +``` + +If feature flags are added, also run `cargo test --all-features` and +`cargo clippy --all-features --all-targets -- -D warnings`. + +## Lifecycle Status + +The project is in **SDD phase 0** (exploration) — see +`docs/sdd_process.md` and `docs/research/README.md`. There is no +architecture yet (`docs/architecture/` is empty; ADR numbering starts at +001 when phase 1 begins). POCs land under `.worktrees/research/` or as +scratch dirs; their findings go into `docs/research/`. Until phase 1 +produces ADRs, "the architecture says" has no referent — cite +`docs/research/` docs instead. + +## Architecture Context + +- `docs/research/` — phase 0 research (current source of truth for design + direction): + - `vision.md` — vision, guiding principles, non-goals + - `gitoxide.md` — gix capability/version alignment (sha1 feature + pinning is mandatory; `default-features = false` without a hash + feature does not compile) + - `alk-stack.md` — alkcall/alkhttp/alktls/alkvault integration surface + - `git-protocol.md` — server-side smart-protocol inventory (what we + own: advertisement, ls-refs, fetch negotiation, receive-pack) + - `reference-policy.md` — license/reuse policy (MPL-2.0 prior art: + facts only, never code) + - `pocs.md` — POC-1..3 plan gating phase 0 completion +- Sibling crates (published, MIT OR Apache-2.0): `alkcall 0.8` (call + + channels RPC with ACL; `BiStream` is the git-session substrate), + `alkhttp 0.5` (HTTP serving + call adapters), `alktls 0.1` (rustls + + ACME), `alkvault 0.1` (secret encryption). Their architecture docs live + in their own repos; alkcall ADRs referenced above are at + `/workspace/@alkdev/alkcall/docs/architecture/decisions/`. +- gitoxide reference clone: `/workspace/gitoxide` (matches published + 0.87.1 plus a few unreleased commits — pin crates.io versions; treat + the clone as a reading aid only, never a path dependency). \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..92aa9df --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,46 @@ +[workspace] +resolver = "2" +members = [ + "crates/alkgit-core", + "crates/alkgit-transport", + "crates/alkgit-http", + "crates/alkgit-ssh", + "crates/alkgitd", +] + +[workspace.package] +version = "0.0.1" +edition = "2021" +rust-version = "1.88" +license = "MIT OR Apache-2.0" +repository = "https://git.alk.dev/alkdev/alkgit" + +[workspace.dependencies] +alkgit-core = { path = "crates/alkgit-core", version = "0.0.1" } +alkgit-transport = { path = "crates/alkgit-transport", version = "0.0.1" } +alkgit-http = { path = "crates/alkgit-http", version = "0.0.1" } +alkgit-ssh = { path = "crates/alkgit-ssh", version = "0.0.1" } +alkcall = "0.8" +alkhttp = "0.5" +alktls = "0.1" +alkvault = "0.1" +gix = { version = "0.87", default-features = false, features = ["sha1"] } +gix-packetline = "0.22" +gix-protocol = "0.65" +gix-transport = "0.59" +gix-pack = { version = "0.74", features = ["sha1"] } +gix-odb = { version = "0.84", default-features = false, features = ["sha1"] } +gix-ref = "0.67" +gix-object = { version = "0.64", features = ["sha1"] } +gix-hash = { version = "0.26", features = ["sha1"] } +tokio = { version = "1", features = ["rt-multi-thread", "io-util", "net", "fs", "time", "sync", "macros"] } +futures = "0.3" +bytes = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" +tracing = "0.1" +parking_lot = "0.12" + +[profile.release] +lto = "thin" \ No newline at end of file diff --git a/crates/alkgit-core/Cargo.toml b/crates/alkgit-core/Cargo.toml new file mode 100644 index 0000000..1f499ef --- /dev/null +++ b/crates/alkgit-core/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "alkgit-core" +description = "Core git plumbing for alkgit: repository storage, ref management, pack handling, and access rules — transport-agnostic" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +gix = { workspace = true } +gix-odb = { workspace = true } +gix-pack = { workspace = true } +gix-ref = { workspace = true } +gix-object = { workspace = true } +gix-hash = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[features] +default = [] +sha256 = ["gix/sha256"] \ No newline at end of file diff --git a/crates/alkgit-core/src/lib.rs b/crates/alkgit-core/src/lib.rs new file mode 100644 index 0000000..45278f2 --- /dev/null +++ b/crates/alkgit-core/src/lib.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/crates/alkgit-http/Cargo.toml b/crates/alkgit-http/Cargo.toml new file mode 100644 index 0000000..043c8e8 --- /dev/null +++ b/crates/alkgit-http/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "alkgit-http" +description = "HTTP interface for alkgit: git smart-http (info/refs, git-upload-pack, git-receive-pack) and the admin API over alkhttp" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +alkgit-core = { workspace = true } +alkgit-transport = { workspace = true } +alkhttp = { workspace = true } +alkcall = { workspace = true } +bytes = { workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[features] +default = [] +sha256 = ["alkgit-transport/sha256"] \ No newline at end of file diff --git a/crates/alkgit-http/src/lib.rs b/crates/alkgit-http/src/lib.rs new file mode 100644 index 0000000..45278f2 --- /dev/null +++ b/crates/alkgit-http/src/lib.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/crates/alkgit-ssh/Cargo.toml b/crates/alkgit-ssh/Cargo.toml new file mode 100644 index 0000000..b2308ab --- /dev/null +++ b/crates/alkgit-ssh/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "alkgit-ssh" +description = "SSH interface for alkgit: git-over-ssh command serving (git-upload-pack / git-receive-pack) on alkcall channels" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +alkgit-core = { workspace = true } +alkgit-transport = { workspace = true } +alkcall = { workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[features] +default = [] +sha256 = ["alkgit-transport/sha256"] \ No newline at end of file diff --git a/crates/alkgit-ssh/src/lib.rs b/crates/alkgit-ssh/src/lib.rs new file mode 100644 index 0000000..45278f2 --- /dev/null +++ b/crates/alkgit-ssh/src/lib.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/crates/alkgit-transport/Cargo.toml b/crates/alkgit-transport/Cargo.toml new file mode 100644 index 0000000..7332124 --- /dev/null +++ b/crates/alkgit-transport/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "alkgit-transport" +description = "Git smart-protocol transport for alkgit: packetline framing, capability advertisement, upload-pack and receive-pack serving over alkcall BiStreams" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +alkgit-core = { workspace = true } +alkcall = { workspace = true } +gix-packetline = { workspace = true } +gix-protocol = { workspace = true } +gix-transport = { workspace = true } +gix-hash = { workspace = true } +bytes = { workspace = true } +tokio = { workspace = true } +futures = { workspace = true } +thiserror = { workspace = true } +tracing = { workspace = true } + +[features] +default = [] +sha256 = ["alkgit-core/sha256"] \ No newline at end of file diff --git a/crates/alkgit-transport/src/lib.rs b/crates/alkgit-transport/src/lib.rs new file mode 100644 index 0000000..45278f2 --- /dev/null +++ b/crates/alkgit-transport/src/lib.rs @@ -0,0 +1 @@ +#![forbid(unsafe_code)] diff --git a/crates/alkgitd/Cargo.toml b/crates/alkgitd/Cargo.toml new file mode 100644 index 0000000..eb7b291 --- /dev/null +++ b/crates/alkgitd/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "alkgitd" +description = "The alkgit server binary: assembles storage, smart protocol, HTTP, SSH, and TLS into one self-hosted git server" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "alkgitd" +path = "src/main.rs" + +[dependencies] +alkgit-core = { workspace = true } +alkgit-transport = { workspace = true } +alkgit-http = { workspace = true } +alkgit-ssh = { workspace = true } +alkhttp = { workspace = true } +alktls = { workspace = true } +alkvault = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +thiserror = { workspace = true } + +[features] +default = [] +sha256 = ["alkgit-http/sha256"] +acme = ["alktls/acme"] \ No newline at end of file diff --git a/crates/alkgitd/src/main.rs b/crates/alkgitd/src/main.rs new file mode 100644 index 0000000..96497ac --- /dev/null +++ b/crates/alkgitd/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("alkgitd: skeleton — server assembly lands after architecture (SDD phase 1)"); +} diff --git a/docs/research/README.md b/docs/research/README.md new file mode 100644 index 0000000..164fba2 --- /dev/null +++ b/docs/research/README.md @@ -0,0 +1,30 @@ +# alkgit Research Index + +Phase 0 (exploration) research. Feeds phase 1 (architecture). + +| Doc | Topic | Status | +|---|---|---| +| [vision.md](vision.md) | Vision, guiding principles, non-goals, phase-0 checklist | draft v1 | +| [gitoxide.md](gitoxide.md) | gitoxide (gix) capability + version alignment | initial pass complete | +| [alk-stack.md](alk-stack.md) | alk stack fit, integration surface, gitea-lesson constraints | initial pass complete | +| [git-protocol.md](git-protocol.md) | Server-side git smart protocol inventory (what we own) | initial pass complete | +| [reference-policy.md](reference-policy.md) | Licenses, reference projects, reuse policy | complete | +| [pocs.md](pocs.md) | POC plan (what to validate before architecture commits) | planned | + +## Key findings so far + +- gitoxide covers storage + pkt-line; the **server half of the smart + protocol is ours to write** (gix-protocol/transport are client-side). +- Published gix 0.87.1 == local clone base; pin crates.io versions. +- alkcall `BiStream` is the natural substrate under pkt-line for both ssh + and http paths. +- The incompatible-license prior art confirms feasibility but contributes + nothing; policy in [reference-policy.md](reference-policy.md). +- `gix` with `default-features = false` requires an explicit hash feature; + workspace pins `sha1` with a `sha256` passthrough feature everywhere. + +## Convergence criteria (phase 0 → phase 1) + +Phase 0 is done when POC-1..3 have results and a recommended-approach +summary is written here (append below). Then the Architect produces +`docs/architecture/` per sdd_process. \ No newline at end of file diff --git a/docs/research/alk-stack.md b/docs/research/alk-stack.md new file mode 100644 index 0000000..ca515a7 --- /dev/null +++ b/docs/research/alk-stack.md @@ -0,0 +1,94 @@ +# Research: alk stack fit for a git server + +**Status**: initial pass complete +**Date**: 2026-09-19 +**Sources**: local sibling crates at `/workspace/@alkdev/{alkcall,alkhttp,alktls,alkvault}` +(all MIT OR Apache-2.0; alkcall/alkhttp/alktls published on crates.io, +alkvault published at 0.1.0). + +## TL;DR + +The alk stack covers the parts of a self-hosted git server that were +historically the attack surface: the HTTP and SSH front doors, the wire +framing, TLS/ACME, and secret storage. alkgit contributes the git brain +(storage + smart protocol + repo/permission model) and the server binary that +assembles everything. The architectural match with alkcall is unusually good: +**alkcall's `BiStream` is exactly the byte-stream abstraction git's +packetline-based protocol wants underneath it** — the smart protocol is a +pair of unidirectional streams with request/response framing, which maps +cleanly onto `BiStream::write_half` / `read_half`. + +## How the pieces map + +| alkgit need | alk crate | mechanism | +|---|---|---| +| Authenticated transport with per-peer identity | alkcall | `Connection`, `Identity`, `IdentityProvider` | +| Internal/external operation split | alkcall | `Visibility::Internal` ops are never wire-callable (ADR-017/024) | +| Per-repo/per-op authorization | alkcall | `AccessControl::check(peer_identity)` + `required_scopes` | +| HTTP front door for git smart-http + admin API | alkhttp | HTTP/1.1 + HTTP/2 serving, adapters for call protocol | +| TLS + ACME for the HTTP endpoint | alktls | rustls server configs, cert resolvers, ACME state machine | +| Encrypted storage of tokens/keys at rest | alkvault | AES-256-GCM vault | +| Byte streams under pkt-line | alkcall | `BiStream` handed to handlers via `ProtocolHandler::connected` | + +## The gitea lesson, restated as alkgit design constraints + +The pwned-gitea incident (CVE-2026-59774) was reachable because an internal +API was exposed over the internet regardless of auth — the openapi-spec +pattern of "describe everything, enforce somewhere" is the structural +antipattern. alkcall already fixes the API-exposure half: only ops the caller +has privileges for are even visible, and internal ops are structurally +unreachable from the wire. alkgit must extend the same philosophy: + +1. **No unauthenticated surface by default.** Even `info/refs` advertisement + requires an authenticated session unless a repo is explicitly public. + Anonymous-clone is a per-repo opt-in, not a global default. +2. **Admin API = internal ops.** Repo/user/permission management rides the + call protocol as `Visibility::Internal` ops over the admin interface + (or via alkhttp with auth); there is no admin endpoint that exists on the + same unauthenticated surface as git traffic. +3. **No plaintext secrets in the DB.** Credentials/tokens go through + alkvault; the metadata store holds references, not keys. +4. **Blast-radius thinking carries into the design**: single binary, no + plugin execution, no markdown renderer by default (a web UI, if it ever + exists, is a separate phase and separate threat model). + +## Interfaces in alkcall terms + +Git transport framing per interface: + +- **SSH**: the alkcall channels protocol carries a channel whose stream is + the git pkt-line conversation. The client (`git clone ssh://...`) expects + an exec of `git-upload-pack ''`; with alkcall the "SSH channel-like" + layer provides the framing and identity, and we dispatch on the requested + command string. alkcall's channel model (channel 0 = call registry, data + channels = `BiStream`) maps to: one data channel per git session. +- **HTTP**: alkhttp hosts the smart-http endpoints (`GET + //info/refs?service=git-upload-pack`, `POST //git-upload-pack`, + `POST //git-receive-pack`). Smart-http is stateless per request + (V0/V1) or one-shot (V2), which fits the http adapter model; the response + body is a pkt-line stream we generate. + +Both interfaces converge on the same core: **a `BiStream`-like duplex +session + repo identity + caller identity** → `alkgit-transport` runs the +smart protocol and produces/consumes packs via `alkgit-core`. The interfaces +are thin adapters; all policy (auth, ACL, limits) lives before the transport +layer gets the stream. + +## What to verify before architecture commits (POC candidates) + +1. **BiStream ↔ pkt-line fit**: run a real `git clone` handshake over an + alkcall `Connection` using `gix-packetline` async codec end-to-end. +2. **HTTP smart protocol shape**: confirm alkhttp's streaming response body + can carry a pack (chunked) and that request bodies stream in without + buffering the whole POST in memory (receive-pack can be gigabytes). +3. **alkcall backpressure/limits vs git sessions**: large clones are + long-lived, high-throughput, single-direction streams; confirm channels + buffer limits (ADR-040) don't fight pack streaming, or route git sessions + as raw duplex streams rather than chunk-framed channels if needed. +4. **alktls ACME**: config shape for the http endpoint; likely trivial. + +## Version pins in workspace manifests + +- `alkcall 0.8` (published), `alkhttp 0.5` (published), `alktls 0.1` + (published), `alkvault 0.1` (published). The workspace root already pins + these; bump via workspace.dependencies when siblings release. \ No newline at end of file diff --git a/docs/research/git-protocol.md b/docs/research/git-protocol.md new file mode 100644 index 0000000..5c363dc --- /dev/null +++ b/docs/research/git-protocol.md @@ -0,0 +1,118 @@ +# Research: git smart protocol (server side) — what we must implement + +**Status**: initial pass complete +**Date**: 2026-09-19 +**Sources**: gitoxide source reading (client-side parsers as reference), +git http-protocol + packfile protocol specs (git-scm.com protocol docs), +observation of gitserver's shape as a sanity check that the surface is this +small (no code reused — MPL-2.0 incompatible with our MIT/Apache-2.0; see +`license-note.md`). + +## TL;DR + +The server-side surface is well-bounded: capability advertisement (V0/V1 and +V2 flavors), `ls-refs`, fetch negotiation (wants/haves → acks → pack), +receive-pack (pack ingestion + ref update CAS + status report), plus the +stateless HTTP framing and the SSH exec framing around them. gitoxide gives +us every primitive (pkt-line codec, pack parse/write, ref transactions, +fsck); what's missing is only the *orchestration*, which is ours to write — +and that's the layer where our auth/ACL model lives, which is exactly where +we want to own code. + +## Protocol surface inventory + +### V2 (the target; what modern git speaks first) + +1. Client sends `GIT_PROTOCOL=version=2` (SSH: env line / http: header). +2. Server replies with capability advertisement: `version 2`, then + capabilities as pkt-lines: `agent=...`, `ls-refs=...`, `fetch=...` + (shallow, filter, sideband-64k, packfile-uris...), `object-format=sha1`. +3. `command=ls-refs` with args (peel, symrefs, ref-prefix) → server streams + ref lines then flush. +4. `command=fetch` with args (want lines, have lines, done, thin-pack, + no-progress, include-tag, shallow/deepen...) → server sends + `acknowledgments` section (acked ids or `NAK`), then either + `ready` + `packfile` section (pack streamed over sideband) if done, or + waits for more haves. + +### V0/V1 (fallback for old clients / http stateless) + +- First response line: `" "` style + advertisement with refs, then fetch loop: wants → haves (NAK/ACK) → pack + over sideband. Stateless-http variant requires the client to POST a + separate request for each round; `ack` state must be re-derived or the + multi-round negotiation declined (we can require V2 for http and keep V0/V1 + for ssh only — OQ candidate). + +### receive-pack (push; mostly version-independent) + +- Client sends update requests (` `) + optional shallow lines + + pack stream (possibly thin). +- Server: validate CAS per ref (old must match current unless zero-id create), + fsck/connectivity the pack, apply ref transaction atomically, reply with + `unpack ` + per-ref `ok|ng ` lines. + +### Framing per interface + +- **SSH**: single exec request `git-upload-pack ''` / + `git-receive-pack ''` / `git-upload-archive`; pkt-line on stdin/stdout; + V2 via env var; sideband on fetch. +- **HTTP**: `GET /info/refs?service=` (advertisement in + `# service=git-upload-pack` preamble for smart clients), + `POST /` with pkt-line body; content-type + `application/x-git-{upload,receive}-pack-*`; chunked streaming both ways; + `Cache-Control: no-cache`. + +## Server-side pack generation — the crux + +`gitserver` (MPL-2.0, read-only reference) demonstrates the pragmatic path: +compute the pack from wants/haves and hand it to the response writer — they +use `gix` + hand-rolled protocol_v2 (742 LOC) and receive_pack (549 LOC) for +the whole protocol surface. We cannot copy that code, but its size confirms +the surface is small enough to own outright with gitoxide primitives: + +- Pack generation options to POC: + a. `gix-pack bundle::write` with an in-memory sink (verify it can stream, + not just write files). + b. `gix-pack::data::output::bytes` entries-to-bytes writer fed by an object + walk over `gix-odb` (full control over want/have closure; no file + intermediates). +- Thin packs (delta against client haves) are an optimization — v0 can + declare `thin-pack` unsupported initially; V2 fetch arg parsing must still + accept/decline it gracefully. +- `filter` (partial clone) and `packfile-uris` can be declined in v1 of + alkgit; advertised capabilities must be honest (never advertise what we + don't serve — the gitea-class bug pattern is promising something and + enforcing elsewhere). + +## Negotiation policy (ours to define) + +Minimal correct initial policy: +- V2: respond with full `acknowledgments` (common ids) each round; send pack + on `done`. +- Enforce max rounds/haves budget per session (DoS bound), max pack size for + receive, wall-clock limits for long negotiations. +- Shallow (` deepen/shallow lines) is a v2-later concern; reject with a + clear pkt-line error initially (honest capability advertisement again). + +## Security-relevant protocol notes + +- `git-upload-archive` is a separate command — do not serve it initially. +- Path traversal: repo names on the wire (`git-upload-pack '~/x'` or + `../`) must normalize to a registry lookup — never to a filesystem path + from the wire. Repo IDs resolve to storage roots configured server-side. +- receive-pack ref names: validate against git ref rules (no `..`, no + control chars, no `refs/heads/foo.lock` games) — `gix-ref` name parsing + plus our own deny-list for e.g. `refs/` reserved namespaces. +- The advertisement phase must run ACL before emitting a single ref line + (ref names leak info; private repos must not advertise anything). + +## Open items → architecture OQs + +- OQ: support V0/V1 at all on http (or V2-only http, V0/V1 ssh)? +- OQ: shallow clone support timeline. +- OQ: thin-pack on fetch (requires delta against client have set). +- OQ: pack streaming composition (POC-1 outcome decides bundle-write vs + entries-to-bytes). +- OQ: `object-format=sha256` support policy (feature flag exists; no real + ecosystem need yet — default sha1, keep flag). \ No newline at end of file diff --git a/docs/research/gitoxide.md b/docs/research/gitoxide.md new file mode 100644 index 0000000..a904183 --- /dev/null +++ b/docs/research/gitoxide.md @@ -0,0 +1,105 @@ +# Research: gitoxide (gix) as the git engine + +**Status**: initial pass complete +**Date**: 2026-09-19 +**Sources**: local clone at `/workspace/gitoxide` (repo HEAD `77c8cd956`, +tag-described as `gix-transport-v0.59.2-130-g77c8cd956`), crates.io API. + +## TL;DR + +gitoxide covers the storage layer (odb, packs, refs, objects, fsck) and gives +us the packetline codec, but it does **not** give us a git *server*. `gix-protocol` +and `gix-transport` are client-side (fetch/clone from the point of view of the +machine asking for objects). The server half of the smart protocol — capability +advertisement, `ls-refs` command dispatch, fetch negotiation on the serving side, +pack generation on demand, and receive-pack application with ref transaction and +update-hook semantics — has to be built by us. This is expected and acceptable: +it is exactly the narrow protocol-shim layer our security model wants to own +anyway (see `git-protocol.md`). + +## Version alignment (important) + +| crate | crates.io max stable | local clone | note | +|---|---|---|---| +| gix | 0.87.1 | 0.87.1 | in sync | +| gix-packetline | 0.22.2 | 0.22.2 | in sync | +| gix-transport | 0.59.2 | 0.59.2 | in sync | +| gix-protocol | 0.65.1 | 0.65.1 | in sync | +| gix-pack | 0.74.2 | 0.74.2 | in sync | +| gix-odb | 0.84.0 | 0.84.0 | in sync | +| gix-ref | 0.67.1 | 0.67.1 | in sync | +| gix-object | 0.64.1 | 0.64.1 | in sync | +| gix-hash | 0.26.2 | 0.26.2 | in sync | +| gix-fsck | 0.25.1 | 0.25.1 | in sync | + +The published `0.87.1` (2026-08-24) matches the clone; the clone carries a +handful of unreleased commits on top. **Pin published crates.io versions** in +our manifests; treat the clone as a reading/reference aid, not a path +dependency. If an unreleased fix turns out to be required, that is an OQ for +architecture (gitoxide git-patch or `[patch]` section is the fallback). + +## Hash algorithm gotcha (encountered live) + +`gix` with `default-features = false` and no hash feature fails to compile: +`gix-hash` emits `compile_error!("Please set either the sha1 or the sha256 +feature flag")`. The workspace manifest now pins `features = ["sha1"]` +throughout (gix, gix-pack, gix-odb, gix-object, gix-hash) and our crates carry +a matching `sha256` passthrough feature for the future. This is a +compile-time-rejected invariant, so the config can't drift silently. + +## What we can use off the shelf + +### Storage (strongest area — use as-is) +- `gix-odb` — object database with loose + packed stores, dynamic multi-index + loading, `Handle` with caching; the `dynamic` store refreshes on mtime + changes, which suits a long-running server. +- `gix-pack` — pack data reading (index file, multi-index, delta resolution) + and pack *writing* (`bundle::write` → `write_to_directory` for + index-from-stream). Pack writing produces index+pack into a directory; + streaming a pack directly to a socket needs evaluation (POC candidate). +- `gix-ref` — ref store with `transaction` module (compare-and-swap semantics, + reflog), which is what receive-pack needs for atomic ref updates. +- `gix-object` — object parsing/encoding (commit, tree, tag, blob). +- `gix-fsck` — connectivity checks for received packs. +- `gix-discover` — repository discovery / `.git` dir resolution. +- `gix` facade — repo opening, config, etc. For library-developer usage gitoxide + recommends `default-features = false` + only the components needed; follow + that to keep compile times down. + +### Wire format (use as-is) +- `gix-packetline` — pkt-line encode/decode, both blocking and `futures-io` + async (`async-io` feature). This is the one crate both the http and ssh + interfaces need most; it is small and stable (0.22.x). +- `gix-transport` — defines `Protocol` (V0/V1/V2), `client::MessageKind`, and + the fetch-side abstractions. Only the *types* are reusable server-side; + its transport implementations (client) are not what we need. + +### Client side (exists, mostly irrelevant for us) +- `gix-protocol` — handshake, `ls-refs`, fetch negotiation *from the client + side* (`fetch::Response::from_line_reader` parses server responses; the + negotiate module builds request arguments). Useful as a *reference* for + response shapes we must produce server-side, and possibly reused for + alkgit-to-alkgit replication later. `gix-transport/src/lib.rs` exports only + `pub mod client` — confirming no server-side half exists upstream. + +## What gitoxide does NOT give us (we own this) + +1. **Capability advertisement** (V0/V1 first-want line and V2 capability + handshake) — must produce ourselves. +2. **Server-side command dispatch** — `ls-refs`, `fetch` V2 command parsing + and the request→response state machine. +3. **Server-side negotiation** — evaluating client haves against our refs + (ack/NAK logic, shallow handling on the serve side). +4. **On-demand pack generation for a fetch** — pack writing exists + (`gix-pack bundle::write`) but "stream a pack computed from a want/have set + to a socket" composition needs a POC; worst case we walk objects ourselves + via odb and feed `gix-pack::data::output::bytes` entries-to-bytes writers. +5. **receive-pack** — parsing client pack stream (`gix-pack::data::input` + can parse a pack from a reader), fsck, ref CAS updates via `gix-ref` + transaction, reporting status (`unpack ok`/`ng` lines). + +## License + +MIT OR Apache-2.0 — same as ours. Clean to depend on and to read for +inspiration (attribution not required under either license, though NOTICE-type +courtesy is fine). \ No newline at end of file diff --git a/docs/research/pocs.md b/docs/research/pocs.md new file mode 100644 index 0000000..5227a0d --- /dev/null +++ b/docs/research/pocs.md @@ -0,0 +1,71 @@ +# alkgit POC Plan + +POCs live in research worktrees (`.worktrees/research//`) per +sdd_process phase 0, or as scratch experiments outside the main workspace +tree if a worktree isn't available yet. Each POC records: hypothesis, +method, result (proceed/pivot/block), and what it changes in the research +docs. + +## POC-1: pkt-line over alkcall BiStream + +**Hypothesis**: a full V2 fetch handshake (`ls-refs` + one `fetch` round) +between real `git` CLI (as client, `git clone`/`git fetch`) and a Rust +listener that bridges alkcall `BiStream` to `gix-packetline`'s async codec +can be completed. + +**Method sketch**: alkcall `Connection` accepted over a local stream; spawn +a handler that receives a `BiStream`; wrap `read_half`/`write_half` in the +pkt-line async reader/writer; advertise V2 capabilities with honest feature +list; parse `command=ls-refs`, emit refs from a fixture repo (created with +`gix`), flush. Client side: `git -c protocol.version=2 clone` against a +local bridge (POC can start with plain tcp and only simulate the alkcall +side if alkcall dialing adds friction — the alkcall-fit part can be tested +with `Connection::from_stream`). + +**Success**: git prints its ref advertisement fetch result without error; +all framing validated against real git's parser (the strictest pkt-line +validator available). + +**Risks**: gix-packetline `async-io` feature uses `futures-io` traits — +confirm alkcall stream halves implement `AsyncRead`/`AsyncWrite` +compatibility (they should, being tokio io objects; may need a thin adapter). + +## POC-2: server-side pack generation + +**Hypothesis**: given a fixture repo and a want/have set, we can produce a +valid pack stream in memory that `git verify-pack`/`git unpack-objects` +accepts, using one of: +a. `gix-pack::bundle::write` into a temp dir then read the file back + (correctness baseline), or +b. `gix-pack::data::output::bytes` fed by an odb object walk (streaming). + +**Method**: build fixture repo via `gix` API or a scripted `git` (fixture +creation may shell out to `git` — only serving-path must be git-binary-free). +Try (b) first; fall back to (a) to establish the correctness baseline. + +**Success**: `git clone` from a bridge that serves the generated pack +completes and `git fsck` passes in the clone. + +**Decision to make**: streaming composition that doesn't materialize the +whole pack in memory for large repos — record memory behavior for a +10k-object repo at minimum. + +## POC-3: smart-http shape through alkhttp + +**Hypothesis**: alkhttp can serve `GET /info/refs` and stream a POST body +(ingest without full buffering) for `git-receive-pack`. + +**Method**: minimal alkhttp service exposing a fake +`/repo.git/info/refs?service=git-upload-pack` and +`/repo.git/git-upload-pack` wired to the POC-1 bridge; run real `git clone +http://...` against it; measure whether alkhttp's request-body API streams +or buffers (inspect/measure, not guess). + +**Success**: real git clones over http; documented answer on body streaming +for receive-pack sizing. + +## Sequencing + +POC-1 first (the BiStream/packetline fit is the load-bearing assumption). +POC-2 next (the crux of fetch). POC-3 last (http shape). Each POC updates +the corresponding research doc with results and a proceed/pivot/block note. \ No newline at end of file diff --git a/docs/research/reference-policy.md b/docs/research/reference-policy.md new file mode 100644 index 0000000..dcd460e --- /dev/null +++ b/docs/research/reference-policy.md @@ -0,0 +1,67 @@ +# Research: reference projects, licenses, and reuse policy + +**Status**: complete +**Date**: 2026-09-19 + +## TL;DR + +gitoxide (MIT OR Apache-2.0) is a dependency and a readable reference — clean. +One similar Rust git server exists (`gitserver`, MPL-2.0) but its license is +incompatible with MIT/Apache-2.0 distribution, so **no code may be copied, +transcribed, or derived from it**. Reading it to confirm facts (e.g. "the +protocol surface is ~1300 LOC") is fine; reading it for implementation +technique is not. We do not name it in alkgit docs, README, or commit +messages; this research doc names it once so the policy itself is recorded. + +## License matrix + +| Source | License | Use as dependency | Read for design facts | Copy/derive code | +|---|---|---|---|---| +| gitoxide (`/workspace/gitoxide`) | MIT OR Apache-2.0 | yes | yes | yes (attribution per license terms) | +| alkcall / alkhttp / alktls / alkvault | MIT OR Apache-2.0 (ours) | yes | yes | yes | +| gitserver (`/workspace/gitserver`) | MPL-2.0 | **no** | facts only, minimally | **no** | +| russh (`/workspace/russh`) | Apache-2.0 | yes (if we ever need raw sshd) | yes | yes | +| git itself (C) | GPL-2.0 | no (never ship binaries) | protocol behavior yes | no | + +MPL-2.0 note: it is file-level copyleft. Linking an MPL-2.0 crate into our +MIT/Apache-2.0 binary is technically possible (MPL is weak copyleft, +LGPL-style), but distributing *this project* as MIT/Apache-2.0 while +containing MPL-2.0 code-derived files would require keeping those files +separately licensed — a mess for a crate intended for crates.io. We simply +don't take anything from it: everything it does, gitoxide primitives + our +own protocol layer does. + +## gitserver — what we may legitimately take from it + +Only the **existence proof and shape facts**: +- A single-binary git server in Rust with http interface fits in a small code + footprint when gix provides storage (their core crate is ~3.4k LOC + including pack/ref/http plumbing). +- They hand-rolled pkt-line + protocol_v2 + receive_pack rather than using + `gix-protocol` — consistent with our finding that gix-protocol is + client-side only. +- Their http crate is a thin axum wrapper — consistent with our plan to make + the http adapter thin over alkhttp. + +What we must NOT take: their file contents, function structures, error + taxonomies, test matrices, or README wording. If during implementation we +find ourselves about to write something that would look like their file +structure, stop and design from gitoxide primitives + git's protocol spec +instead. + +## Other prior art (not vendored, for awareness) + +- `rudolfs` (`/workspace/rudolfs`) — a git-lfs server in Rust, not a git + server; relevant later if LFS support is scoped (it is not in v1). +- `sftp-rs` / `russh-sftp` — for an SFTP endpoint; out of scope for v1 + (git over alkcall channels is our ssh story, not an sshd). +- gitea/gitlab — the threat-model references, not code references. + +## Naming policy + +The incompatible-licensed project is referenced in this doc only (and +possibly in future ADR context blocks explaining "we looked at prior art"). +It does not appear in README, architecture docs, or code comments. Reason: +license-hostile maintainers have historically DMCA'd or harassed projects +that even referenced their work in docs; there is no need — our design +sources are git's own protocol specifications and gitoxide. \ No newline at end of file diff --git a/docs/research/vision.md b/docs/research/vision.md new file mode 100644 index 0000000..5d66554 --- /dev/null +++ b/docs/research/vision.md @@ -0,0 +1,101 @@ +# alkgit Phase 0: Vision and Guiding Principles + +**Status**: draft v1 — 2026-09-19 +**Phase**: 0 (exploration) — this document captures WHAT we are building and +WHY before architecture (phase 1) commits to HOW. + +## Vision + +A self-hosted, single-binary git server in Rust: `alkgitd` serves repositories +over **http** and **ssh** interfaces with an authenticated-by-default surface, +built on the alk stack (alkcall, alkhttp, alktls, alkvault) and gitoxide. +It exists because self-hosted git platforms (gitea/gitlab) are large +multi-component applications with a long-tail of exposed APIs, plaintext +secret storage, and web-UI attack surface — and because CVE-2026-59774 +(gitea) demonstrated that a single logic bug in an internally-exposed API is +enough for full compromise of the host. Our blast-radius and exposure model +is designed from day one, not retrofitted. + +## The problem, precisely + +1. **Exposure model**: mainstream self-hosted git servers expose a large + HTTP API surface regardless of auth (openapi describes everything; + enforcement is somewhere else). alkcall inverts this: only operations the + caller has privileges for are visible on the wire, and `Visibility::Internal` + ops are structurally unreachable. alkgit inherits that property by using + alkcall as the transport/protocol substrate. +2. **Secret hygiene**: gitea stores secrets plaintext in its DB. alkgit uses + alkvault for anything credential-shaped; the metadata store holds vault + references, never plaintext secrets. +3. **Footprint**: a git server is a small program (storage + smart protocol + + two front doors). The platform features (issues, PRs, wikis, CI) are where + the CVEs live. alkgit v1 is deliberately just the git server; everything + else stays out. +4. **Language**: the whole serving path is Rust (memory-safe, no C deps in + the packet path). + +## Guiding principles (architecture must honor) + +1. **Authenticated by default** — no unauthenticated endpoint exists unless a + repo is explicitly marked public; even then, advertisement is the only + anonymous surface, and push is always authenticated. +2. **Visible-surface = authorized-surface** — the alkcall model end to end; + no endpoint exists on the wire that a caller cannot see themselves + authorized for; internal/admin ops are never wire-reachable. +3. **No plaintext secrets at rest** — alkvault or nothing. +4. **Thin interfaces, one core** — http and ssh are adapters that authenticate, + resolve a repo, and hand a duplex stream to the transport layer. Policy + lives in exactly one place. +5. **Honest capability advertisement** — the git protocol advertises only + what we actually serve (this is both protocol correctness and the + security pattern: promise/enforce in the same place). +6. **gitoxide for storage/wire primitives** — never shell out to the `git` + binary (no GPL dependency, no process injection surface); own the smart + protocol server layer ourselves on gitoxide primitives. +7. **Bounded resources** — every protocol session carries wall-clock, size, + and round limits; git servers are internet-facing by definition. + +## Non-goals for v1 + +- Web UI of any kind. +- Issues/PR/review features. +- Git LFS (later phase, separate decision). +- federation/replication between alkgit instances (later phase). +- Serving as a general sshd (only the git command surface). +- Windows as a serving platform (linux first; keep code portable-ish but + don't test it). + +## Sub-crate shape (provisional, matches workspace skeleton) + +| crate | role | +|---|---| +| `alkgit-core` | storage: repos, refs, odb, pack read/write, access-rule types | +| `alkgit-transport` | smart protocol: pkt-line sessions, advertise, ls-refs, fetch, receive-pack | +| `alkgit-http` | http front door (smart-http endpoints + admin API via alkhttp) | +| `alkgit-ssh` | ssh front door (git commands over alkcall channels) | +| `alkgitd` | binary: config, assembly, TLS/ACME, serving loops | + +## What phase 0 must still produce before phase 1 + +- [x] gitoxide capability + version research (`gitoxide.md`) +- [x] alk stack fit + integration surface (`alk-stack.md`) +- [x] server-side protocol inventory (`git-protocol.md`) +- [x] license/reference policy (`reference-policy.md`) +- [ ] POC-1: real `git clone` over alkcall BiStream using gix-packetline + (validates BiStream fit + packetline async codec end-to-end) +- [ ] POC-2: server-side pack generation composition (bundle-write vs + entries-to-bytes) for a small want/have set +- [ ] POC-3: http smart-endpoint streaming shape through alkhttp + (chunked pack response, unbuffered receive-pack POST ingestion) +- [ ] Convergence: recommended approach summary feeding phase 1 architecture + +## Immediate threat-model notes carried into architecture + +- The admin API (repo/user/permission management) is alkcall + `Visibility::Internal` ops over an admin-only listener; it is never part + of the git traffic surface. +- Repo names arriving on the wire are registry IDs, never paths; storage + roots are configured server-side only. +- The advertisement phase runs ACL before the first ref line is emitted. +- receive-pack applies ref updates via CAS transactions; hooks/plugins do + not exist in v1 (no arbitrary code execution surface at all). \ No newline at end of file