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
This commit is contained in:
glm-5.3-flash committed 2026-09-19 15:36:21 +00:00
1 parent a633dddd6b
commit a3cdef909b
20 files changed
+965

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
/target
Cargo.lock
.worktrees/
+199
View File
@@ -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:
`<type>(<scope>): <summary>` — 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 <glm-5.3-flash@alk.dev>`).
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).
+46
View File
@@ -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"
+22
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
#![forbid(unsafe_code)]
+25
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
#![forbid(unsafe_code)]
+21
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
#![forbid(unsafe_code)]
+25
View File
@@ -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"]
+1
View File
@@ -0,0 +1 @@
#![forbid(unsafe_code)]
+31
View File
@@ -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"]
+3
View File
@@ -0,0 +1,3 @@
fn main() {
println!("alkgitd: skeleton — server assembly lands after architecture (SDD phase 1)");
}
+30
View File
@@ -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.
+94
View File
@@ -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 '<repo>'`; 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
/<repo>/info/refs?service=git-upload-pack`, `POST /<repo>/git-upload-pack`,
`POST /<repo>/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.
+118
View File
@@ -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: `"<capabilities> <null-octet> <ref> <obj-id>"` 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 (`<old> <new> <ref>`) + 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 <ok|ng>` + per-ref `ok|ng <ref> <reason>` lines.
### Framing per interface
- **SSH**: single exec request `git-upload-pack '<path>'` /
`git-receive-pack '<path>'` / `git-upload-archive`; pkt-line on stdin/stdout;
V2 via env var; sideband on fetch.
- **HTTP**: `GET /info/refs?service=<name>` (advertisement in
`# service=git-upload-pack` preamble for smart clients),
`POST /<name>` 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).
+105
View File
@@ -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).
+71
View File
@@ -0,0 +1,71 @@
# alkgit POC Plan
POCs live in research worktrees (`.worktrees/research/<task-id>/`) 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.
+67
View File
@@ -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.
+101
View File
@@ -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).