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
@@ -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)
|
||||
Reference in new issue
Block a user