Files
alkgit/docs/architecture/decisions/011-per-repo-authorization.md
T
glm-5.3-flash c4b9c53674 docs(architecture): ADR-015 — manage grant tier + repo-op gate, OQ-16 identity namespace
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.
2026-09-26 11:50:25 +00:00

7.2 KiB
Raw Blame History

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)