diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 9b51ab4..203132c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index b1584ac..cb127e4 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -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` 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`, + `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) \ No newline at end of file +- alknet ADR-033/035 (the repo/adapter + read/write-split pattern) +- ADR-010 (the structural decision), ADR-011/012 (the seam this doc + specifies) \ No newline at end of file diff --git a/docs/architecture/decisions/011-per-repo-authorization.md b/docs/architecture/decisions/011-per-repo-authorization.md new file mode 100644 index 0000000..8b5c714 --- /dev/null +++ b/docs/architecture/decisions/011-per-repo-authorization.md @@ -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) \ No newline at end of file diff --git a/docs/architecture/decisions/012-registry-backing-and-ops.md b/docs/architecture/decisions/012-registry-backing-and-ops.md new file mode 100644 index 0000000..4293c48 --- /dev/null +++ b/docs/architecture/decisions/012-registry-backing-and-ops.md @@ -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; +} + +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`. 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) \ No newline at end of file diff --git a/docs/architecture/doors.md b/docs/architecture/doors.md index e8ed7ff..8e5d951 100644 --- a/docs/architecture/doors.md +++ b/docs/architecture/doors.md @@ -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 diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 5438ac4..1450148 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -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) \ No newline at end of file +- **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" \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index a2d5758..585b2b1 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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). \ No newline at end of file +- **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). \ No newline at end of file diff --git a/tasks/architecture/oq-06-metadata-backing.md b/tasks/architecture/oq-06-metadata-backing.md index 822f235..1779c8d 100644 --- a/tasks/architecture/oq-06-metadata-backing.md +++ b/tasks/architecture/oq-06-metadata-backing.md @@ -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. \ No newline at end of file +Resolved early via ADR-012: the repo/adapter pattern made the default +independent of scale requirements; the deferral dissolved. \ No newline at end of file