Resolves review 001 finding A-1 (critical): ADR-012 §3's "scope
git:admin OR ownership" gate is not expressible in alkcall's
AccessControl (AND-composition). Resolution is the review's option (a)
shape with the OR-term generalized: the per-repo grant action set gains
manage, authorize(record, identity, read|write|manage) becomes the
single policy function for git access and repo administration, and the
delete/update/get gate is admin scope OR manage grant (handler-side,
generic FORBIDDEN, unknown-repo = unauthorized per ADR-008). Repo
create seeds the creator's {read, write, manage} grants —
administration is grantable, so collaborators/bots/app-compiled roles
work without global scopes. Ownership stays as alkcall spawn-tracking
(mint at create unchanged); "ownership never implies git access" is
superseded.
- ADR-015 (new): manage grant tier, op gate, flat-grants-as-replication-
substrate, opaque grant-key rule
- ADR-011: action set + policy domain amended, references updated
- ADR-012 §3: gate table replaced, two-tier paragraph superseded
- backend.md/doors.md/overview.md: gate + grant restatements, ADR tables
- OQ-16 (new, deferred(scope)): grant-key identity namespace —
globally-comparable ids for cross-assembly/replicator grant state;
tracker task tasks/architecture/oq-16-grant-identity-namespace.md
- review 001: A-1 marked resolved (ADR-015)
- vision.md: supersession notes (Internal-ops framing, v1 grant set)
Verification: cargo test, clippy -D warnings, fmt --check, doc --no-deps
all clean.
172 lines
9.3 KiB
Markdown
172 lines
9.3 KiB
Markdown
---
|
||
status: draft
|
||
last_updated: 2026-09-25
|
||
---
|
||
|
||
# Backend traits: the storage seam
|
||
|
||
## What this is
|
||
|
||
The payload side of the protocol crate: the traits a service deployer
|
||
implements (or consumes via the feature-gated gix implementation) so the
|
||
git protocol can talk to storage. Per ADR-010 this mirrors alktty's
|
||
`TtyBackend` pattern — traits in-crate, real implementation behind a
|
||
feature. Unlike alktunnels (no backend trait), git needs the seam: pack
|
||
generation/ingestion is too heavy to hard-wire.
|
||
|
||
## The trait family (ADR-010 sub-decision 4, ADR-011/012)
|
||
|
||
Five traits, kept small and orthogonal — the protocol crate never sees
|
||
gix types:
|
||
|
||
1. **`GitRegistry`** — repo id → `RepoRecord` (storage root, visibility,
|
||
grants keyed on the stable logical identity id, action set
|
||
`{read, write, manage}` — ADR-011, ADR-015). Async
|
||
`resolve`; the authoritative mapping (ADR-008); resolution failure is
|
||
indistinguishable from authorization failure (ADR-007). Authorization
|
||
is alkgit-core's policy function evaluated on the record (data in the
|
||
backend, policy in core — an embedder with a foreign permission
|
||
system maps its ACL into grants at resolve time).
|
||
2. **`GitRegistryStore: GitRegistry`** — the write supertrait
|
||
(`put_repo`/`update_repo`/`remove_repo`; alknet ADR-035's read/write
|
||
split shape). The `git/repo/*` ops wrap it (ADR-012 §3). Grants ride
|
||
the record, so grant mutation is `update_repo` (convenience wrappers
|
||
additive later, two-way) — which is also the app's promotion path
|
||
(roles/teams compile to flat grants upstream; ADR-015 §6) and the
|
||
shape a future replication sync would write through (ADR-015 §8).
|
||
3. **`GitRefs`** — listing for advertisement (refs + peeled tags + symref
|
||
targets, the ls-refs response data) and ref transactions (CAS apply
|
||
for receive-pack, name validation per git ref rules + reserved-
|
||
namespace deny-list).
|
||
4. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack
|
||
(`io::Write` consumer), plus `common_haves(repo, haves) -> recognized
|
||
subset` for the negotiation ack loop (a have is recognized iff it
|
||
exists in the object store and is a commit — the honest boundary:
|
||
never ack what we cannot subtract; ADR-014). Negotiation-agnostic
|
||
generation. Missing objects abort with an error, never a broken pack
|
||
(ADR-004).
|
||
5. **`GitPackIngest`** — client pack stream → indexed pack + fsck/
|
||
connectivity report. Thin-pack bases resolve from the server's own odb
|
||
(push sends thin packs by default — ADR-013 §5); the pack lands with a
|
||
`.keep` guard; missing objects → `unpack ng`. It *prepares* the
|
||
validated ref updates; the transaction itself is applied by `GitRefs`
|
||
(single CAS home — ingest validates, refs commits; one transaction per
|
||
push is what makes `atomic` correct — ADR-013 §7). Budgeted (ADR-009
|
||
max pack size); blocking-thread friendly.
|
||
|
||
Minimal-vs-full was the open sub-question; resolved as **full family** —
|
||
each trait is one or two methods plus types, and collapsing them (e.g.
|
||
refs into the registry) would force one impl block per downstream where
|
||
independent seams are cheaper to satisfy. The `gix` feature implements
|
||
the three object-storage traits (3–5: `GitRefs`, `GitPackGen`,
|
||
`GitPackIngest`); a downstream with its own object store implements 3–5
|
||
and reuses 1–2 (`GitRegistry`/`GitRegistryStore`), or none of it.
|
||
|
||
## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story)
|
||
|
||
Two independent seams, two default-on features:
|
||
|
||
| Feature | Contents | gitoxide |
|
||
|---|---|---|
|
||
| `gix` | `GitRefs`/`GitPackGen`/`GitPackIngest` impls — the local-disk object-storage engine (odb/pack/ref/object/fsck components) | yes |
|
||
| `registry-file` | `GitRegistry`/`GitRegistryStore` default impl (per-repo record files + in-memory index, config-seeded, op-mutable, atomic writes) | no |
|
||
|
||
- `default-features = false` — wire/protocol layer only (an embedder
|
||
brings its own backends). Both features default-on (batteries
|
||
included); they are independent seams — DB-registry + gix-engine and
|
||
S3-storage + simple-registry combinations both exist downstream.
|
||
- The `gix` **facade crate is not a dependency** (ADR-012 §4): the
|
||
backends drive `gix-odb`/`gix-pack`/`gix-ref` at the component level
|
||
(POC-2's shape); repo opening from a registry-resolved root is
|
||
`gix-discover` + `gix-odb::Store::at`. Verified at implementation.
|
||
- The registry-file store needs no gitoxide; persistence adapters
|
||
(SQLite et al.) are future, separate, additive — ADR-012 §2.
|
||
- Hash: `sha1` pinned (the compile-time-rejected invariant from
|
||
`docs/research/gitoxide.md`); `sha256` passthrough feature (OQ-05
|
||
policy unchanged).
|
||
- Encodes the POC-2 prerequisites by construction: odb handle sharing
|
||
(`Arc<Store>` shared, per-session handles, `prevent_pack_unload()` +
|
||
`ignore_replacements = true`), generation on blocking threads,
|
||
O(counts) memory, missing-objects abort.
|
||
- Received-pack ingestion via `gix-pack::Bundle::write_to_directory_eagerly`
|
||
(`streaming-input`, thin-base lookup) + `gix-fsck` + `gix-ref`
|
||
transactions with `PreviousValue::MustExistAndMatch` CAS (ADR-004, ADR-013
|
||
— the full push state machine is decided there).
|
||
|
||
## Concurrency model
|
||
|
||
- `gix` structures: `parking_lot` short-held locks; per-session handles
|
||
moved into `spawn_blocking` tasks (POC-2's shape: store shared, handle
|
||
per session, generation on blocking threads).
|
||
- Poisoned locks: `unwrap_or_else(|e| e.into_inner())` (convention 2).
|
||
- The traits are `Send + Sync` object-safe; impls run under the
|
||
adapter's tokio context. `GitRegistry::resolve` is async (one call per
|
||
session/request, before the first protocol byte — ADR-011 §6); no
|
||
sync-hot-path constraint exists here, unlike alkcall's accept loop.
|
||
|
||
## Public API surface
|
||
|
||
Crate-root re-exports (the alktty pattern): backend traits + types
|
||
(including `RepoRecord`, `AccessAction`, the `authorize` policy
|
||
function, and the `git/repo/*` op spec+handler pairs), `GitAdapter`/
|
||
`register_openable` (producer), `GitSession` (consumer), substrate
|
||
types, `Limits`, protocol error enums; feature types (`GixBackend`
|
||
family under `gix`, the file registry under `registry-file`) exported
|
||
under their features. The embedder-facing freeze point remains OQ-03
|
||
(narrowed: it is now this crate's own publish).
|
||
|
||
## The two op kinds (ADR-012 §3, the classification)
|
||
|
||
alkgit ships both alkcall op kinds from one crate — the first family
|
||
payload to do so (alktty/alktunnels: open ops only; alknet-docker,
|
||
planned: call ops only):
|
||
|
||
- **Open op** (`alk/git` via `register_openable`; repo id in the
|
||
open-op params) — the binary half: negotiation + ACL point (ADR-007),
|
||
yields the duplex git session.
|
||
- **Call ops** (`git/repo/{create,delete,update,get}`) — the JSON half:
|
||
thin `OperationSpec`+handler pairs over `Arc<dyn GitRegistryStore>`,
|
||
`Visibility::External`, always-on (no gitoxide). `create` is gated by
|
||
the static registry check (`required_scopes: ["git:repo:create"]`);
|
||
`delete`/`update`/`get` carry an empty `AccessControl` and the handler
|
||
evaluates the ADR-015 gate — scope `git:admin` OR the record's
|
||
`manage` grant, via the `authorize` policy function, generic FORBIDDEN
|
||
denial, unknown-repo ≡ unauthorized (ADR-008). Registered by the
|
||
assembler wherever it wants them exposed. Managing records (op scopes)
|
||
and git access (repo grants) are distinct sources — but the record is
|
||
the single per-repo authz surface, and repo creation seeds the
|
||
creator's `{read, write, manage}` grants (ADR-015).
|
||
|
||
## Design Decisions
|
||
|
||
| ADR | Decision | Summary |
|
||
|---|---|---|
|
||
| [004](decisions/004-pack-pipeline.md) | Pack pipeline | `data::output` gen / `data::input` ingestion |
|
||
| [007](decisions/007-acl-before-advertisement.md) | ACL first | registry returns rule inputs |
|
||
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids |
|
||
| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits flow into gen/ingest |
|
||
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, impls behind features |
|
||
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in records, policy in core (amended: ADR-015) |
|
||
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split (gate amended: ADR-015) |
|
||
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-scope-OR-manage gate, create seeds manage |
|
||
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing |
|
||
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` |
|
||
|
||
## Open Questions
|
||
|
||
- **OQ-05**: sha256 policy (deferred(scope)).
|
||
- **OQ-03**: publish-time API freeze inventory (the op set enters it).
|
||
- OQ-04 resolved (ADR-013 — ingestion composition bound: thin-base
|
||
lookup, `.keep` landing, transaction CAS).
|
||
- OQ-02 resolved (ADR-014 — `common_haves` ack seam added to
|
||
`GitPackGen`).
|
||
|
||
## References
|
||
|
||
- `docs/research/gitoxide.md` (API contract notes — normative for the
|
||
gix impl)
|
||
- `docs/research/poc2-findings.md` (generation pipeline + prerequisites)
|
||
- alktty `backend.rs`/`local` module (the trait + feature template)
|
||
- alknet ADR-033/035 (the repo/adapter + read/write-split pattern)
|
||
- ADR-010 (the structural decision), ADR-011/012 (the seam this doc
|
||
specifies) |