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.
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
-
The grant action set gains
manage. ADR-011'sgrants: identity_id → {read, write}becomesgrants: identity_id → {read, write, manage}— a flat set, no hierarchy: a grant entry is the set of actions one identity holds on one repo, andmanageincludes read and write (a manager of the repo can access it;authorizetreatsmanage ⊇ write ⊇ readfor the actions below it). -
authorizeis 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 amanagegrant. No anonymous and no owner-bypass path: the owner administers because repo creation seeds theirmanagegrant (§3), not because identity has any special shape.- Grant-monotonicity: a
managegrant satisfiesmanage,write, andreadchecks; awritegrant satisfieswriteandread;readsatisfiesreadonly.
-
The repo-op gate is: global admin scope OR
managegrant. ADR-012 §3's gate table becomes:Op Gate git/repo/createscope git:repo:create(static registry gate, unchanged)git/repo/deletescope git:adminormanagegrant (handler-evaluated)git/repo/updatescope git:adminormanagegrant (handler-evaluated)git/repo/get(list/read)scope git:adminormanagegrant (handler-evaluated)Mechanically this is review 001's option (a) with the OR-term generalized from ownership to the
managegrant:createkeeps its pure-scopeAccessControl(required_scopes: ["git:repo:create"]— the static engine serves it, AND-composition is fine with one restriction);delete/update/getcarry an emptyAccessControl::default()and the handler evaluates the two-tier rule as one tested function in alkgit core (admin scope inIdentity.scopesORauthorize(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. -
Repo creation seeds the creator's
managegrant.git/repo/createmints 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 theRepoRecord. "Creator administers their own repos" becomes a grant, administered with the repo: the creator canupdate_repoto promote a collaborator tomanageor demote themselves, using the op that already exists (update_repo), with no new op and no trait change. -
alkcall's
AccessControlis 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 sidestepsAccessControl'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-suppliedOwnershipProvideroverload (option (b)'s rejected shape). -
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
managegrant is the app's promotion path today (the platform app compiles roles/teams into flat grants and writes them withupdate_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. -
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. -
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_repois 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 isupdate_repocalls 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
authorizepolicy 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)