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

8.8 KiB

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)