From c4b9c536744d3622c80f0a3aa62f70f87d642267 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 26 Sep 2026 11:50:25 +0000 Subject: [PATCH] =?UTF-8?q?docs(architecture):=20ADR-015=20=E2=80=94=20man?= =?UTF-8?q?age=20grant=20tier=20+=20repo-op=20gate,=20OQ-16=20identity=20n?= =?UTF-8?q?amespace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/architecture/README.md | 12 +- docs/architecture/backend.md | 28 +-- .../decisions/011-per-repo-authorization.md | 41 +++-- .../decisions/012-registry-backing-and-ops.md | 42 +++-- .../decisions/015-manage-grant-and-op-gate.md | 168 ++++++++++++++++++ docs/architecture/doors.md | 15 +- docs/architecture/open-questions.md | 65 +++++-- docs/architecture/overview.md | 14 +- docs/research/vision.md | 5 + ...1-architecture-pre-decomposition-review.md | 2 +- .../oq-16-grant-identity-namespace.md | 53 ++++++ 11 files changed, 381 insertions(+), 64 deletions(-) create mode 100644 docs/architecture/decisions/015-manage-grant-and-op-gate.md create mode 100644 tasks/architecture/oq-16-grant-identity-namespace.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index d9a5c06..1edaafc 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -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 diff --git a/docs/architecture/backend.md b/docs/architecture/backend.md index 29b8101..76f5ecf 100644 --- a/docs/architecture/backend.md +++ b/docs/architecture/backend.md @@ -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`, - `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` | diff --git a/docs/architecture/decisions/011-per-repo-authorization.md b/docs/architecture/decisions/011-per-repo-authorization.md index 8b5c714..7f5f43b 100644 --- a/docs/architecture/decisions/011-per-repo-authorization.md +++ b/docs/architecture/decisions/011-per-repo-authorization.md @@ -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 diff --git a/docs/architecture/decisions/012-registry-backing-and-ops.md b/docs/architecture/decisions/012-registry-backing-and-ops.md index 4293c48..26a2dce 100644 --- a/docs/architecture/decisions/012-registry-backing-and-ops.md +++ b/docs/architecture/decisions/012-registry-backing-and-ops.md @@ -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) diff --git a/docs/architecture/decisions/015-manage-grant-and-op-gate.md b/docs/architecture/decisions/015-manage-grant-and-op-gate.md new file mode 100644 index 0000000..7d9e7cc --- /dev/null +++ b/docs/architecture/decisions/015-manage-grant-and-op-gate.md @@ -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) \ No newline at end of file diff --git a/docs/architecture/doors.md b/docs/architecture/doors.md index 55d74b7..eeef15f 100644 --- a/docs/architecture/doors.md +++ b/docs/architecture/doors.md @@ -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 diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index baeed07..62df4fa 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -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" \ No newline at end of file + (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" \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 675d977..12dd60d 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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). \ No newline at end of file +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). \ No newline at end of file diff --git a/docs/research/vision.md b/docs/research/vision.md index 1ef7ac6..2f09e85 100644 --- a/docs/research/vision.md +++ b/docs/research/vision.md @@ -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. diff --git a/docs/reviews/001-architecture-pre-decomposition-review.md b/docs/reviews/001-architecture-pre-decomposition-review.md index b41b406..f45c080 100644 --- a/docs/reviews/001-architecture-pre-decomposition-review.md +++ b/docs/reviews/001-architecture-pre-decomposition-review.md @@ -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 | diff --git a/tasks/architecture/oq-16-grant-identity-namespace.md b/tasks/architecture/oq-16-grant-identity-namespace.md new file mode 100644 index 0000000..540cf47 --- /dev/null +++ b/tasks/architecture/oq-16-grant-identity-namespace.md @@ -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. \ No newline at end of file