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
|
||||
design was doing the most implicit work." Walking it under "call++"
|
||||
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"}}`
|
||||
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
|
||||
`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 })`.
|
||||
Browser is the initiator / consumer.
|
||||
Consumer is the initiator / consumer.
|
||||
3. Hub's `CallAdapter` recognizes the `channel_open` marker. The hub
|
||||
does NOT run a local `TtyAdapter` — the hub never runs
|
||||
protocol-specific handlers (ADR-079). It forwards to the spoke via
|
||||
`from_call`: hub re-issues `channels/tty/open` on the spoke leg
|
||||
with `forwarded_for = browser`.
|
||||
4. Spoke's `CallAdapter` receives `channels/tty/open`.
|
||||
protocol-specific handlers (ADR-079). It forwards to the producer
|
||||
(spoke/worker) via `from_call`: hub re-issues `channels/tty/open`
|
||||
on the producer leg with `forwarded_for = consumer`.
|
||||
4. Producer's `CallAdapter` receives `channels/tty/open`.
|
||||
`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`
|
||||
(per ADR-050). The spoke consults
|
||||
(per ADR-050). The producer consults
|
||||
`ChannelLifecyclePolicy::check_open(hub)` (Gap 2 — keyed by
|
||||
direct caller, opener recorded in the per-connection ledger).
|
||||
5. Spoke's `ChannelCore` allocates `channel_id`, spawns `TtyAdapter`
|
||||
on the channel's `BiStream` with the docker backend, records
|
||||
opener (hub) in the ledger, returns `{channel_id}`.
|
||||
6. Hub receives the spoke's `{channel_id}`, opens a matching channel
|
||||
on the browser's side (hub is the responder for the browser leg),
|
||||
records the `channel_id` mapping `browser_id ↔ spoke_id`, returns
|
||||
`{channel_id: browser_id}` to the browser.
|
||||
7. Hub byte-forwards between `browser_id` and `spoke_id` with 4-byte
|
||||
`channel_id` rewrite (ADR-079 unchanged).
|
||||
5. Producer's `ChannelCore` allocates `channel_id`, spawns
|
||||
`TtyAdapter` on the channel's `BiStream` with the docker backend,
|
||||
records opener (hub) in the ledger, returns `{channel_id}`.
|
||||
6. Hub receives the producer's `{channel_id}`, opens a matching
|
||||
channel on the consumer's side (hub is the responder for the
|
||||
consumer leg), records the `channel_id` mapping
|
||||
`consumer_id ↔ producer_id`, returns
|
||||
`{channel_id: consumer_id}` to the consumer.
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
specced; the expose case is for push scenarios (a worker pushes a log
|
||||
stream to a monitoring browser that's already connected) and will be
|
||||
specced when a concrete consumer forces the design.
|
||||
specced; the expose case is for push scenarios where a worker offers
|
||||
resources to a hub, which then acts as a proxy — re-exposing them to
|
||||
other consumers.
|
||||
|
||||
Sketch: worker sends `channels/tty/expose` on its channel 0. Hub
|
||||
checks `access_control` against the worker's identity (separate ACL
|
||||
from `channels/tty/open`). Hub recognizes the `channel_open` marker
|
||||
and must relay to a connected browser that wants to consume. The hub
|
||||
initiates `channels/tty/expose` on the browser leg (hub is
|
||||
producer transparently — it forwards the data plane to the worker).
|
||||
Hub allocates `channel_id` on both legs, records the mapping, and
|
||||
byte-forwards with `channel_id` rewrite.
|
||||
**Concrete example.** A worker runs on a remote instance (vastai,
|
||||
runpod, a docker container). It wraps an opencode server's OpenAPI spec
|
||||
via `from_openapi` (call ops, JSON) and also registers
|
||||
`channels/tty/open` for terminal access. The worker connects to a hub
|
||||
and exposes both: the call ops and the TTY channel. The hub consumes
|
||||
these — the worker's resources become the hub's resources (ownership
|
||||
model: the hub owns what the worker exposes). Another consumer (a
|
||||
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
|
||||
consumer" — is 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 hub-as-proxy pattern.** The hub is a consumer of the worker's
|
||||
resource and a producer for downstream consumers. The relay is
|
||||
protocol-agnostic: swap channel IDs, tunnel reads/writes between the
|
||||
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
|
||||
|
||||
In both cases, `channel_id` allocation is by the responder (DP-1,
|
||||
unchanged). In the open case, the responder is the spoke (spoke
|
||||
allocates). In the expose case, the responder is the hub on the
|
||||
worker→hub leg (hub allocates) and the browser on the hub→browser
|
||||
leg (browser allocates). The hub relay records the mapping across
|
||||
legs. This preserves ADR-073's "channel_id allocation is always by
|
||||
the responder" invariant.
|
||||
worker→hub leg (hub allocates) and the downstream consumer on the
|
||||
hub→consumer leg (consumer allocates). The hub relay records the
|
||||
mapping across legs. This preserves ADR-073's "channel_id allocation
|
||||
is always by the responder" invariant.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in new issue
Block a user