# ADR-011: Per-repo authorization — grants live in repo records ## Status Accepted (resolves OQ-08; grant action set + policy domain amended by ADR-015) ## 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}` *(amended by ADR-015: the action set gains `manage` — `grants: identity_id → {read, write, manage}`, a flat set with no hierarchy; `manage ⊇ write ⊇ read`)*. 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). *(Also amended by ADR-015: the grant-key id format is deliberately an opaque string — the identity namespace is the embedder's to define.)* 3. **Authorization is a policy function in alkgit core**, evaluated against the record: ``` authorize(record, identity: Option<&Identity>, action: read|write) -> bool ``` *(Amended by ADR-015: the action domain extends to `read|write|manage`; `manage` requires identity plus a `manage` grant, and the grant check is monotonic — a `manage` grant satisfies `write` and `read` checks. The repo-op gate "admin scope OR manage grant" is defined there.)* - `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, for platform-level capability) and repo grants (for per-repo capability). This split is deliberate but is a thing a reader must hold both of. *(Amended by ADR-015: repo administration is the `manage` grant — the record is the single per-repo authz surface; "ownership never implies git access" is superseded — creation seeds `manage`, which is a grant, not an ownership-derived bypass, and grants are promotable via `update_repo`.)* - **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), OQ-16 (the grant-key identity-namespace question this ADR's opaque-string rule defers) - ADR-007 (enforcement point, anonymous-public read rule), ADR-008 (registry-resolved ids), ADR-010 (trait family, no-doors rule) - ADR-015 (the `manage` grant tier — amends this ADR's action set and policy domain) - 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)