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:
1 parent
a633dddd6b
commit
a3cdef909b
20 files changed
+965
No files matched your search
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
.worktrees/
|
||||
@@ -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
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1 @@
|
||||
#![forbid(unsafe_code)]
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
println!("alkgitd: skeleton — server assembly lands after architecture (SDD phase 1)");
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user