Files
alkgit/docs/architecture/decisions/012-registry-backing-and-ops.md
T
glm-5.3-flash addc874667 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.
2026-09-21 16:26:59 +00:00

9.4 KiB

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)