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:
1 parent
d478361a15
commit
c4b9c53674
11 files changed
+381
-64
No files matched your search
@@ -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,
|
||||
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
|
||||
are the publish-freeze timing (OQ-03, a release decision) and sha256
|
||||
policy (OQ-05, deferred on ecosystem need).
|
||||
are the publish-freeze timing (OQ-03, a release decision), sha256
|
||||
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
|
||||
|
||||
@@ -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 |
|
||||
| [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 |
|
||||
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant tier + repo-op gate (admin scope OR manage grant) | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
All unresolved questions are tracked in [open-questions.md](open-questions.md)
|
||||
with stable OQ-IDs, priorities, and cross-references. Remaining: OQ-03
|
||||
(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.
|
||||
|
||||
## Document Lifecycle
|
||||
|
||||
@@ -20,7 +20,8 @@ Five traits, kept small and orthogonal — the protocol crate never sees
|
||||
gix types:
|
||||
|
||||
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
|
||||
indistinguishable from authorization failure (ADR-007). Authorization
|
||||
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
|
||||
split shape). The `git/repo/*` ops wrap it (ADR-012 §3). Grants ride
|
||||
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
|
||||
targets, the ls-refs response data) and ref transactions (CAS apply
|
||||
for receive-pack, name validation per git ref rules + reserved-
|
||||
@@ -123,12 +126,16 @@ planned: call ops only):
|
||||
yields the duplex git session.
|
||||
- **Call ops** (`git/repo/{create,delete,update,get}`) — the JSON half:
|
||||
thin `OperationSpec`+handler pairs over `Arc<dyn GitRegistryStore>`,
|
||||
`Visibility::External`, always-on (no gitoxide), gated by
|
||||
`required_scopes` (global tier) + ownership (self-owned tier, alkcall
|
||||
ADR-011 mint-and-own at create). Registered by the assembler wherever
|
||||
it wants them exposed. Managing records (op ACL) and git access (repo
|
||||
grants) are different capabilities — ownership never implies git
|
||||
access (ADR-011, ADR-012 §3).
|
||||
`Visibility::External`, always-on (no gitoxide). `create` is gated by
|
||||
the static registry check (`required_scopes: ["git:repo:create"]`);
|
||||
`delete`/`update`/`get` carry an empty `AccessControl` and the handler
|
||||
evaluates the ADR-015 gate — scope `git:admin` OR the record's
|
||||
`manage` grant, via the `authorize` policy function, generic FORBIDDEN
|
||||
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
|
||||
|
||||
@@ -139,8 +146,9 @@ planned: call ops only):
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | write trait, file default, CRUD ops, feature split |
|
||||
| [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 (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 |
|
||||
| [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
|
||||
|
||||
## Status
|
||||
Accepted (resolves OQ-08)
|
||||
Accepted (resolves OQ-08; grant action set + policy domain amended by
|
||||
ADR-015)
|
||||
|
||||
## Context
|
||||
|
||||
@@ -47,11 +48,16 @@ metadata must hold.
|
||||
|
||||
2. **Grants live in repo records, keyed on the logical identity id.**
|
||||
`RepoRecord` carries `visibility` (Public/Private) and
|
||||
`grants: identity_id → {read, write}`. Per-repo grants are repo
|
||||
lifecycle data — created with the repo, 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).
|
||||
`grants: identity_id → {read, write}` *(amended by ADR-015: the
|
||||
action set gains `manage` — `grants: identity_id → {read, write,
|
||||
manage}`, a flat set with no hierarchy; `manage ⊇ write ⊇ read`)*.
|
||||
Per-repo grants are repo lifecycle data — created with the repo,
|
||||
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
|
||||
against the record:
|
||||
@@ -60,6 +66,12 @@ metadata must hold.
|
||||
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`
|
||||
(anonymous-first-class; ADR-007's operative clause).
|
||||
- `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
|
||||
growth); no vault placement question remains for v1.
|
||||
- **Negative:** two authorization data sources exist by design — op ACL
|
||||
(scopes + ownership, for *managing records*) and repo grants (for *git
|
||||
access*). Ownership never implies git access: repo creation seeds the
|
||||
creator's grants explicitly (ADR-012). This split is deliberate
|
||||
(managing vs accessing are different capabilities) but is a thing a
|
||||
reader must hold both of.
|
||||
(scopes, for platform-level capability) and repo grants (for per-repo
|
||||
capability). This split is deliberate but is a thing a reader must
|
||||
hold both of. *(Amended by ADR-015: repo administration is the
|
||||
`manage` grant — the record is the single per-repo authz surface;
|
||||
"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
|
||||
alkcall's `PeerEntry` model, not guaranteed here — alkgit's contract
|
||||
is only "grants key on the stable logical id."
|
||||
|
||||
## 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
|
||||
(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),
|
||||
ADR-017 (the static ACL engine this complements)
|
||||
- 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
|
||||
|
||||
## 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
|
||||
|
||||
@@ -83,16 +84,20 @@ pub trait GitRegistryStore: GitRegistry {
|
||||
separate, additive — built when a deployment with dynamic repo
|
||||
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):
|
||||
|
||||
| Op | Acts on | Gate |
|
||||
|---|---|---|
|
||||
| `git/repo/create` | store: mint id, allocate root, seed record | scope `git:repo:create` |
|
||||
| `git/repo/delete` | store: remove record | scope `git:admin` **or** ownership |
|
||||
| `git/repo/update` | store: visibility/grants/root | scope `git:admin` **or** ownership |
|
||||
| `git/repo/get` (list/read) | registry reads | scope `git:admin` **or** ownership |
|
||||
| `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** `manage` grant (ADR-015) |
|
||||
| `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** `manage` grant (ADR-015) |
|
||||
|
||||
- **Shape:** ordinary alkcall call ops (`OperationSpec`, JSON,
|
||||
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
|
||||
"Internal ops over an admin listener" framing had the right mechanism
|
||||
and the wrong axis.
|
||||
- **Two-tier authorization** uses the checks `AccessControl::check`
|
||||
already composes: `required_scopes` for the global tier,
|
||||
`resource_type` + ownership for the self-owned tier (alkcall ADR-011
|
||||
— repo create mints ownership via `OwnershipStore::record`, so
|
||||
"creator administers their own repos" is the ownership path, not a
|
||||
grant table).
|
||||
- **Ownership never implies git access.** Repo create seeds the
|
||||
creator's read/write grants in the record explicitly (ADR-011's
|
||||
two-data-sources rule: op ACL governs *managing records*, repo grants
|
||||
govern *git access*).
|
||||
- **Two-tier authorization** *(superseded by ADR-015's shape: `create`
|
||||
keeps a pure-scope `AccessControl`; `delete`/`update`/`get` carry an
|
||||
empty `AccessControl` and the handler evaluates "admin scope OR
|
||||
`authorize(record, identity, manage)`" as one tested function in
|
||||
alkgit core, with the generic FORBIDDEN denial and the unknown-repo ≡
|
||||
unauthorized rule (ADR-008) at the op layer. Repo create mints
|
||||
ownership via `OwnershipStore::record` *and seeds the creator's
|
||||
`{read, write, manage}` grants* — "creator administers their own
|
||||
repos" is now the `manage` grant path.)*
|
||||
- **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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
it belongs). alkgit consumes the resolved identity and the registry
|
||||
record: the per-repo check is alkgit-core's `authorize` policy function
|
||||
(ADR-011 — public+read anonymous-first-class, write always
|
||||
authenticated+granted), run at ADR-007's step-3 position by every door
|
||||
and by the channels open-op gate. There is no door-specific auth surface
|
||||
(ADR-011, ADR-015 — public+read anonymous-first-class, write always
|
||||
authenticated+granted, `manage` for repo administration), run at
|
||||
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
|
||||
`tty:open`-style gate (the single-scope shape cannot express
|
||||
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 |
|
||||
| [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 |
|
||||
| [011](decisions/011-per-repo-authorization.md) | Per-repo authorization | `authorize` policy, grants in records |
|
||||
| [012](decisions/012-registry-backing-and-ops.md) | Registry + ops | op registration surface, two op kinds |
|
||||
| [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 (gate amended: ADR-015) |
|
||||
| [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 |
|
||||
| [015](decisions/015-manage-grant-and-op-gate.md) | Manage grant + op gate | `manage` tier, admin-OR-manage op gate |
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None. (OQ-08 resolved by ADR-011 — door auth mechanics stay here,
|
||||
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
|
||||
|
||||
|
||||
@@ -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-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
|
||||
resolved in earlier cycles. No `open` or `deferred(unclear)` questions
|
||||
remain.
|
||||
resolved in earlier cycles. OQ-16 is deferred but blocks nothing in v1
|
||||
(grants key on cluster-local ids within one assembly).
|
||||
|
||||
## Theme: composition / crate shapes
|
||||
|
||||
@@ -152,10 +153,48 @@ remain.
|
||||
function (the clause ADR-007 pinned but the static ACL engine could
|
||||
not express); vault placement resolved as **nothing to place in v1**
|
||||
(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]
|
||||
- **Cross-references**: ADR-007, ADR-008, ADR-011, ADR-012, OQ-06, OQ-07,
|
||||
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
|
||||
|
||||
### OQ-06: Registry/metadata backing store (gix feature)
|
||||
@@ -179,14 +218,16 @@ remain.
|
||||
- **Status**: **resolved** — ADR-012 (§3, §5): the crate ships a minimal
|
||||
CRUD op set (`git/repo/{create,delete,update,get}`) as thin call ops
|
||||
over `GitRegistryStore`, `Visibility::External`, always-on, gated by
|
||||
scopes (global tier, e.g. `git:admin` / `git:repo:create`) + ownership
|
||||
(self-owned tier; create mints ownership per alkcall ADR-011 and
|
||||
seeds the creator's grants — ownership never implies git access,
|
||||
ADR-011). The old "Internal ops over an admin listener" framing is
|
||||
superseded (right mechanism, wrong axis — visible-surface =
|
||||
authorized-surface is served by the ACL). Ops stay record-scoped;
|
||||
anything beyond repo records (users, orgs) is a downstream platform
|
||||
crate (the recorded split trigger, ADR-012 §5).
|
||||
the global admin scope and per-repo grants. The old "Internal ops over
|
||||
an admin listener" framing is superseded (right mechanism, wrong axis
|
||||
— visible-surface = authorized-surface is served by the ACL). The
|
||||
gate table as written used scope+ownership and is replaced by ADR-015
|
||||
(the `manage` grant tier — admin scope OR manage grant, handler-
|
||||
evaluated; create seeds the creator's `{read, write, manage}` grants).
|
||||
Ops stay record-scoped; anything beyond repo records (users, orgs) is
|
||||
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
|
||||
- **Cross-references**: ADR-007, ADR-011, ADR-012, alkcall ADR-017/011,
|
||||
backend.md §"The two op kinds"
|
||||
(gate amended by [decisions/015-manage-grant-and-op-gate.md])
|
||||
- **Cross-references**: ADR-007, ADR-011, ADR-012, ADR-015, alkcall
|
||||
ADR-017/011, backend.md §"The two op kinds"
|
||||
@@ -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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [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` |
|
||||
| [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
|
||||
|
||||
Key questions tracked in [open-questions.md](open-questions.md):
|
||||
|
||||
- **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-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,
|
||||
capture-grounded), OQ-02 (ADR-014 — negotiation ack loop). Earlier:
|
||||
OQ-08 (ADR-011 — per-repo authorization, grants in records, vault-nil),
|
||||
OQ-06 (ADR-012 — `registry-file` default, persistence adapters
|
||||
additive), OQ-07 (ADR-012 — CRUD ops shipped External with
|
||||
scope+ownership ACL), OQ-09/OQ-01 (ADR-010 — pure protocol crate).
|
||||
additive), OQ-07 (ADR-012 — CRUD ops shipped External with scope+grant
|
||||
ACL, gate amended by ADR-015), OQ-09/OQ-01 (ADR-010 — pure protocol
|
||||
crate).
|
||||
@@ -123,6 +123,9 @@ in use**. So:
|
||||
- Push is always authenticated, on every repo, no exceptions.
|
||||
- ACL must be simple enough to reason about completely: repo visibility
|
||||
(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
|
||||
|
||||
@@ -143,6 +146,8 @@ in use**. So:
|
||||
- Registry management ops, if the gix feature ships any, are alkcall
|
||||
`Visibility::Internal` ops over an admin-only listener; they are never
|
||||
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
|
||||
roots are configured server-side only.
|
||||
- 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 |
|
||||
|----|---------|----------------|--------|------|--------|
|
||||
| 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-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 |
|
||||
|
||||
@@ -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.
|
||||
Reference in new issue
Block a user