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.
7.2 KiB
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_idstability 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
AccessControlwith restrictions fails closed onidentity: 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
-
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-025peer_idproperty, 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. -
Grants live in repo records, keyed on the logical identity id.
RepoRecordcarriesvisibility(Public/Private) andgrants: identity_id → {read, write}(amended by ADR-015: the action set gainsmanage—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 intoIdentity.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.) -
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;managerequires identity plus amanagegrant, and the grant check is monotonic — amanagegrant satisfieswriteandreadchecks. The repo-op gate "admin scope OR manage grant" is defined there.)readon a Public repo → allowed for anyone, includingNone(anonymous-first-class; ADR-007's operative clause).readon 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).
-
Data and policy are split across the backend seam. The
GitRegistrytrait returns the record (resolve); the policy function is alkgit-core code evaluated on it. An embedder with an existing permission system implementsresolveand 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). -
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.
-
resolveis 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.awaitthere 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 isresolve-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
managegrant — the record is the single per-repo authz surface; "ownership never implies git access" is superseded — creation seedsmanage, which is a grant, not an ownership-derived bypass, and grants are promotable viaupdate_repo.) - Neutral: identity-id stability (key rotation) is inherited from
alkcall's
PeerEntrymodel, 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
managegrant tier — amends this ADR's action set and policy domain) - alkcall ADR-003/025 (IdentityProvider,
PeerEntrystable logical id), ADR-017 (the static ACL engine this complements) - alktty
tty:openposture (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)