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:
1 parent
86bf5a0cf0
commit
addc874667
8 files changed
+492
-104
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user