Resolves review 001 finding A-1 (critical): ADR-012 §3's "scope
git:admin OR ownership" gate is not expressible in alkcall's
AccessControl (AND-composition). Resolution is the review's option (a)
shape with the OR-term generalized: the per-repo grant action set gains
manage, authorize(record, identity, read|write|manage) becomes the
single policy function for git access and repo administration, and the
delete/update/get gate is admin scope OR manage grant (handler-side,
generic FORBIDDEN, unknown-repo = unauthorized per ADR-008). Repo
create seeds the creator's {read, write, manage} grants —
administration is grantable, so collaborators/bots/app-compiled roles
work without global scopes. Ownership stays as alkcall spawn-tracking
(mint at create unchanged); "ownership never implies git access" is
superseded.
- ADR-015 (new): manage grant tier, op gate, flat-grants-as-replication-
substrate, opaque grant-key rule
- ADR-011: action set + policy domain amended, references updated
- ADR-012 §3: gate table replaced, two-tier paragraph superseded
- backend.md/doors.md/overview.md: gate + grant restatements, ADR tables
- OQ-16 (new, deferred(scope)): grant-key identity namespace —
globally-comparable ids for cross-assembly/replicator grant state;
tracker task tasks/architecture/oq-16-grant-identity-namespace.md
- review 001: A-1 marked resolved (ADR-015)
- vision.md: supersession notes (Internal-ops framing, v1 grant set)
Verification: cargo test, clippy -D warnings, fmt --check, doc --no-deps
all clean.
196 lines
10 KiB
Markdown
196 lines
10 KiB
Markdown
# 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; §3 gate
|
|
table replaced by ADR-015)
|
|
|
|
## 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+grant ACL
|
|
|
|
*(Gate table amended by ADR-015 — the original "scope OR ownership"
|
|
formulation was not expressible in alkcall's `AccessControl` (review 001
|
|
A-1); the two-tier rule below is the ADR-015 shape.)*
|
|
|
|
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` (static registry gate) |
|
|
| `git/repo/delete` | store: remove record | scope `git:admin` **or** `manage` grant (ADR-015) |
|
|
| `git/repo/update` | store: visibility/grants/root | scope `git:admin` **or** `manage` grant (ADR-015) |
|
|
| `git/repo/get` (list/read) | registry reads | scope `git:admin` **or** `manage` grant (ADR-015) |
|
|
|
|
- **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** *(superseded by ADR-015's shape: `create`
|
|
keeps a pure-scope `AccessControl`; `delete`/`update`/`get` carry an
|
|
empty `AccessControl` and the handler evaluates "admin scope OR
|
|
`authorize(record, identity, manage)`" as one tested function in
|
|
alkgit core, with the generic FORBIDDEN denial and the unknown-repo ≡
|
|
unauthorized rule (ADR-008) at the op layer. Repo create mints
|
|
ownership via `OwnershipStore::record` *and seeds the creator's
|
|
`{read, write, manage}` grants* — "creator administers their own
|
|
repos" is now the `manage` grant path.)*
|
|
- **Grants govern access; scopes govern platform capability.** Repo
|
|
create seeds the creator's `{read, write, manage}` grants in the
|
|
record explicitly (ADR-015: administration is the `manage` grant —
|
|
the record is the single per-repo authz surface; op scopes and record
|
|
grants remain distinct *sources*, platform capability vs per-repo
|
|
capability).
|
|
|
|
### 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) |