docs(review): 012 — gateway ownership seam, consumer input from alkgit's LFS POC

Report-only: GatewayDispatch::invoke hardwires ownership: None, so
External namespace-ACL ops through the HTTP gateway fall back to the
static Identity.resources model — token-carried claims admit without
consulting revocable grant tables (POC-L tests/gateway_gap.rs).
Three resolution shapes recorded; disposition is session-owned.
This commit is contained in:
glm-5.3-flash committed 2026-10-17 07:28:44 +00:00
1 parent 1a493e9748
commit 03a6689c99
1 file changed
+105
@@ -0,0 +1,105 @@
# Review 012 — The gateway dispatch's hardwired `ownership: None`: downstream evidence from alkgit's LFS POC
## Status
Open — filed 2026-10-17. Report-only consumer input; the resolution
(session-owned) either threads an ownership provider through the
gateway dispatch path or blesses/documents the door-side compensation
pattern as the durable answer. Filed here rather than in the consumer
because the anchor is this crate's surface
(`src/gateway/dispatch.rs`), and because the family precedent exists
(review 011 — downstream input filed in the crate that owns the
seam).
Input basis:
- **POC-L** (alkgit, 2026-10-16; findings at
`/workspace/@alkdev/alkgit/docs/research/pocl-findings.md`; the
evidence test is `tests/gateway_gap.rs` in `/workspace/lfs-poc`) —
a real `git-lfs` client served through an alkhttp extras door
mapping the LFS batch API onto alkblobs' `ops` (`blobs/stat`,
namespace-ACL-gated, External visibility).
- The ask ledger: alkgit's `substrate-recomposition.md`
§Upstream-push candidates #6 (the "alkhttp gateway ownership seam"
entry — filed here under the crate that owns the dispatch spine).
---
## 1. The finding
`GatewayDispatch::invoke`'s `OperationContext` builder hardwires
`ownership: None` when it assembles an external identity's context
(`src/gateway/dispatch.rs`, the builder around lines 215–249; the
`ownership: None` field assignment verified this session). Downstream
consequence, demonstrated live in POC-L:
- `AccessControl::check` for a namespace-ACL-gated op (`resource_id`
extracted from the payload — alkblobs' `blobs/stat` with
`resource_id_path: $.namespace`, External visibility) falls back
to the **static** `Identity.resources` model when no ownership
provider is present in the context.
- Therefore a token-**carried static claim** — e.g. an LFS-style
bearer JWT embedding `{"blob-namespace": ["read"]}` in its
resources map — **is admitted through the gateway with no runtime
provider consulted**, while the same op served through the native
call protocol consults the dynamic (revocable) grant table and
admits only live grants.
- The dynamic grant table is *unreachable* from this dispatch spine:
no thread of the gateway path carries an ownership provider, so a
deployment whose authorization truth is a revocable table gets
stale-at-birth answers on the HTTP-gateway surface — revocation
and grant changes never apply there.
The security frame is the family's standing invariant
(visible-surface = authorized-surface): an External op must not have
two authorization paths with different revocation semantics
depending on which door the caller walked through. The static
fallback is admission-by-claim at the exact point where the family's
posture wants admission-by-live-grant.
## 2. What it means for the consumer (and what they did about it)
The POC-L door cannot use the gateway dispatch spine for these ops.
Its compensation: build the `OperationContext` itself (identity from
the resolved HTTP identity + the ownership provider wired
explicitly) and call `OperationRegistry::invoke("blobs/stat", …)`
directly — after which the namespace ACL runs exactly as it would on
the native wire, against live grant sets (POC-L finding 2, the batch
binding section). The compensation is correct and verified, but it
is per-door hand-assembly of what the dispatch spine exists to do —
every future door (alkhttp's `git` feature, the LFS door proper,
alkssh's surfaces) inherits the same manual step and the same trap of
forgetting it (the spine *works* with the fallback, silently).
## 3. Resolution shapes (recommendation only — own session)
1. **Thread an ownership provider through the dispatch path** — an
`Option` on the gateway dispatch builder (per-dispatch config or
a connection-establishment property), `None` only where a
deployment genuinely has no ownership model. Preserves the
single-spine posture; needs an ADR if the context shape is
frozen-published (alkcall is 0.9, published — the context builder
surface's semver class decides).
2. **Bless the compensation**: document the door-side pattern (door
builds its own context with ownership wired; never route
External namespace-ACL ops through `GatewayDispatch::invoke`
without it) as a *door contract* line in the gateway docs, plus
a loud static-fallback warning at the hardwired site. Cheap, but
the trap stays structural — silent-by-default.
3. **Hybrid**: doc contract now (2), seam threading as the ADR'd
follow-up (1). Probably where the session lands, given 0.9 is
published and the ADR round for the context shape is not open.
**Verification status**: the `ownership: None` anchor and the
builder's context assembly were re-read this session; the admission
behavior (static `Identity.resources` fallback, token-carried claim
admitted) is POC-L's `tests/gateway_gap.rs` result, witnessed in the
findings doc. Re-verify the behavior during resolution by re-running
that test against a current alkcall.
## 4. Non-goal note
Not asking for anything on the native call-protocol path: the
channel-path dispatch carries ownership correctly (the ops wire path
is what the whole family gates through). The finding is scoped to
the gateway dispatch spine used by HTTP-door routing.