findings: reframe relay flow with producer/consumer terminology, hub-as-proxy pattern, concrete opencode-on-remote example, N-consumer fan-out sketch
This commit is contained in:
1 parent
7c1af0d71f
commit
0ef277730f
1 file changed
+69
-41
@@ -563,43 +563,45 @@ holds.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## The hub-relay + worker-expose flow (walked end-to-end)
|
## The hub-relay flow (walked end-to-end)
|
||||||
|
|
||||||
This is the flow the outsider flagged as "where ADR-073's single-op
|
This is the flow the outsider flagged as "where ADR-073's single-op
|
||||||
design was doing the most implicit work." Walking it under "call++"
|
design was doing the most implicit work." Walking it under "call++"
|
||||||
to verify it holds.
|
to verify it holds.
|
||||||
|
|
||||||
### Browser → hub → spoke, "open me a TTY" (the common case)
|
### Consumer → hub → producer, "open me a TTY" (the common case)
|
||||||
|
|
||||||
1. Browser sends `channels/tty/open` with
|
1. Consumer (browser, another worker, another hub) sends
|
||||||
|
`channels/tty/open` with
|
||||||
`{params: {backend: docker, cmd: ["bash"], container: "abc123"}}`
|
`{params: {backend: docker, cmd: ["bash"], container: "abc123"}}`
|
||||||
on its channel 0 (call op on the browser→hub leg).
|
on its channel 0 (call op on the consumer→hub leg).
|
||||||
2. Hub's `CallAdapter` receives `channels/tty/open`. The
|
2. Hub's `CallAdapter` receives `channels/tty/open`. The
|
||||||
`OperationRegistry` checks the op's `access_control` against the
|
`OperationRegistry` checks the op's `access_control` against the
|
||||||
browser's identity. The op's spec has
|
consumer's identity. The op's spec has
|
||||||
`channel_open: Some(ChannelOpenSpec { alpn: "alknet/tty", direction: Open })`.
|
`channel_open: Some(ChannelOpenSpec { alpn: "alknet/tty", direction: Open })`.
|
||||||
Browser is the initiator / consumer.
|
Consumer is the initiator / consumer.
|
||||||
3. Hub's `CallAdapter` recognizes the `channel_open` marker. The hub
|
3. Hub's `CallAdapter` recognizes the `channel_open` marker. The hub
|
||||||
does NOT run a local `TtyAdapter` — the hub never runs
|
does NOT run a local `TtyAdapter` — the hub never runs
|
||||||
protocol-specific handlers (ADR-079). It forwards to the spoke via
|
protocol-specific handlers (ADR-079). It forwards to the producer
|
||||||
`from_call`: hub re-issues `channels/tty/open` on the spoke leg
|
(spoke/worker) via `from_call`: hub re-issues `channels/tty/open`
|
||||||
with `forwarded_for = browser`.
|
on the producer leg with `forwarded_for = consumer`.
|
||||||
4. Spoke's `CallAdapter` receives `channels/tty/open`.
|
4. Producer's `CallAdapter` receives `channels/tty/open`.
|
||||||
`OperationRegistry` checks the op's `access_control` against the
|
`OperationRegistry` checks the op's `access_control` against the
|
||||||
hub's identity (the direct caller per ADR-032). The spoke's
|
hub's identity (the direct caller per ADR-032). The producer's
|
||||||
ownership store verifies the hub owns `container:abc123`
|
ownership store verifies the hub owns `container:abc123`
|
||||||
(per ADR-050). The spoke consults
|
(per ADR-050). The producer consults
|
||||||
`ChannelLifecyclePolicy::check_open(hub)` (Gap 2 — keyed by
|
`ChannelLifecyclePolicy::check_open(hub)` (Gap 2 — keyed by
|
||||||
direct caller, opener recorded in the per-connection ledger).
|
direct caller, opener recorded in the per-connection ledger).
|
||||||
5. Spoke's `ChannelCore` allocates `channel_id`, spawns `TtyAdapter`
|
5. Producer's `ChannelCore` allocates `channel_id`, spawns
|
||||||
on the channel's `BiStream` with the docker backend, records
|
`TtyAdapter` on the channel's `BiStream` with the docker backend,
|
||||||
opener (hub) in the ledger, returns `{channel_id}`.
|
records opener (hub) in the ledger, returns `{channel_id}`.
|
||||||
6. Hub receives the spoke's `{channel_id}`, opens a matching channel
|
6. Hub receives the producer's `{channel_id}`, opens a matching
|
||||||
on the browser's side (hub is the responder for the browser leg),
|
channel on the consumer's side (hub is the responder for the
|
||||||
records the `channel_id` mapping `browser_id ↔ spoke_id`, returns
|
consumer leg), records the `channel_id` mapping
|
||||||
`{channel_id: browser_id}` to the browser.
|
`consumer_id ↔ producer_id`, returns
|
||||||
7. Hub byte-forwards between `browser_id` and `spoke_id` with 4-byte
|
`{channel_id: consumer_id}` to the consumer.
|
||||||
`channel_id` rewrite (ADR-079 unchanged).
|
7. Hub byte-forwards between `consumer_id` and `producer_id` with
|
||||||
|
4-byte `channel_id` rewrite (ADR-079 unchanged).
|
||||||
|
|
||||||
The hub ran **zero** protocol-specific auth and zero protocol-specific
|
The hub ran **zero** protocol-specific auth and zero protocol-specific
|
||||||
data-plane work. It ran `channels/tty/open`'s `access_control`
|
data-plane work. It ran `channels/tty/open`'s `access_control`
|
||||||
@@ -607,38 +609,64 @@ data-plane work. It ran `channels/tty/open`'s `access_control`
|
|||||||
ADR-079 holds unchanged in shape; only the op name changed (from
|
ADR-079 holds unchanged in shape; only the op name changed (from
|
||||||
generic `channel/open` to per-ALPN `channels/tty/open`).
|
generic `channel/open` to per-ALPN `channels/tty/open`).
|
||||||
|
|
||||||
### Worker → hub → browser, "worker exposes a TTY" (the push case — deferred)
|
### Worker → hub → consumer, "worker exposes a resource" (the proxy case — deferred)
|
||||||
|
|
||||||
The `expose` verb is reserved in the `ChannelDirection` enum but
|
The `expose` verb is reserved in the `ChannelDirection` enum but
|
||||||
deferred. The walk-through below is a sketch of the shape, not a spec.
|
deferred. The walk-through below is a sketch of the shape, not a spec.
|
||||||
The open case (consumer initiates on demand) is the common case and is
|
The open case (consumer initiates on demand) is the common case and is
|
||||||
specced; the expose case is for push scenarios (a worker pushes a log
|
specced; the expose case is for push scenarios where a worker offers
|
||||||
stream to a monitoring browser that's already connected) and will be
|
resources to a hub, which then acts as a proxy — re-exposing them to
|
||||||
specced when a concrete consumer forces the design.
|
other consumers.
|
||||||
|
|
||||||
Sketch: worker sends `channels/tty/expose` on its channel 0. Hub
|
**Concrete example.** A worker runs on a remote instance (vastai,
|
||||||
checks `access_control` against the worker's identity (separate ACL
|
runpod, a docker container). It wraps an opencode server's OpenAPI spec
|
||||||
from `channels/tty/open`). Hub recognizes the `channel_open` marker
|
via `from_openapi` (call ops, JSON) and also registers
|
||||||
and must relay to a connected browser that wants to consume. The hub
|
`channels/tty/open` for terminal access. The worker connects to a hub
|
||||||
initiates `channels/tty/expose` on the browser leg (hub is
|
and exposes both: the call ops and the TTY channel. The hub consumes
|
||||||
producer transparently — it forwards the data plane to the worker).
|
these — the worker's resources become the hub's resources (ownership
|
||||||
Hub allocates `channel_id` on both legs, records the mapping, and
|
model: the hub owns what the worker exposes). Another consumer (a
|
||||||
byte-forwards with `channel_id` rewrite.
|
browser, another worker, another hub) that needs terminal access to
|
||||||
|
that remote dev environment calls `channels/tty/open` on the hub. The
|
||||||
|
hub is now a producer for that consumer — it proxies the data plane
|
||||||
|
between the worker (the real producer) and the consumer.
|
||||||
|
|
||||||
The hard question — "hold until consumer connects" vs "reject if no
|
**The hub-as-proxy pattern.** The hub is a consumer of the worker's
|
||||||
consumer" — is deferred with the verb. The `direction` field is
|
resource and a producer for downstream consumers. The relay is
|
||||||
per-leg, not end-to-end; the relay passes the verb through
|
protocol-agnostic: swap channel IDs, tunnel reads/writes between the
|
||||||
(`expose` → `expose`), same as in the open case (`open` → `open`).
|
two `BiStream`s. From the worker's perspective, the hub is the sole
|
||||||
|
consumer — the hub owns the resource and re-exposes it under its own
|
||||||
|
authority (the `forwarded_for` chain carries attribution, not
|
||||||
|
authority; ADR-032). From the downstream consumer's perspective, the
|
||||||
|
hub is the producer — it doesn't know or care that the real backend is
|
||||||
|
on a worker.
|
||||||
|
|
||||||
|
This is the same shape as the open-case relay (ADR-079) but initiated
|
||||||
|
from the producer side. The hub allocates `channel_id` on both legs,
|
||||||
|
records the mapping, and byte-forwards with `channel_id` rewrite.
|
||||||
|
|
||||||
|
**Single-producer, N-consumer.** The hub can proxy one producer's
|
||||||
|
stream to multiple consumers — e.g., a worker producing a live video
|
||||||
|
stream, with the hub re-streaming to N viewers. This is a future use
|
||||||
|
case (the channels wire format already supports it — channels are
|
||||||
|
independent, the hub just opens N consumer legs for one producer leg),
|
||||||
|
but the ownership and ACL model is the same: the hub owns the
|
||||||
|
producer's resource and controls which consumers may access it.
|
||||||
|
|
||||||
|
**The hard questions** — "hold until consumer connects" vs "reject if
|
||||||
|
no consumer," and the N-consumer fan-out semantics — are deferred with
|
||||||
|
the verb. The `direction` field is per-leg, not end-to-end; the relay
|
||||||
|
passes the verb through (`expose` → `expose`), same as in the open
|
||||||
|
case (`open` → `open`).
|
||||||
|
|
||||||
### The `channel_id` allocation symmetry
|
### The `channel_id` allocation symmetry
|
||||||
|
|
||||||
In both cases, `channel_id` allocation is by the responder (DP-1,
|
In both cases, `channel_id` allocation is by the responder (DP-1,
|
||||||
unchanged). In the open case, the responder is the spoke (spoke
|
unchanged). In the open case, the responder is the spoke (spoke
|
||||||
allocates). In the expose case, the responder is the hub on the
|
allocates). In the expose case, the responder is the hub on the
|
||||||
worker→hub leg (hub allocates) and the browser on the hub→browser
|
worker→hub leg (hub allocates) and the downstream consumer on the
|
||||||
leg (browser allocates). The hub relay records the mapping across
|
hub→consumer leg (consumer allocates). The hub relay records the
|
||||||
legs. This preserves ADR-073's "channel_id allocation is always by
|
mapping across legs. This preserves ADR-073's "channel_id allocation
|
||||||
the responder" invariant.
|
is always by the responder" invariant.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user