docs(architecture): resolve OQ-06/07/08 — per-repo authz, registry backing, CRUD ops

ADR-011 (resolves OQ-08): per-repo authorization — grants live in repo
records keyed on the stable logical identity id (alkcall ADR-025,
referenced); policy is alkgit-core's authorize() function (public+read
anonymous-first-class, write always authenticated+granted); alkgit
stores no identity records; vault placement resolved as nothing to
place in v1.

ADR-012 (resolves OQ-06/OQ-07): registry backing + write surface —
GitRegistryStore write supertrait (alknet ADR-035 read/write split
shape); registry-file default (per-repo record files + in-memory
index, config-seeded, op-mutable, no gitoxide); git/repo/* CRUD ops
shipped External with scope+ownership ACL (create mints ownership and
seeds creator grants; ownership never implies git access); the
two-op-kind classification recorded (open op + call ops from one
crate, per alkcall ADR-047); recorded split trigger for a downstream
platform crate.

Doc sync: backend.md (five-trait family, feature model split,
two-op-kinds), doors.md + overview.md (authorize policy, dual-kind
crate map), open-questions.md (OQ-06/07/08 resolved), README (ADR
table, current state), oq-06 tracker task closed (resolved early).

Verification: cargo test (default + --no-default-features), clippy
-D warnings, fmt --check.
This commit is contained in:
glm-5.3-flash committed 2026-09-21 16:26:59 +00:00
1 parent 86bf5a0cf0
commit addc874667
8 files changed
+492 -104

No files matched your search

+10 -5
View File
@@ -16,8 +16,10 @@ to a POC finding or research doc, or is flagged as an open question.
Phase 1, architecture committed to the pure-protocol-crate shape (ADR-010;
OQ-09 resolved). All docs below are `draft` except the superseded ADRs.
POC-1/2/3 validated the git protocol half end-to-end against real git
2.43; the remaining design work is the receive-pack state machine (OQ-04)
and backend/identity decisions (OQ-06, OQ-08).
2.43. This cycle settled the auth/backend theme: per-repo authorization
(ADR-011, OQ-08), registry backing + write surface + CRUD ops (ADR-012,
OQ-06/OQ-07). The remaining design work is the receive-pack state machine
(OQ-04) and V2 multi-round negotiation (OQ-02).
## Architecture Documents
@@ -43,13 +45,16 @@ and backend/identity decisions (OQ-06, OQ-08).
| [008](decisions/008-registry-resolved-repo-identity.md) | Wire repo names are registry IDs, never paths | Accepted |
| [009](decisions/009-bounded-resources-budget.md) | Bounded-resources budget model | Accepted |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate (alktty/alktunnels template) | Accepted |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization (grants in records, policy in core) | Accepted |
| [012](decisions/012-registry-backing-and-ops.md) | Registry backing, write surface, CRUD ops, feature split | Accepted |
## Open Questions
All unresolved questions are tracked in [open-questions.md](open-questions.md)
with stable OQ-IDs, priorities, and cross-references. Highest-priority opens:
OQ-04 (receive-pack validation), OQ-06 (registry backing), OQ-08 (registry
identity space + vault placement).
with stable OQ-IDs, priorities, and cross-references. Highest-priority
open: OQ-04 (receive-pack validation). Also open: OQ-02 (multi-round
negotiation), OQ-03 (publish freeze inventory), OQ-05 (sha256,
deferred).
## Document Lifecycle
+74 -27
View File
@@ -14,23 +14,31 @@ git protocol can talk to storage. Per ADR-010 this mirrors alktty's
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)
## The trait family (ADR-010 sub-decision 4, ADR-011/012)
Four traits, kept small and orthogonal — the protocol crate never sees
Five traits, kept small and orthogonal — the protocol crate never sees
gix types:
1. **`GitRegistry`** — repo id → (storage root, visibility, ACL scope).
The authoritative mapping (ADR-008); resolution failure is
indistinguishable from authorization failure (ADR-007). Metadata holds
vault references, never secrets.
2. **`GitRefs`** — listing for advertisement (refs + peeled tags + symref
1. **`GitRegistry`** — repo id → `RepoRecord` (storage root, visibility,
grants keyed on the stable logical identity id — ADR-011). 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).
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).
3. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack
4. **`GitPackGen`** — (repo, wants, haves, limits) → streaming pack
(`io::Write` consumer). Negotiation-agnostic. Missing objects abort
with an error, never a broken pack (ADR-004).
4. **`GitPackIngest`** — client pack stream → indexed pack + fsck/
5. **`GitPackIngest`** — client pack stream → indexed pack + fsck/
connectivity report. It *prepares* the validated ref updates; the
transaction itself is applied by `GitRefs` (single CAS home — ingest
validates, refs commits). Budgeted (ADR-009 max pack size);
@@ -43,13 +51,28 @@ downstream where independent seams are cheaper to satisfy. The `gix`
feature implements all four; a downstream with its own object store
implements 3–4 and reuses 1–2, or none of it.
## The gix feature (default on)
## Feature model (ADR-012 §4, amends ADR-010's single-`gix` story)
- `alkgit = { default-features = true }` — wire layer + gix backend;
`default-features = false` — wire/protocol layer only (an embedder
brings its own backend). Hash: `sha1` pinned (the compile-time-rejected
invariant from `docs/research/gitoxide.md`); `sha256` passthrough
feature (OQ-05 policy unchanged).
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,
@@ -65,16 +88,38 @@ implements 3–4 and reuses 1–2, or none of it.
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.
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,
`GitAdapter`/`register_openable` (producer), `GitSession` (consumer),
substrate types, `Limits`, protocol error enums; `gix`-feature types
(`GixBackend`-family) exported under the feature. The embedder-facing
freeze point remains OQ-03 (narrowed: it is now this crate's own publish,
not a multi-crate freeze).
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), gated by
`required_scopes` (global tier) + ownership (self-owned tier, alkcall
ADR-011 mint-and-own at create). Registered by the assembler wherever
it wants them exposed. Managing records (op ACL) and git access (repo
grants) are different capabilities — ownership never implies git
access (ADR-011, ADR-012 §3).
## Design Decisions
@@ -84,15 +129,15 @@ not a multi-crate freeze).
| [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, gix behind a feature |
| [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 |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split |
## Open Questions
- **OQ-06**: registry backing store (deferred(scope) — the trait is what
matters; the gix feature can ship a config-file/classic-on-disk impl,
and richer backing is downstream's choice).
- **OQ-04**: pack ingestion validation (deferred(unclear)).
- **OQ-05**: sha256 policy (deferred(scope)).
- **OQ-03**: publish-time API freeze inventory (the op set enters it).
## References
@@ -100,4 +145,6 @@ not a multi-crate freeze).
gix impl)
- `docs/research/poc2-findings.md` (generation pipeline + prerequisites)
- alktty `backend.rs`/`local` module (the trait + feature template)
- ADR-010 (the structural decision)
- alknet ADR-033/035 (the repo/adapter + read/write-split pattern)
- ADR-010 (the structural decision), ADR-011/012 (the seam this doc
specifies)
@@ -0,0 +1,127 @@
# ADR-011: Per-repo authorization — grants live in repo records
## Status
Accepted (resolves OQ-08)
## Context
OQ-08 (narrowed by ADR-010) asked for the registry's identity model: what
an identity *is* to the `GitRegistry`, what identity records exist, where
they live, and where credential material goes.
The family precedents constrain the answer:
- alkcall resolves credentials to a stable logical `Identity`
(`IdentityProvider`, ADR-003; `PeerEntry.peer_id` stability across key
and token rotation, ADR-025). `AccessControl::check` (ADR-017) is a
flat scope/resource match against that resolved identity.
- Doors own credential presentation (alkhttp bearer → token; ssh →
fingerprint) and resolve identity before alkgit sees anything.
- alktty/alktunnels gate with a single scope constant because their
resources are session- or deployment-scoped. Git cannot use that shape:
its authorization is **per-repo**, and its primary deployment target
(vision §"Primary deployment target") makes **anonymous fetch on
explicitly-public repos first-class** — which an identity-scoped gate
cannot express, since a static `AccessControl` with restrictions fails
closed on `identity: None`.
So the git analog of `TTY_OPEN_SCOPE` is not a scope constant — it is the
per-repo check itself, at ADR-007's step-3 position. The gap is real and
known: ADR-007 already states "anonymous access on explicitly-public
repos grants the same full read path," a clause the static ACL engine
cannot express.
Two sub-questions rode along: whether identity *records* (credential
paths, scopes) live in the git metadata store, and what vault material
metadata must hold.
## Decision
1. **Identity space is alkcall's; alkgit stores no identity records.**
The subject of every authorization decision is the resolved alkcall
`Identity`, keyed by its stable logical id (`Identity.id` — the
alkcall ADR-025 `peer_id` property, referenced here, not
reimplemented). Credential material, fingerprints, and token hashes
remain in the door/assembly identity-provider seam (alkcall ADR-003,
ADR-025). alkgit's metadata store holds no identity records.
2. **Grants live in repo records, keyed on the logical identity id.**
`RepoRecord` carries `visibility` (Public/Private) and
`grants: identity_id → {read, write}`. Per-repo grants are repo
lifecycle data — created with the repo, administered with it — so
they are stored with the record, not folded into `Identity.resources`
(which is static identity-provider data resolved per credential path;
per-repo entries would bend that model).
3. **Authorization is a policy function in alkgit core**, evaluated
against the record:
```
authorize(record, identity: Option<&Identity>, action: read|write) -> bool
```
- `read` on a Public repo → allowed for anyone, including `None`
(anonymous-first-class; ADR-007's operative clause).
- `read` on a Private repo → identity required with a read grant.
- `write` → identity required **and** a write grant, on every repo
(push is always authenticated — vision §"Primary deployment
target"; ADR-007).
Enforcement timing is unchanged: resolve → authorize →
authorized-repo marker → transport (ADR-007 steps 1–4). Unknown-repo
and unauthorized remain indistinguishable (ADR-008).
4. **Data and policy are split across the backend seam.** The
`GitRegistry` trait returns the *record* (`resolve`); the policy
function is alkgit-core code evaluated on it. An embedder with an
existing permission system implements `resolve` and maps its own ACL
into grants at resolve time — no policy hook is needed, and none is
offered in v1 (a custom-policy override would be an additive default
method, a two-way door, added when an embedder needs it).
5. **Vault placement: nothing to place in v1.** With no identity records
and no credentials in metadata (AGENTS.md convention 4), alkvault
holds nothing for alkgit in v1. Vault references enter later only if
a record grows credential-shaped material (the plausible case:
mirroring/remotes records in the consumer half's orbit) — and by then
the rule is already fixed: metadata holds references, never secrets.
6. **`resolve` is async.** Unlike alkcall's accept-loop identity reads
(alknet ADR-035's sync-hot-path constraint — that trait stays sync),
the registry resolve runs once per session/request before the first
protocol byte; an `.await` there costs nothing and keeps the backend
trait family uniform (ADR-010 sub-decision 4). No cache-invalidation
machinery is load-bearing in alkgit v1 — the hot paths (pack
generation, ref listing) hit the object DB, not metadata. Backing
stores that need invalidation strategies own that inside their impl.
## Consequences
- **Positive:** anonymous-public fetch is expressible without bending
`AccessControl` (the clause ADR-007 pinned is now mechanically true);
per-repo grants are administered with the repo, in one store; policy
is one function, tested once, in core; the embedder seam for foreign
permission systems is `resolve`-time mapping (no hook, no trait
growth); no vault placement question remains for v1.
- **Negative:** two authorization data sources exist by design — op ACL
(scopes + ownership, for *managing records*) and repo grants (for *git
access*). Ownership never implies git access: repo creation seeds the
creator's grants explicitly (ADR-012). This split is deliberate
(managing vs accessing are different capabilities) but is a thing a
reader must hold both of.
- **Neutral:** identity-id stability (key rotation) is inherited from
alkcall's `PeerEntry` model, not guaranteed here — alkgit's contract
is only "grants key on the stable logical id."
## References
- OQ-08 (resolved by this ADR)
- ADR-007 (enforcement point, anonymous-public read rule), ADR-008
(registry-resolved ids), ADR-010 (trait family, no-doors rule)
- alkcall ADR-003/025 (IdentityProvider, `PeerEntry` stable logical id),
ADR-017 (the static ACL engine this complements)
- alktty `tty:open` posture (the scope-constant pattern this ADR
deliberately does NOT reuse)
- vision.md §"Primary deployment target", §"Guiding principles" 1–2
- backend.md §"The trait family" (GitRegistry), ADR-012 (record store +
op set)
@@ -0,0 +1,186 @@
# ADR-012: Registry backing, write surface, and the two-op-kind shape
## Status
Accepted (resolves OQ-06, OQ-07; amends ADR-010's feature story)
## Context
With OQ-08's model fixed (ADR-011: grants in repo records, policy in
core), three decisions remained, tangled together:
1. **Backing store for the gix feature's `GitRegistry` impl** (OQ-06,
deferred on "metadata-scale requirements"). The deferral assumed the
backing choice gated something; under the alknet repo/adapter pattern
(traits + in-memory/config default in-crate, persistence adapters
additive in separate crates — alknet ADR-033/035), the default's
choice never depended on scale. Scale only ever gates a *persistence
adapter*, which is additive by construction.
2. **Registry mutation ops** (OQ-07): whether the gix feature ships
repo create/delete/visibility/grant management as reusable alkcall
ops, or leaves all mutation to downstream assembly. The user-session
resolution: CRUD is wanted, with a two-tier shape — a global admin
has the scopes for most things, while a user who creates a repo
administers *their own* repos.
3. **Op-kind classification** (user session, 2026-09-21): git is the
first family payload that naturally wants both alkcall op kinds —
call ops (JSON RPC, schema-validated) for record management, an open
op (channels, `BiStream`) for git sessions. The alkcall registry
already supports this (openable ALPNs are operations — alkcall
ADR-047; `ChannelOpenSpec` + `OperationSpec` in one
`OperationRegistry`); alktty/alktunnels ship only open ops, and the
coming docker crate will ship only call ops (bollard management)
plus consume alk/tty's open op. Git supplying both kinds from one
crate is new for the family but classification-only — no new
registry machinery.
Also carried: the "gix feature" name conflated the gitoxide engine
components with the `gix` facade crate (a client-oriented entry point)
and with the registry store (which needs no gitoxide at all).
## Decision
### 1. Read/write split on the record family
```
pub trait GitRegistry: Send + Sync {
async fn resolve(&self, repo_id: &str) -> Result<RepoRecord, RegistryError>;
}
pub trait GitRegistryStore: GitRegistry {
async fn put_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;
async fn update_repo(&self, record: &RepoRecord) -> Result<(), RegistryError>;
async fn remove_repo(&self, repo_id: &str) -> Result<(), RegistryError>;
}
```
- The write supertrait follows alknet ADR-035's shape (read on the wire
hot path, write on the management path; `ConfigIdentityProvider` does
not implement the write trait there, and the config-backed default
here follows the same posture — its write path is record files, §2).
- Grants ride `RepoRecord` (ADR-011); grant mutation is `update_repo`.
Convenience wrappers (grant/revoke as named methods) are additive
later, two-way.
- The `git/repo/*` ops are thin wrappers over `Arc<dyn
GitRegistryStore>`. An embedder with a DB registry registers the same
ops over their own store.
### 2. Default backing: per-repo record files, in-memory index (`registry-file` feature)
- One small record file per repo (repo id, storage root, visibility,
grants), plus an in-memory index (the resolve path reads the index,
never the file). Config-seeded at boot; op-mutable at runtime.
- Writes are atomic (write-temp + rename) and update the index on
change. Reload-on-change via file watching is the file-scale analog
of alknet's honker NOTIFY story (alknet ADR-035) — adequate at
repo-record scale; if multi-process sharing ever matters, that is a
persistence-adapter concern, not a trait change.
- Storage roots are derived by the store's own naming rules at create
time. Wire names never touch paths (ADR-008 unchanged): creation is
not a wire-input-to-path operation, and resolution returns the
configured root.
- Feature: **`registry-file`**, default-on, **no gitoxide
dependency**. Persistence adapters (SQLite et al.) are future,
separate, additive — built when a deployment with dynamic repo
management at persistence scale exists.
### 3. CRUD ops shipped in-crate, always-on, External with scope+ownership ACL
Ops (names indicative, schema-stable once published):
| Op | Acts on | Gate |
|---|---|---|
| `git/repo/create` | store: mint id, allocate root, seed record | scope `git:repo:create` |
| `git/repo/delete` | store: remove record | scope `git:admin` **or** ownership |
| `git/repo/update` | store: visibility/grants/root | scope `git:admin` **or** ownership |
| `git/repo/get` (list/read) | registry reads | scope `git:admin` **or** ownership |
- **Shape:** ordinary alkcall call ops (`OperationSpec`, JSON,
schema-validated), `Visibility::External`, registered by the
assembler wherever it wants them exposed. They are **not**
feature-gated (thin over the store trait, no gitoxide) and not
Internal: ops an end user must call are wire-callable by definition;
the gitea lesson is served by the ACL (visible-surface =
authorized-surface), not by hiding the ops. OQ-07's old
"Internal ops over an admin listener" framing had the right mechanism
and the wrong axis.
- **Two-tier authorization** uses the checks `AccessControl::check`
already composes: `required_scopes` for the global tier,
`resource_type` + ownership for the self-owned tier (alkcall ADR-011
— repo create mints ownership via `OwnershipStore::record`, so
"creator administers their own repos" is the ownership path, not a
grant table).
- **Ownership never implies git access.** Repo create seeds the
creator's read/write grants in the record explicitly (ADR-011's
two-data-sources rule: op ACL governs *managing records*, repo grants
govern *git access*).
### 4. Feature split (amends ADR-010's single-`gix` feature story)
| Feature | Default | Contents | Needs gitoxide |
|---|---|---|---|
| `gix` | yes | `GitRefs` / `GitPackGen` / `GitPackIngest` impls (the local-disk object-storage engine: odb/pack/ref/object/fsck components) | yes |
| `registry-file` | yes | `GitRegistry`/`GitRegistryStore` default impl + the record store | no |
| neither | — | wire/protocol-only embed (`default-features = false`) | — |
- Two features, not one: independent seams (backend.md's full-family
rationale) — a downstream with a DB registry still wants the gix
engine; a downstream with S3 object storage still wants a simple
registry. Batteries-included unchanged (both default-on).
- **The `gix` facade crate is dropped from the dependency list.** POC-2
drove `gix-odb`/`gix-pack` at the component level; the backend needs
`gix-discover` + `gix-odb::Store::at` from a registry-resolved root,
not facade machinery. Verified at implementation; the facade comes
back only if a concrete need appears.
- `gix-protocol`/`gix-transport` (client-side) remain in the wire
layer's dependency set only pending the consumer-half decision
(`GitSession` may reuse `gix-protocol`'s response parsing or
hand-roll the small client surface; `gix-transport` is almost
certainly droppable). Implementation-time call, recorded, not an
architecture commitment.
### 5. The recorded future trigger for a crate split
If the op set ever grows past repo records — users, orgs, cross-repo
permission management — that is a platform application, and it is
downstream's crate, not alkgit's. The ops stay minimal ("ops wrap a
method on the repo"); the trait family and the one-crate shape are
unpublished, so the split remains a two-way door.
## Consequences
- **Positive:** OQ-06 resolves without its deferred "scale
requirements" ever arriving (the trait is the boundary; the default
is file-scale by design; persistence adapters stay additive); OQ-07
resolves with a shipped, small, reusable op set; the docker-crate
comparison (call ops / open op separation) lands as a classification,
not a structural change; the feature story now matches what the
features actually contain.
- **Negative:** the op set is public API before any downstream exists
(names + schemas become compat surface at first publish — OQ-03's
freeze inventory). Grant-convenience methods deferred means grant
edits go through full-record `update_repo` in v1 (last-writer races
are the assembler's concern; acceptable at record scale, and the
additive path exists).
- **Neutral:** the registry-file store's watch/reload loop is an
implementation detail; its failure mode (watch loss) degrades to
"records update on next process start," never to wrong-path or
wrong-authz behavior (the index is authoritative per record file
content, re-verified on reload).
## References
- OQ-06, OQ-07 (resolved by this ADR), OQ-03 (the publish freeze this
op set enters)
- ADR-010 (superseded sub-parts: single `gix` feature story), ADR-008
(registry-resolved ids), ADR-007 (enforcement order), ADR-011
(grants in records, two-data-sources rule)
- alkcall ADR-047 (openable ALPNs are operations — the classification),
ADR-017 (visibility + handler identity), ADR-011 (ownership model —
repo-create mint-and-own), alkcall `AccessControl` (scope + resource
composition)
- alknet ADR-033/035 (repo/adapter pattern; read-sync/write-async split;
honker NOTIFY — the pattern this ADR's file-watch analog follows)
- alktty (open-op-only posture), alknet-docker (planned call-op-only
posture — the two siblings this ADR's dual-kind shape sits between)
- backend.md (trait family, feature model), doors.md (registration
surface), vision.md §"Registry management ops" (amended by §3)
+15 -5
View File
@@ -22,9 +22,16 @@ and socks5 are payloads doors optionally expose. Downstream consumers
want with the payloads they want.
Auth semantics are the door's auth (http: the door's token mechanism; ssh:
the door's key-based identity). alkgit consumes the resulting alkcall
identity — the identity-extractor seam that ADR-006 needed exists only in
the door, where it belongs.
the door's key-based identity) resolving an alkcall `Identity` (the
identity-extractor seam that ADR-006 needed exists only in the door, where
it belongs). alkgit consumes the resolved identity and the registry
record: the per-repo check is alkgit-core's `authorize` policy function
(ADR-011 — public+read anonymous-first-class, write always
authenticated+granted), run at ADR-007's step-3 position by every door
and by the channels open-op gate. There is no door-specific auth surface
in alkgit and no scope constant — per-repo grants replaced the
`tty:open`-style gate (the single-scope shape cannot express
anonymous-public fetch).
## alkhttp `git` feature (http mounting)
@@ -100,11 +107,14 @@ deployment's docs, not here.
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits on every session |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | doors are family infrastructure |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | `authorize` policy, grants in records |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | op registration surface, two op kinds |
## Open Questions
- **OQ-08**: registry identity space + vault placement (narrowed by
ADR-010; door auth mechanics belong to the door crates).
- None. (OQ-08 resolved by ADR-011 — door auth mechanics stay here,
registry identity model settled; ADR-012 §3 pins the op registration
surface.)
## References
+43 -47
View File
@@ -24,8 +24,8 @@ when their impacts say so; it records how careful the resolution must be.
| OQ | Status | Blocked on / investigation |
|---|---|---|
| OQ-04 | deferred(unclear) | receive-pack walkthrough (capabilities, shallow, thin-pack, CAS timing) + push POC |
| OQ-06 | deferred(scope) | concrete metadata-scale requirements (feeders: OQ-07, OQ-08 outputs) |
| OQ-05 | deferred(scope) | ecosystem need for sha256 |
| OQ-03 | partially resolved | first-publish timing (release decision; the freeze inventory lives in backend.md/transport.md) |
## Theme: composition / crate shapes
@@ -60,9 +60,11 @@ when their impacts say so; it records how careful the resolution must be.
- **Origin**: [overview.md], [transport.md], vision §"ALPN as a service"
- **Status**: **partially resolved** — the shape is settled (ADR-010):
embedding = one crate + backend traits (own storage via
`default-features = false`, or the gix feature) + optional door
features. What remains deferred is the publish-time API freeze itself:
which type/feature names are pinned at first crates.io publish.
`default-features = false`, or the `gix`/`registry-file` features) +
optional door features. What remains deferred is the publish-time API
freeze itself: which type/feature/op names are pinned at first
crates.io publish. ADR-012 added the `git/repo/*` op set (names +
schemas) to the freeze inventory.
- **Door type**: one-way (API freeze is registry-visible to dependents)
- **Priority**: medium
- **Impacts**: blocks the first publish only, not implementation.
@@ -70,7 +72,7 @@ when their impacts say so; it records how careful the resolution must be.
architecture question). The API surface inventory lives in
[backend.md](backend.md) §public API and [transport.md](transport.md)
§public API.
- **Cross-references**: ADR-010, ADR-002, backend.md, transport.md
- **Cross-references**: ADR-010, ADR-002, ADR-012, backend.md, transport.md
## Theme: transport / protocol
@@ -138,55 +140,49 @@ when their impacts say so; it records how careful the resolution must be.
- **Origin**: [overview.md], [doors.md], [backend.md]; originally "identity
sources per front door"
- **Status**: open — narrowed by ADR-010. Door auth mechanics (http token
handling, ssh keys) belong to the door crates; what remains for alkgit
is: the identity model the `GitRegistry` knows (what an identity is,
what identity records exist, whether they live in the same metadata
store as repo records — OQ-06's field list may grow), and where
credential material lives (alkvault; metadata holds references only).
- **Door type**: two-way
- **Priority**: high
- **Impacts**: blocks backend.md's registry trait field list finalizing;
blocks the admin-ops shapes (OQ-07).
- **Resolution path**: one focused session on the registry identity model
+ alkvault placement.
- **Cross-references**: ADR-007, ADR-010, OQ-06, OQ-07, backend.md
§GitRegistry, doors.md
- **Status**: **resolved** — ADR-011 (per-repo authorization): the
identity model is alkcall's resolved `Identity` keyed on its stable
logical id (alkcall ADR-025 property, referenced); alkgit stores no
identity records; grants live in repo records; the public+read /
always-authenticated-write policy is alkgit-core's `authorize`
function (the clause ADR-007 pinned but the static ACL engine could
not express); vault placement resolved as **nothing to place in v1**
(no credential-shaped material in metadata).
- **Resolution**: [decisions/011-per-repo-authorization.md]
- **Cross-references**: ADR-007, ADR-008, ADR-011, ADR-012, OQ-06, OQ-07,
backend.md §GitRegistry, doors.md
## Theme: storage / metadata
### OQ-06: Registry/metadata backing store (gix feature)
- **Origin**: [backend.md] (was storage.md), ADR-008
- **Status**: deferred(scope)
- **Door type**: two-way (backing choice is swappable behind the
`GitRegistry` trait)
- **Priority**: high for the gix feature's default story, but choice
deferrable because the trait boundary is what matters
- **Impacts**: blocks backend.md's gix-feature registry impl finalizing;
does NOT block the wire layer (it codes against the trait).
- **Blocked on**: concrete metadata-scale requirements (how many repos,
what metadata fields beyond id/root/visibility/ACL scope, whether
identity records join — OQ-08's output, whether hub integration lands
in v1). A config-file or embedded-store decision without those inputs
would be a guess. Tracker task:
`tasks/architecture/oq-06-metadata-backing.md`.
- **Cross-references**: ADR-008, ADR-010, backend.md §"The trait family" (GitRegistry), OQ-08
(whose identity-records question may extend this store's field list)
- **Status**: **resolved** — ADR-012 (registry backing + write surface):
the alknet repo/adapter pattern applied — `GitRegistry` read trait +
`GitRegistryStore` write supertrait; default backing is the
`registry-file` feature (per-repo record files + in-memory index,
config-seeded, op-mutable, atomic writes — no gitoxide); persistence
adapters (SQLite et al.) are future, separate, additive, gated by a
real deployment need — the deferred "scale requirements" never gated
the default, only that future adapter.
- **Resolution**: [decisions/012-registry-backing-and-ops.md] §1–2, §4
- **Cross-references**: ADR-008, ADR-010 (feature story amended), ADR-011,
backend.md §feature model
### OQ-07: Admin API operation set (v1 scope)
- **Origin**: [overview.md], alk-stack.md §"The gitea lesson"
- **Status**: open — rescope note (ADR-010): there is no alkgit binary, so
there is no alkgit-owned admin surface. The question narrows to whether
the gix feature's `GitRegistry` implementation ships with any management
ops (repo create/delete, visibility, ACL grant) as reusable alkcall ops,
or whether registry mutation is entirely downstream assembly work. If
shipped, they are `Visibility::Internal` alkcall ops over the
assembler's admin listener — never the git traffic surface.
- **Priority**: medium
- **Impacts**: blocks the gix feature's registry impl scope; OQ-08's
identity model determines the op shapes.
- **Resolution path**: decide alongside OQ-08 (one session can settle
both).
- **Cross-references**: ADR-007, ADR-010, OQ-08, backend.md §"The trait family" (GitRegistry)
- **Status**: **resolved** — ADR-012 (§3, §5): the crate ships a minimal
CRUD op set (`git/repo/{create,delete,update,get}`) as thin call ops
over `GitRegistryStore`, `Visibility::External`, always-on, gated by
scopes (global tier, e.g. `git:admin` / `git:repo:create`) + ownership
(self-owned tier; create mints ownership per alkcall ADR-011 and
seeds the creator's grants — ownership never implies git access,
ADR-011). The old "Internal ops over an admin listener" framing is
superseded (right mechanism, wrong axis — visible-surface =
authorized-surface is served by the ACL). Ops stay record-scoped;
anything beyond repo records (users, orgs) is a downstream platform
crate (the recorded split trigger, ADR-012 §5).
- **Resolution**: [decisions/012-registry-backing-and-ops.md] §3, §5
- **Cross-references**: ADR-007, ADR-011, ADR-012, alkcall ADR-017/011,
backend.md §"The two op kinds"
+21 -8
View File
@@ -32,10 +32,13 @@ Single crate `alkgit`:
| Producer | `GitAdapter` (`alk/git` ALPN via alkcall `ProtocolHandler`), channels `register_openable` (repo id in open-op params — the negotiation + ACL point) | POC-1 verbatim |
| Consumer | `GitSession` typed client (`connect_direct`, `open_via_channels`) — the replication/mirroring primitive for alknet | new, small (TtySession analog) |
| Substrate | duplex session + stateless request/response layer (ADR-005); wire framing, V2 state machines (ADR-003) | POC-1, POC-3 |
| Backends | `GitRegistry`, `GitRefs`, `GitPackGen`, `GitPackIngest` traits; gix impl behind the default-on `gix` feature | POC-2 (gix impl) |
| Backends | `GitRegistry` (+ write supertrait), `GitRefs`, `GitPackGen`, `GitPackIngest` traits; impls behind the default-on `gix` (engine) and `registry-file` (records) features | POC-2 (gix impl) |
| Management ops | `git/repo/*` call ops over `GitRegistryStore` (ADR-012 §3) — the JSON half alongside the `alk/git` open op (first dual-kind payload; ADR-012 §5) | thin over the store trait |
Feature model: `default-features = false` gives the wire/protocol layer
without gix (wasm-clean as a side effect, not a goal); the `sha256`
Feature model: `gix` (engine impls) and `registry-file` (record store +
ops) are independent default-on seams (ADR-012 §4);
`default-features = false` gives the wire/protocol layer without either
(wasm-clean as a side effect, not a goal); the `sha256`
passthrough and (eventually, in alkhttp) the `git` door feature ride the
same pattern. Doors live in the door crates — see [doors.md](doors.md).
@@ -51,7 +54,9 @@ same pattern. Doors live in the door crates — see [doors.md](doors.md).
4. **Registry-resolved repo identity** — wire names are ids, never paths
(ADR-008).
5. **No secret material on the wire or at rest outside alkvault** —
metadata holds vault references; no env-var credential reads.
metadata holds vault references; no env-var credential reads. (In v1
alkgit's metadata holds no credential-shaped material at all, so
nothing is vault-placed — ADR-011 §5.)
6. **No shelling out to `git`** — pure Rust on gix primitives.
7. **Bounded resources** — every session carries `Limits` (ADR-009).
8. **Honest capability advertisement** — advertise exactly what we serve
@@ -82,6 +87,8 @@ designed but not yet exercised (OQ-04).
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry IDs |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | every session carries limits |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | single crate, producer/consumer halves, no doors/binary |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in repo records, policy in core, vault-nil |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | read/write split, file default, CRUD ops, feature split |
## Open Questions
@@ -89,7 +96,13 @@ Key questions tracked in [open-questions.md](open-questions.md):
- **OQ-04**: receive-pack (push) validation gap (high — the
always-authenticated half of the wire surface).
- **OQ-06**: registry backing store for the gix feature (deferred on
scale requirements).
- **OQ-08**: registry identity space + vault placement (narrowed by
ADR-010).
- **OQ-02**: V2 multi-round negotiation (medium — efficiency, not
correctness).
- **OQ-03**: publish-time API freeze inventory (the `git/repo/*` op
set enters it; ADR-012).
- **OQ-05**: sha256 policy (deferred(scope), low).
Resolved this cycle: OQ-08 (ADR-011 — per-repo authorization, grants in
records, vault-nil), OQ-06 (ADR-012 — `registry-file` default,
persistence adapters additive), OQ-07 (ADR-012 — CRUD ops shipped
External with scope+ownership ACL).
+16 -12
View File
@@ -1,37 +1,41 @@
---
id: architecture/oq-06-metadata-backing
name: OQ-06 unblock — metadata-scale requirements arrive
status: pending
status: done
depends_on: []
scope: narrow
risk: trivial
impact: component
level: research
tags: [external-trigger, deferred-oq]
tags: [external-trigger, deferred-oq, resolved-early]
---
## Description
Tracker for OQ-06 (`deferred(scope)`): registry/metadata backing store.
This task is not actionable work — it tracks whether the blocking
condition has arrived: concrete metadata-scale requirements (repo count,
metadata fields beyond id/root/visibility/ACL scope, alkcall-hub
integration scope). When they exist, mark completed; OQ-06 transitions to
`open`.
**Resolved early (2026-09-21, ADR-012)** before the blocking condition
arrived: the deferral's premise was wrong — the deferred "metadata-scale
requirements" only ever gated a *persistence adapter* (future, separate,
additive), never the default backing. The default (`registry-file`
feature: per-repo record files + in-memory index, config-seeded,
op-mutable) is file-scale by design under the alknet repo/adapter
pattern. Marking done closes the tracker.
## Work
None by default. On trigger: set OQ-06 to `open` and schedule the backing
decision session.
None. OQ-06 is resolved by
`docs/architecture/decisions/012-registry-backing-and-ops.md` (§1–2, §4).
## Verification
- OQ-06 status matches `docs/architecture/open-questions.md`.
- OQ-06 status matches `docs/architecture/open-questions.md` (resolved).
## Out of scope
- Choosing the backing store.
- Building a persistence adapter (gated on a real deployment need —
additive, no tracker needed until then).
## Summary
> Filled on completion.
Resolved early via ADR-012: the repo/adapter pattern made the default
independent of scale requirements; the deferral dissolved.