Files
alkgit/docs/architecture/decisions/015-manage-grant-and-op-gate.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

168 lines
8.8 KiB
Markdown

# 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)