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.
This commit is contained in:
glm-5.3-flash committed 2026-09-26 11:50:25 +00:00
1 parent d478361a15
commit c4b9c53674
11 files changed
+381 -64

No files matched your search

+9 -3
View File
@@ -20,8 +20,11 @@ against real git 2.43. Previous cycles settled the auth/backend theme
real-client captures: the receive-pack push state machine (ADR-013, real-client captures: the receive-pack push state machine (ADR-013,
OQ-04) and the V2 multi-round negotiation ack loop (ADR-014, OQ-02). All OQ-04) and the V2 multi-round negotiation ack loop (ADR-014, OQ-02). All
wire-layer design is now capture-grounded; the remaining open questions wire-layer design is now capture-grounded; the remaining open questions
are the publish-freeze timing (OQ-03, a release decision) and sha256 are the publish-freeze timing (OQ-03, a release decision), sha256
policy (OQ-05, deferred on ecosystem need). policy (OQ-05, deferred on ecosystem need), and the grant-key identity
namespace (OQ-16, deferred on the first cross-assembly deployment —
blocks nothing in v1). ADR-015 resolved
review 001's A-1 (the repo-op gate) with the `manage` grant tier.
## Architecture Documents ## Architecture Documents
@@ -51,13 +54,16 @@ policy (OQ-05, deferred on ecosystem need).
| [012](decisions/012-registry-backing-and-ops.md) | Registry backing, write surface, CRUD ops, feature split | Accepted | | [012](decisions/012-registry-backing-and-ops.md) | Registry backing, write surface, CRUD ops, feature split | Accepted |
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack state machine (V0-framed push, thin-pack, unpack-first CAS) | Accepted | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack state machine (V0-framed push, thin-pack, unpack-first CAS) | Accepted |
| [014](decisions/014-v2-negotiation-ack-loop.md) | V2 negotiation ack loop (no `ready`, wait-for-done stays) | Accepted | | [014](decisions/014-v2-negotiation-ack-loop.md) | V2 negotiation ack loop (no `ready`, wait-for-done stays) | Accepted |
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant tier + repo-op gate (admin scope OR manage grant) | Accepted |
## Open Questions ## Open Questions
All unresolved questions are tracked in [open-questions.md](open-questions.md) All unresolved questions are tracked in [open-questions.md](open-questions.md)
with stable OQ-IDs, priorities, and cross-references. Remaining: OQ-03 with stable OQ-IDs, priorities, and cross-references. Remaining: OQ-03
(publish freeze inventory — partially resolved, blocked on first-publish (publish freeze inventory — partially resolved, blocked on first-publish
timing) and OQ-05 (sha256, deferred on ecosystem need). The wire-layer timing), OQ-05 (sha256, deferred on ecosystem need), and OQ-16 (grant-key
identity namespace, deferred on the first cross-assembly record-sharing
deployment). The wire-layer
questions (OQ-02, OQ-04) resolved this cycle with ADR-014/ADR-013. questions (OQ-02, OQ-04) resolved this cycle with ADR-014/ADR-013.
## Document Lifecycle ## Document Lifecycle
+18 -10
View File
@@ -20,7 +20,8 @@ Five traits, kept small and orthogonal — the protocol crate never sees
gix types: gix types:
1. **`GitRegistry`** — repo id → `RepoRecord` (storage root, visibility, 1. **`GitRegistry`** — repo id → `RepoRecord` (storage root, visibility,
grants keyed on the stable logical identity id — ADR-011). Async grants keyed on the stable logical identity id, action set
`{read, write, manage}` — ADR-011, ADR-015). Async
`resolve`; the authoritative mapping (ADR-008); resolution failure is `resolve`; the authoritative mapping (ADR-008); resolution failure is
indistinguishable from authorization failure (ADR-007). Authorization indistinguishable from authorization failure (ADR-007). Authorization
is alkgit-core's policy function evaluated on the record (data in the is alkgit-core's policy function evaluated on the record (data in the
@@ -30,7 +31,9 @@ gix types:
(`put_repo`/`update_repo`/`remove_repo`; alknet ADR-035's read/write (`put_repo`/`update_repo`/`remove_repo`; alknet ADR-035's read/write
split shape). The `git/repo/*` ops wrap it (ADR-012 §3). Grants ride split shape). The `git/repo/*` ops wrap it (ADR-012 §3). Grants ride
the record, so grant mutation is `update_repo` (convenience wrappers the record, so grant mutation is `update_repo` (convenience wrappers
additive later, two-way). additive later, two-way) — which is also the app's promotion path
(roles/teams compile to flat grants upstream; ADR-015 §6) and the
shape a future replication sync would write through (ADR-015 §8).
3. **`GitRefs`** — listing for advertisement (refs + peeled tags + symref 3. **`GitRefs`** — listing for advertisement (refs + peeled tags + symref
targets, the ls-refs response data) and ref transactions (CAS apply targets, the ls-refs response data) and ref transactions (CAS apply
for receive-pack, name validation per git ref rules + reserved- for receive-pack, name validation per git ref rules + reserved-
@@ -123,12 +126,16 @@ planned: call ops only):
yields the duplex git session. yields the duplex git session.
- **Call ops** (`git/repo/{create,delete,update,get}`) — the JSON half: - **Call ops** (`git/repo/{create,delete,update,get}`) — the JSON half:
thin `OperationSpec`+handler pairs over `Arc<dyn GitRegistryStore>`, thin `OperationSpec`+handler pairs over `Arc<dyn GitRegistryStore>`,
`Visibility::External`, always-on (no gitoxide), gated by `Visibility::External`, always-on (no gitoxide). `create` is gated by
`required_scopes` (global tier) + ownership (self-owned tier, alkcall the static registry check (`required_scopes: ["git:repo:create"]`);
ADR-011 mint-and-own at create). Registered by the assembler wherever `delete`/`update`/`get` carry an empty `AccessControl` and the handler
it wants them exposed. Managing records (op ACL) and git access (repo evaluates the ADR-015 gate — scope `git:admin` OR the record's
grants) are different capabilities — ownership never implies git `manage` grant, via the `authorize` policy function, generic FORBIDDEN
access (ADR-011, ADR-012 §3). denial, unknown-repo ≡ unauthorized (ADR-008). Registered by the
assembler wherever it wants them exposed. Managing records (op scopes)
and git access (repo grants) are distinct sources — but the record is
the single per-repo authz surface, and repo creation seeds the
creator's `{read, write, manage}` grants (ADR-015).
## Design Decisions ## Design Decisions
@@ -139,8 +146,9 @@ planned: call ops only):
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids | | [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits flow into gen/ingest | | [009](decisions/009-bounded-resources-budget.md) | Budgets | limits flow into gen/ingest |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, impls behind features | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | traits in-crate, impls behind features |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in records, policy in core | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in records, policy in core (amended: ADR-015) |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split | | [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split (gate amended: ADR-015) |
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-scope-OR-manage gate, create seeds manage |
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | thin-pack ingestion, transaction CAS, report framing |
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` |
@@ -1,7 +1,8 @@
# ADR-011: Per-repo authorization — grants live in repo records # ADR-011: Per-repo authorization — grants live in repo records
## Status ## Status
Accepted (resolves OQ-08) Accepted (resolves OQ-08; grant action set + policy domain amended by
ADR-015)
## Context ## Context
@@ -47,11 +48,16 @@ metadata must hold.
2. **Grants live in repo records, keyed on the logical identity id.** 2. **Grants live in repo records, keyed on the logical identity id.**
`RepoRecord` carries `visibility` (Public/Private) and `RepoRecord` carries `visibility` (Public/Private) and
`grants: identity_id → {read, write}`. Per-repo grants are repo `grants: identity_id → {read, write}` *(amended by ADR-015: the
lifecycle data — created with the repo, administered with it — so action set gains `manage` — `grants: identity_id → {read, write,
they are stored with the record, not folded into `Identity.resources` manage}`, a flat set with no hierarchy; `manage ⊇ write ⊇ read`)*.
(which is static identity-provider data resolved per credential path; Per-repo grants are repo lifecycle data — created with the repo,
per-repo entries would bend that model). 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 3. **Authorization is a policy function in alkgit core**, evaluated
against the record: against the record:
@@ -60,6 +66,12 @@ metadata must hold.
authorize(record, identity: Option<&Identity>, action: read|write) -> bool 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` - `read` on a Public repo → allowed for anyone, including `None`
(anonymous-first-class; ADR-007's operative clause). (anonymous-first-class; ADR-007's operative clause).
- `read` on a Private repo → identity required with a read grant. - `read` on a Private repo → identity required with a read grant.
@@ -104,20 +116,25 @@ metadata must hold.
permission systems is `resolve`-time mapping (no hook, no trait permission systems is `resolve`-time mapping (no hook, no trait
growth); no vault placement question remains for v1. growth); no vault placement question remains for v1.
- **Negative:** two authorization data sources exist by design — op ACL - **Negative:** two authorization data sources exist by design — op ACL
(scopes + ownership, for *managing records*) and repo grants (for *git (scopes, for platform-level capability) and repo grants (for per-repo
access*). Ownership never implies git access: repo creation seeds the capability). This split is deliberate but is a thing a reader must
creator's grants explicitly (ADR-012). This split is deliberate hold both of. *(Amended by ADR-015: repo administration is the
(managing vs accessing are different capabilities) but is a thing a `manage` grant — the record is the single per-repo authz surface;
reader must hold both of. "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 - **Neutral:** identity-id stability (key rotation) is inherited from
alkcall's `PeerEntry` model, not guaranteed here — alkgit's contract alkcall's `PeerEntry` model, not guaranteed here — alkgit's contract
is only "grants key on the stable logical id." is only "grants key on the stable logical id."
## References ## References
- OQ-08 (resolved by this ADR) - 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 - ADR-007 (enforcement point, anonymous-public read rule), ADR-008
(registry-resolved ids), ADR-010 (trait family, no-doors rule) (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), - alkcall ADR-003/025 (IdentityProvider, `PeerEntry` stable logical id),
ADR-017 (the static ACL engine this complements) ADR-017 (the static ACL engine this complements)
- alktty `tty:open` posture (the scope-constant pattern this ADR - alktty `tty:open` posture (the scope-constant pattern this ADR
@@ -1,7 +1,8 @@
# ADR-012: Registry backing, write surface, and the two-op-kind shape # ADR-012: Registry backing, write surface, and the two-op-kind shape
## Status ## Status
Accepted (resolves OQ-06, OQ-07; amends ADR-010's feature story) Accepted (resolves OQ-06, OQ-07; amends ADR-010's feature story; §3 gate
table replaced by ADR-015)
## Context ## Context
@@ -83,16 +84,20 @@ pub trait GitRegistryStore: GitRegistry {
separate, additive — built when a deployment with dynamic repo separate, additive — built when a deployment with dynamic repo
management at persistence scale exists. management at persistence scale exists.
### 3. CRUD ops shipped in-crate, always-on, External with scope+ownership ACL ### 3. CRUD ops shipped in-crate, always-on, External with scope+grant ACL
*(Gate table amended by ADR-015 — the original "scope OR ownership"
formulation was not expressible in alkcall's `AccessControl` (review 001
A-1); the two-tier rule below is the ADR-015 shape.)*
Ops (names indicative, schema-stable once published): Ops (names indicative, schema-stable once published):
| Op | Acts on | Gate | | Op | Acts on | Gate |
|---|---|---| |---|---|---|
| `git/repo/create` | store: mint id, allocate root, seed record | scope `git:repo:create` | | `git/repo/create` | store: mint id, allocate root, seed record | scope `git:repo:create` (static registry gate) |
| `git/repo/delete` | store: remove record | scope `git:admin` **or** ownership | | `git/repo/delete` | store: remove record | scope `git:admin` **or** `manage` grant (ADR-015) |
| `git/repo/update` | store: visibility/grants/root | scope `git:admin` **or** ownership | | `git/repo/update` | store: visibility/grants/root | scope `git:admin` **or** `manage` grant (ADR-015) |
| `git/repo/get` (list/read) | registry reads | scope `git:admin` **or** ownership | | `git/repo/get` (list/read) | registry reads | scope `git:admin` **or** `manage` grant (ADR-015) |
- **Shape:** ordinary alkcall call ops (`OperationSpec`, JSON, - **Shape:** ordinary alkcall call ops (`OperationSpec`, JSON,
schema-validated), `Visibility::External`, registered by the schema-validated), `Visibility::External`, registered by the
@@ -103,16 +108,21 @@ Ops (names indicative, schema-stable once published):
authorized-surface), not by hiding the ops. OQ-07's old authorized-surface), not by hiding the ops. OQ-07's old
"Internal ops over an admin listener" framing had the right mechanism "Internal ops over an admin listener" framing had the right mechanism
and the wrong axis. and the wrong axis.
- **Two-tier authorization** uses the checks `AccessControl::check` - **Two-tier authorization** *(superseded by ADR-015's shape: `create`
already composes: `required_scopes` for the global tier, keeps a pure-scope `AccessControl`; `delete`/`update`/`get` carry an
`resource_type` + ownership for the self-owned tier (alkcall ADR-011 empty `AccessControl` and the handler evaluates "admin scope OR
— repo create mints ownership via `OwnershipStore::record`, so `authorize(record, identity, manage)`" as one tested function in
"creator administers their own repos" is the ownership path, not a alkgit core, with the generic FORBIDDEN denial and the unknown-repo ≡
grant table). unauthorized rule (ADR-008) at the op layer. Repo create mints
- **Ownership never implies git access.** Repo create seeds the ownership via `OwnershipStore::record` *and seeds the creator's
creator's read/write grants in the record explicitly (ADR-011's `{read, write, manage}` grants* — "creator administers their own
two-data-sources rule: op ACL governs *managing records*, repo grants repos" is now the `manage` grant path.)*
govern *git access*). - **Grants govern access; scopes govern platform capability.** Repo
create seeds the creator's `{read, write, manage}` grants in the
record explicitly (ADR-015: administration is the `manage` grant —
the record is the single per-repo authz surface; op scopes and record
grants remain distinct *sources*, platform capability vs per-repo
capability).
### 4. Feature split (amends ADR-010's single-`gix` feature story) ### 4. Feature split (amends ADR-010's single-`gix` feature story)
@@ -0,0 +1,168 @@
# 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)
+9 -6
View File
@@ -26,9 +26,10 @@ the door's key-based identity) resolving an alkcall `Identity` (the
identity-extractor seam that ADR-006 needed exists only in the door, where identity-extractor seam that ADR-006 needed exists only in the door, where
it belongs). alkgit consumes the resolved identity and the registry it belongs). alkgit consumes the resolved identity and the registry
record: the per-repo check is alkgit-core's `authorize` policy function record: the per-repo check is alkgit-core's `authorize` policy function
(ADR-011 — public+read anonymous-first-class, write always (ADR-011, ADR-015 — public+read anonymous-first-class, write always
authenticated+granted), run at ADR-007's step-3 position by every door authenticated+granted, `manage` for repo administration), run at
and by the channels open-op gate. There is no door-specific auth surface ADR-007's step-3 position by every door and by the channels open-op
gate. There is no door-specific auth surface
in alkgit and no scope constant — per-repo grants replaced the in alkgit and no scope constant — per-repo grants replaced the
`tty:open`-style gate (the single-scope shape cannot express `tty:open`-style gate (the single-scope shape cannot express
anonymous-public fetch). anonymous-public fetch).
@@ -111,16 +112,18 @@ deployment's docs, not here.
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids | | [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry ids |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | limits on every session | | [009](decisions/009-bounded-resources-budget.md) | Budgets | limits on every session |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | doors are family infrastructure | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | doors are family infrastructure |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | `authorize` policy, grants in records | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | `authorize` policy, grants in records (amended: ADR-015) |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | op registration surface, two op kinds | | [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | op registration surface, two op kinds (gate amended: ADR-015) |
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push advertisement per door, report framing | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push advertisement per door, report framing |
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | one round per POST; ack section is per-round, stateless | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | one round per POST; ack section is per-round, stateless |
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage op gate |
## Open Questions ## Open Questions
- None. (OQ-08 resolved by ADR-011 — door auth mechanics stay here, - None. (OQ-08 resolved by ADR-011 — door auth mechanics stay here,
registry identity model settled; ADR-012 §3 pins the op registration registry identity model settled; ADR-012 §3 pins the op registration
surface.) surface, its gate amended by ADR-015. OQ-16 — grant-key identity
namespace — lives at the identity-provider seam, not the door.)
## References ## References
+53 -12
View File
@@ -25,10 +25,11 @@ when their impacts say so; it records how careful the resolution must be.
|---|---|---| |---|---|---|
| OQ-05 | deferred(scope) | ecosystem need for sha256 | | OQ-05 | deferred(scope) | ecosystem need for sha256 |
| OQ-03 | partially resolved | first-publish timing (release decision; the freeze inventory lives in backend.md/transport.md) | | OQ-03 | partially resolved | first-publish timing (release decision; the freeze inventory lives in backend.md/transport.md) |
| OQ-16 | deferred(scope) | first cross-assembly record sharing (distributed-git phase; tracker `architecture/oq-16-grant-identity-namespace`) |
OQ-02 and OQ-04 resolved this cycle (ADR-014, ADR-013); OQ-08/06/07/09/01 OQ-02 and OQ-04 resolved this cycle (ADR-014, ADR-013); OQ-08/06/07/09/01
resolved in earlier cycles. No `open` or `deferred(unclear)` questions resolved in earlier cycles. OQ-16 is deferred but blocks nothing in v1
remain. (grants key on cluster-local ids within one assembly).
## Theme: composition / crate shapes ## Theme: composition / crate shapes
@@ -152,10 +153,48 @@ remain.
function (the clause ADR-007 pinned but the static ACL engine could function (the clause ADR-007 pinned but the static ACL engine could
not express); vault placement resolved as **nothing to place in v1** not express); vault placement resolved as **nothing to place in v1**
(no credential-shaped material in metadata). (no credential-shaped material in metadata).
*(Grant-key id format declared deliberately open by ADR-015 §7 — the
cross-identity-namespace question this leaves is tracked as OQ-16.)*
- **Resolution**: [decisions/011-per-repo-authorization.md] - **Resolution**: [decisions/011-per-repo-authorization.md]
- **Cross-references**: ADR-007, ADR-008, ADR-011, ADR-012, OQ-06, OQ-07, - **Cross-references**: ADR-007, ADR-008, ADR-011, ADR-012, OQ-06, OQ-07,
backend.md §GitRegistry, doors.md backend.md §GitRegistry, doors.md
### OQ-16: Grant-key identity namespace (globally-comparable ids for multi-hub grants)
- **Origin**: ADR-015 §7; user session (2026-09-26)
- **Status**: deferred(scope)
- **Door type**: two-way (schema field format; grants are already opaque
strings, so this constrains only the *contents*, not the alkgit API)
- **Priority**: low (blocks nothing in v1 — grants key on alkcall's
cluster-local id within one assembly, which is the only deployment
v1 serves)
- **Question**: what identity-id format do grants key on when repo
records are shared across assemblies (the distributed/replicator
deployment: contract-watching hubs mirroring grant state)? Today the
id is alkcall's `Identity.id` — stable within one cluster, but
meaningless across hubs. Candidate shapes: `{namespace}/{id}`
compound ids (the `{user|org}/{repo}` convention familiar from
self-hosted git — e.g. `alkdev/alkgit` locally, `alkimiadev/alkgit`
when mirrored — extended to identity: `gh:alkimiadev`,
`alk:alkdev`), did-key / contract-derived identifiers (the planned
smart-contract org/user/repo model), or a two-part
(authority, local-id) tuple.
- **Constraints any resolution must satisfy** (recorded from ADR-015 §7):
grants remain flat sets keyed on opaque strings; alkgit never parses,
validates, or interprets the id — collision-freedom is the
identity-provider's guarantee; a downstream namespace can be
introduced at the assembly seam without an alkgit change.
- **Impacts**: only the downstream identity-provider seam and the
future platform app / replicator grant-sync format. The v1
`authorize` policy function, the grant schema shape, and the op gate
are all id-format-agnostic by construction.
- **Blocked on**: the first deployment that shares repo records across
assemblies (the distributed-git phase; also touches alkcall's
identity model if a family-wide id format is preferred over a
payload-level namespace convention). Tracker task:
`tasks/architecture/oq-16-grant-identity-namespace.md`.
- **Cross-references**: ADR-011 §1, ADR-015 §7, OQ-08
## Theme: storage / metadata ## Theme: storage / metadata
### OQ-06: Registry/metadata backing store (gix feature) ### OQ-06: Registry/metadata backing store (gix feature)
@@ -179,14 +218,16 @@ remain.
- **Status**: **resolved** — ADR-012 (§3, §5): the crate ships a minimal - **Status**: **resolved** — ADR-012 (§3, §5): the crate ships a minimal
CRUD op set (`git/repo/{create,delete,update,get}`) as thin call ops CRUD op set (`git/repo/{create,delete,update,get}`) as thin call ops
over `GitRegistryStore`, `Visibility::External`, always-on, gated by over `GitRegistryStore`, `Visibility::External`, always-on, gated by
scopes (global tier, e.g. `git:admin` / `git:repo:create`) + ownership the global admin scope and per-repo grants. The old "Internal ops over
(self-owned tier; create mints ownership per alkcall ADR-011 and an admin listener" framing is superseded (right mechanism, wrong axis
seeds the creator's grants — ownership never implies git access, — visible-surface = authorized-surface is served by the ACL). The
ADR-011). The old "Internal ops over an admin listener" framing is gate table as written used scope+ownership and is replaced by ADR-015
superseded (right mechanism, wrong axis — visible-surface = (the `manage` grant tier — admin scope OR manage grant, handler-
authorized-surface is served by the ACL). Ops stay record-scoped; evaluated; create seeds the creator's `{read, write, manage}` grants).
anything beyond repo records (users, orgs) is a downstream platform Ops stay record-scoped; anything beyond repo records (users, orgs) is
crate (the recorded split trigger, ADR-012 §5). a downstream platform crate (the recorded split trigger, ADR-012 §5) —
the app compiles roles/teams into flat grants (ADR-015 §6).
- **Resolution**: [decisions/012-registry-backing-and-ops.md] §3, §5 - **Resolution**: [decisions/012-registry-backing-and-ops.md] §3, §5
- **Cross-references**: ADR-007, ADR-011, ADR-012, alkcall ADR-017/011, (gate amended by [decisions/015-manage-grant-and-op-gate.md])
backend.md §"The two op kinds" - **Cross-references**: ADR-007, ADR-011, ADR-012, ADR-015, alkcall
ADR-017/011, backend.md §"The two op kinds"
+10 -4
View File
@@ -88,22 +88,28 @@ multi-round negotiation are design-complete against real-client captures
| [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry IDs | | [008](decisions/008-registry-resolved-repo-identity.md) | Repo identity | wire names are registry IDs |
| [009](decisions/009-bounded-resources-budget.md) | Budgets | every session carries limits | | [009](decisions/009-bounded-resources-budget.md) | Budgets | every session carries limits |
| [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | single crate, producer/consumer halves, no doors/binary | | [010](decisions/010-pure-protocol-crate.md) | Pure protocol crate | single crate, producer/consumer halves, no doors/binary |
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in repo records, policy in core, vault-nil | | [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | grants in repo records, policy in core, vault-nil (amended: ADR-015) |
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | read/write split, file default, CRUD ops, feature split | | [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | read/write split, file default, CRUD ops, feature split |
| [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push machine, thin-pack acceptance, unpack-first CAS | | [013](decisions/013-receive-pack-state-machine.md) | receive-pack | V0-framed push machine, thin-pack acceptance, unpack-first CAS |
| [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` | | [014](decisions/014-v2-negotiation-ack-loop.md) | Negotiation | ack loop, `common_haves` seam, no `ready` |
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage gate, create seeds manage |
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage gate, create seeds manage |
## Open Questions ## Open Questions
Key questions tracked in [open-questions.md](open-questions.md): Key questions tracked in [open-questions.md](open-questions.md):
- **OQ-03**: publish-time API freeze inventory (the `git/repo/*` op set - **OQ-03**: publish-time API freeze inventory (the `git/repo/*` op set
and the trait family enter it; ADR-012, ADR-013/014's trait additions). and the trait family enter it; ADR-012, ADR-013/014's trait additions;
the ADR-015 three-action grant shape must land in it).
- **OQ-05**: sha256 policy (deferred(scope), low). - **OQ-05**: sha256 policy (deferred(scope), low).
- **OQ-16**: grant-key identity namespace (deferred(scope); blocks
nothing in v1 — ADR-015 §7).
Resolved this cycle: OQ-04 (ADR-013 — receive-pack state machine, Resolved this cycle: OQ-04 (ADR-013 — receive-pack state machine,
capture-grounded), OQ-02 (ADR-014 — negotiation ack loop). Earlier: capture-grounded), OQ-02 (ADR-014 — negotiation ack loop). Earlier:
OQ-08 (ADR-011 — per-repo authorization, grants in records, vault-nil), OQ-08 (ADR-011 — per-repo authorization, grants in records, vault-nil),
OQ-06 (ADR-012 — `registry-file` default, persistence adapters OQ-06 (ADR-012 — `registry-file` default, persistence adapters
additive), OQ-07 (ADR-012 — CRUD ops shipped External with additive), OQ-07 (ADR-012 — CRUD ops shipped External with scope+grant
scope+ownership ACL), OQ-09/OQ-01 (ADR-010 — pure protocol crate). ACL, gate amended by ADR-015), OQ-09/OQ-01 (ADR-010 — pure protocol
crate).
+5
View File
@@ -123,6 +123,9 @@ in use**. So:
- Push is always authenticated, on every repo, no exceptions. - Push is always authenticated, on every repo, no exceptions.
- ACL must be simple enough to reason about completely: repo visibility - ACL must be simple enough to reason about completely: repo visibility
(public/private) + identity-based read/write, nothing richer in v1. (public/private) + identity-based read/write, nothing richer in v1.
*(Amended by ADR-015: the grant set gains `manage` — repo
administration — still flat, still per-repo, still one policy
function.)*
## What phase 0 must still produce before phase 1 ## What phase 0 must still produce before phase 1
@@ -143,6 +146,8 @@ in use**. So:
- Registry management ops, if the gix feature ships any, are alkcall - Registry management ops, if the gix feature ships any, are alkcall
`Visibility::Internal` ops over an admin-only listener; they are never `Visibility::Internal` ops over an admin-only listener; they are never
part of the git traffic surface (OQ-07). part of the git traffic surface (OQ-07).
*(Superseded by ADR-012 §3 and ADR-015: the ops are External,
gated by scope + the `manage` grant — right mechanism, wrong axis.)*
- Repo names arriving on the wire are registry IDs, never paths; storage - Repo names arriving on the wire are registry IDs, never paths; storage
roots are configured server-side only. roots are configured server-side only.
- The advertisement phase runs ACL before the first ref line is emitted. - The advertisement phase runs ACL before the first ref line is emitted.
@@ -758,7 +758,7 @@ criticals are ADR-writing work, not code):
| ID | Finding | Recommended fix | Effort | Risk | Status | | ID | Finding | Recommended fix | Effort | Risk | Status |
|----|---------|----------------|--------|------|--------| |----|---------|----------------|--------|------|--------|
| A-1 | op-gate OR not expressible in `AccessControl` | new ADR (or ADR-012 §3 amendment): handler-side two-tier check, create keeps static scope gate | small | none | open | | A-1 | op-gate OR not expressible in `AccessControl` | new ADR (or ADR-012 §3 amendment): handler-side two-tier check, create keeps static scope gate | small | none | **resolved (ADR-015)** — option (a) shape with the OR-term generalized to the `manage` grant |
| A-2 | `async fn` traits not dyn-compatible | ADR-012 §1 + backend.md amendment: `#[async_trait]`; add `async-trait = "0.1"` to manifest | small | none | open | | A-2 | `async fn` traits not dyn-compatible | ADR-012 §1 + backend.md amendment: `#[async_trait]`; add `async-trait = "0.1"` to manifest | small | none | open |
| A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | open | | A-3 | native preamble / service dimension unpinned | new ADR: open-op params `{repo, service}`, session tuple + stateless entry gain the service selector, `GitAdapter` preamble pinned | moderate | wire-format (freeze inventory) | open |
| A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = `common_haves`-filtered haves | trivial | none | open | | A-4 | done-round boundary set unverified | ADR-014 §2 + transport.md clause: boundary = `common_haves`-filtered haves | trivial | none | open |
@@ -0,0 +1,53 @@
---
id: architecture/oq-16-grant-identity-namespace
name: OQ-16 unblock — first cross-assembly record sharing
status: pending
depends_on: []
scope: narrow
risk: trivial
impact: component
level: research
tags: [external-trigger, deferred-oq]
---
## Description
Tracker for OQ-16 (`deferred(scope)`): the grant-key identity namespace.
Grants on `RepoRecord` key on opaque identity-id strings (ADR-011,
ADR-015 §7). Within one assembly those are alkcall's cluster-local
`Identity.id` — sufficient for v1. The question of a
globally-comparable id format (compound `{namespace}/{id}` shapes,
did-key/contract-derived identifiers, or (authority, local-id)
tuples) only becomes real when repo records are shared across
assemblies — the distributed-git/replicator phase, where a hub mirrors
grant state published by an external authority.
Constraints any resolution must satisfy are already recorded in the OQ
entry (ADR-015 §7): grants stay flat sets keyed on opaque strings;
alkgit never parses or interprets the id; collision-freedom is the
identity-provider's guarantee.
## Work
None while blocked. When the first cross-assembly record-sharing
deployment is designed (the distributed-git phase, or a platform app
that federates grant state), decide the id format: either a
payload-level namespace convention (alkgit-side, opaque strings with a
namespaced grammar) or a family-wide identity-id format (alkcall-level
change). Record the resolution in an ADR; update the OQ entry.
## Verification
- OQ-16 status matches `docs/architecture/open-questions.md`.
- If resolved: an ADR exists and the grant-schema freeze inventory
(OQ-03) reflects the chosen id format.
## Out of scope
- Building the distributed-git replication machinery (separate phase).
- Changing alkcall's `Identity` model (only if the family-wide option
is chosen, and as that crate's own decision).
## Summary
> Filled on completion.