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:
deepseek-v4-pro committed 2026-07-19 16:33:17 +00:00
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.
--- ---