# ADR-015: The `manage` grant tier and the repo-op gate ## Status Accepted (resolves review 001 A-1; amends ADR-011's action set and ADR-012 §3's gate table) ## Context Review 001 (A-1, critical) established that ADR-012 §3's two-tier op gate — "scope `git:admin` **or** ownership" — is not expressible in alkcall's `AccessControl`, which composes restrictions as AND (`required_scopes` fail hard before the ownership block ever runs; spec.rs:94-171). The review's recommended resolution was a handler-side check: admin scope OR `OwnershipProvider::owns(...)`. That resolution is correct mechanically but wrong in granularity. "Owner or admin" is the ceiling of what ownership can express, and it is the wrong ceiling: the moment a second maintainer, a bot account, or a collaborator needs to administer a repo they did not create, the model has no answer short of handing out global `git:admin` (too broad) or minting alkcall ownership records for them (wrong home — ownership is spawn-tracking, alkcall ADR-011's mint-and-own records *who created a resource*, not *who may administer it*). The coming platform app (issues, PRs, per-repo admins, teams) needs per-repo administration to be grantable, and the eventual distributed/replicator deployment needs the same grant data to be replicable as flat state. The mechanism to express this already exists in the corpus: ADR-011's `authorize` policy function evaluated on the `RepoRecord` — which today knows only `read|write`. The gap is not a missing mechanism; it is a missing action. ## Decision 1. **The grant action set gains `manage`.** ADR-011's `grants: identity_id → {read, write}` becomes `grants: identity_id → {read, write, manage}` — a flat set, no hierarchy: a grant entry is the set of actions one identity holds on one repo, and `manage` includes read and write (a manager of the repo can access it; `authorize` treats `manage ⊇ write ⊇ read` for the actions below it). 2. **`authorize` is the single per-repo policy function for both git access and repo administration.** Its action domain extends: `authorize(record, identity: Option<&Identity>, action: read|write|manage) -> bool`. Semantics per action: - `read` — unchanged (ADR-011 §3: anonymous-first-class on Public). - `write` — unchanged (identity + write-or-manage grant). - `manage` — identity required, plus a `manage` grant. No anonymous and no owner-bypass path: the owner administers because repo creation *seeds their `manage` grant* (§3), not because identity has any special shape. - Grant-monotonicity: a `manage` grant satisfies `manage`, `write`, and `read` checks; a `write` grant satisfies `write` and `read`; `read` satisfies `read` only. 3. **The repo-op gate is: global admin scope OR `manage` grant.** ADR-012 §3's gate table becomes: | Op | Gate | |---|---| | `git/repo/create` | scope `git:repo:create` (static registry gate, unchanged) | | `git/repo/delete` | scope `git:admin` **or** `manage` grant (handler-evaluated) | | `git/repo/update` | scope `git:admin` **or** `manage` grant (handler-evaluated) | | `git/repo/get` (list/read) | scope `git:admin` **or** `manage` grant (handler-evaluated) | Mechanically this is review 001's option (a) with the OR-term generalized from ownership to the `manage` grant: `create` keeps its pure-scope `AccessControl` (`required_scopes: ["git:repo:create"]` — the static engine serves it, AND-composition is fine with one restriction); `delete`/`update`/`get` carry an empty `AccessControl::default()` and the handler evaluates the two-tier rule as one tested function in alkgit core (admin scope in `Identity.scopes` OR `authorize(record, identity, manage)`), denying with the generic FORBIDDEN. The ADR-008 rule is preserved at the op layer: unknown-repo and not-authorized collapse to the same denial — no existence oracle. 4. **Repo creation seeds the creator's `manage` grant.** `git/repo/create` mints alkcall ownership (alkcall ADR-011, `OwnershipStore::record` — unchanged; ownership remains the platform's spawn-tracking record) **and** writes the creator's grants as the full set `{read, write, manage}` in the `RepoRecord`. "Creator administers their own repos" becomes a grant, administered with the repo: the creator can `update_repo` to promote a collaborator to `manage` or demote themselves, using the op that already exists (`update_repo`), with no new op and no trait change. 5. **alkcall's `AccessControl` is not extended.** The scope-OR-grant composition lives handler-side, which is exactly where ADR-011 §4 put policy ("data and policy are split across the backend seam" — data in the backend, policy in core). This sidesteps `AccessControl`'s AND-composition rather than changing it: an upstream family-crate change (review 001's option (c)) is not required, and the two-tier rule stays unit-testable in-crate instead of hiding in an assembly-supplied `OwnershipProvider` overload (option (b)'s rejected shape). 6. **Ownership no longer implies administration; grants do.** This supersedes the ADR-011/ADR-012 sentence "ownership never implies git access" — the two-data-sources rule is *retired*, not extended: repo records now carry the grant state both access tiers evaluate. The op ACL (scopes) and the record grants remain distinct *sources* — scopes govern platform-level capability, grants govern per-repo capability — but the record is now the single per-repo surface. The `manage` grant is the app's promotion path today (the platform app compiles roles/teams into flat grants and writes them with `update_repo`); nothing about roles, teams, or orgs enters the protocol crate — ADR-012 §5's split trigger stands: that is downstream's crate, compiling down to these flat grants. 7. **Grant keys are opaque strings; the id namespace is deliberately open.** Grants key on the resolved identity's stable logical id (ADR-011 §1), which is alkcall-cluster-local today. The id format is deliberately unspecified at the alkgit layer — an opaque string — so a downstream identity space (e.g. a `user/repo`-shaped, or contract-derived, globally-comparable id) can be introduced by the assembly/identity-provider seam without an alkgit change. Two identities colliding on one string is the embedder's identity guarantee to make, not alkgit's. 8. **Flat grants are the replication substrate.** Grants being flat sets keyed on opaque id strings, carried in the record, and mutated only through the store's `update_repo` is precisely the shape a future replicator/hub needs: an external authority (the planned contract watcher) can hold and publish grant state, and the local store consumes it as records. Nothing here builds replication — but the data model is chosen so that the external-authority case is `update_repo` calls from a sync writer, not a schema change. ## Consequences - **Positive:** A-1 resolves without upstream changes; per-repo administration becomes grantable (the collaborator/admin case is now expressible); one policy function serves git access and repo management; the eventual app's roles/teams compile to flat grants with no protocol-crate growth; the record is the single per-repo authz surface a replicator would mirror. - **Negative:** the grant set widens from two to three actions — a wire-schema decision that enters OQ-03's freeze inventory (N-3's schema pinning must land *after* this, so the freeze captures the three-action shape once); the two-data-sources consequence recorded in ADR-011 is partially retired, so readers of that ADR must follow the amendment (recorded there). - **Neutral:** ownership records and grants coexist with distinct purposes (platform spawn-tracking vs per-repo capability); nothing in v1 reads ownership for authorization except the create-op's mint, so the mint could even become optional in a downstream that doesn't want spawn-tracking — kept in v1 because `git/repo/*` ops are family-shared and the record is cheap. ## References - Review 001 A-1 (the trigger; this ADR takes its option (a) shape with the OR-term generalized), N-3 (the schema-pinning rider this ADR must precede) - ADR-011 (the `authorize` policy function and grant storage this ADR amends), ADR-012 §3 (the gate table this ADR replaces), ADR-012 §5 (the platform-crate split trigger this ADR preserves) - ADR-007 (enforcement order), ADR-008 (unknown ≡ unauthorized at the op layer), ADR-010 (trait family, no-doors) - alkcall ADR-011 (ownership mint-and-own — retained as spawn-tracking), `AccessControl` (spec.rs:94 — the AND-composition this ADR routes around, not through) - vision.md §"Primary deployment target" (the grant set remains simple enough to reason about completely) - backend.md §"The two op kinds" (op-gate restatement)