diff --git a/docs/architecture/crates/channels/README.md b/docs/architecture/crates/channels/README.md index 324f7c8..36624eb 100644 --- a/docs/architecture/crates/channels/README.md +++ b/docs/architecture/crates/channels/README.md @@ -40,7 +40,7 @@ protocol work itself. | [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Shutdown-on-Completion Pattern | The two-pump deadlock contract; handler-level, not channels-layer | | [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay — Translate, Not Transparently Forward | The hub translates channel 0, byte-forwards data channels with ID rewrite | | [080](../../decisions/080-channelclient.md) | ChannelClient — the Client Side of a Channels Connection | `ChannelClient`, QUIC-only; `AlknetClient` deferred (OQ-55) | -| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling) / hub / worker | +| [081](../../decisions/081-channels-subcrate-decomposition.md) | channels Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers, not sub-crates | | [070](../../decisions/070-bidistreamsource-trait.md) | BidiStreamSource Trait | The `Connection` extension point `ChannelBidiStreamSource` implements | | [065](../../decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | The transport-agnostic `Connection` the channels layer rides on | | [052](../../decisions/052-alknet-tty-wire-format-and-two-carriage.md) | alknet-tty Wire Format | The 5-byte format the 9-byte format generalizes (amended by ADR-077 — scoped to direct TTY) | diff --git a/docs/architecture/crates/channels/channels-wire.md b/docs/architecture/crates/channels/channels-wire.md index 3dbad48..5e80af7 100644 --- a/docs/architecture/crates/channels/channels-wire.md +++ b/docs/architecture/crates/channels/channels-wire.md @@ -48,8 +48,8 @@ stream_types are grouped in threes: | Data | 0 | write (client→server) | data in (stdin equivalent) | | | 1 | read (server→client) | data out (stdout equivalent) | | | 2 | read (server→client) | data err (stderr equivalent, optional) | -| Control | 3 | write (client→server) | control in (resize, signal, eof) | -| | 4 | read (server→client) | control out (exit, keepalive response) | +| Control | 3 | write (client→server) | control in (ALPN-specific format) | +| | 4 | read (server→client) | control out (ALPN-specific format) | | | 5 | read (server→client) | control err (optional) | | Future | 6/7/8 | write/read/read | next group, same pattern | | | ... | | | @@ -65,6 +65,13 @@ own flow control, its own EOF. Control is bidirectional via two halves (3 in, 4 out), not one shared stream both sides write to. This resolves the TTY control channel's "not actually bidirectional" flaw (ADR-077). +**Control payload format is ALPN-specific.** The channels layer is blind to +what stream_types 3/4/5 carry — it reassembles bytes and delivers them to +the handler. TTY happens to use JSON for its control channel; another ALPN +might use a binary format. The channels layer does not mandate JSON on +control stream_types, the same way it doesn't mandate a format for data +stream_types. + Not all channels use all sub-streams. The active set is declared at `channel/open` time (ADR-073 `stream_types` field) and fixed for the channel's lifetime. diff --git a/docs/architecture/crates/channels/overview.md b/docs/architecture/crates/channels/overview.md index d6b9a1f..de88c4a 100644 --- a/docs/architecture/crates/channels/overview.md +++ b/docs/architecture/crates/channels/overview.md @@ -105,18 +105,17 @@ alknet-channels-core alknet-channels-call ├── alknet-channels-core (ChannelManager, ChannelsAdapter, -│ ChannelBidiStreamSource) +│ ChannelBidiStreamSource, ChannelClient) ├── alknet-call (OperationRegistry, HandlerKind, make_handler, │ make_streaming_handler, CallError, ResponseEnvelope) └── tokio -alknet-channels-hub (or the hub role within alknet-hub) +alknet-hub (the existing hub crate — consumes channels) ├── alknet-channels-call └── alknet-call (from_call, CallAdapter, forwarded_for — ADR-079) -alknet-channels-worker (or the worker/client role) -├── alknet-channels-call -└── alknet-call (CallClient-equivalent) +worker crates (any crate that dials a hub — consumes channels) +└── alknet-channels-call (ChannelClient — ADR-080) ``` `alknet-channels-core` is the pure multiplexer — wire format, demux/mux, @@ -125,15 +124,17 @@ only. No `alknet-call` dependency. ALPN-blind, call-protocol-blind, transport-blind. This is where the "streams are streams" insight lives. `alknet-channels-call` is the call-protocol coupling — channel 0 -pre-negotiation as `alknet/call` (ADR-072) and the four lifecycle operations -(ADR-073) registered on the call protocol's `OperationRegistry`. This is -where the call-protocol coupling lives, isolated from the pure multiplexer. +pre-negotiation as `alknet/call` (ADR-072), the four lifecycle operations +(ADR-073) registered on the call protocol's `OperationRegistry`, and +`ChannelClient` (ADR-080). This is where the call-protocol coupling lives, +isolated from the pure multiplexer. -`alknet-channels-hub` and `alknet-channels-worker` (or the hub/worker roles -within `channels-call`) are the two roles: the relay (ADR-079) and the -`ChannelClient` (ADR-080). Whether these are separate sub-crates or -feature-gated modules within `channels-call` is a two-way-door packaging -decision (ADR-081). +The hub and worker are **consumers**, not sub-crates. The existing +`alknet-hub` crate IS the channels hub — it depends on `channels-call` and +uses channels as its substrate, with the relay logic (ADR-079) living in +`alknet-hub` alongside its existing peer lifecycle and service discovery +responsibilities. A worker is any crate that uses `ChannelClient` to dial. +There are no `channels-hub` or `channels-worker` sub-crates. See ADR-081 for the full decomposition rationale. @@ -249,7 +250,7 @@ All design decisions are documented as ADRs in [decisions/](../../decisions/). | [078](../../decisions/078-two-pump-shutdown-on-completion.md) | Two-Pump Pattern | Shutdown-on-completion contract; handler-level | | [079](../../decisions/079-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels with ID rewrite | | [080](../../decisions/080-channelclient.md) | ChannelClient | Client side; QUIC-only; `AlknetClient` deferred (OQ-55) | -| [081](../../decisions/081-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling) / hub / worker | +| [081](../../decisions/081-channels-subcrate-decomposition.md) | Sub-Crate Decomposition | `channels-core` (pure multiplexer) / `channels-call` (call coupling + ChannelClient); hub and worker are consumers | ## Open Questions diff --git a/docs/architecture/decisions/071-channels-wire-format.md b/docs/architecture/decisions/071-channels-wire-format.md index c195f1e..b3d2ed8 100644 --- a/docs/architecture/decisions/071-channels-wire-format.md +++ b/docs/architecture/decisions/071-channels-wire-format.md @@ -98,8 +98,8 @@ stream_types are grouped in threes: | Data | 0 | write (client→server) | data in (stdin equivalent) | | | 1 | read (server→client) | data out (stdout equivalent) | | | 2 | read (server→client) | data err (stderr equivalent, optional) | -| Control | 3 | write (client→server) | control in (resize, signal, eof) | -| | 4 | read (server→client) | control out (exit, keepalive response) | +| Control | 3 | write (client→server) | control in (ALPN-specific format) | +| | 4 | read (server→client) | control out (ALPN-specific format) | | | 5 | read (server→client) | control err (optional) | | Future | 6/7/8 | write/read/read | next group, same pattern | | | 9/10/11 | write/read/read | next group | @@ -120,6 +120,15 @@ isn't actually bidirectional" problem the TTY crate has today. The same principle applies to any future channel type — control is two halves, not one shared stream. +**Control payload format is ALPN-specific, not channels-enforced.** The +channels layer is blind to what stream_types 3/4/5 carry — it reassembles +bytes and delivers them to the handler. The TTY crate happens to use JSON +for its control channel (resize, signal, eof, exit) because its control +messages map cleanly to JSON; another ALPN might use a binary control +format. The channels layer does not mandate JSON on control stream_types. +This is the same ALPN-blindness principle that applies to the data +stream_types: the channels layer routes bytes, the handler interprets them. + ### Per-ALPN stream_type sets | ALPN | Active stream_types | Why | diff --git a/docs/architecture/decisions/081-channels-subcrate-decomposition.md b/docs/architecture/decisions/081-channels-subcrate-decomposition.md index b625efc..6a916c9 100644 --- a/docs/architecture/decisions/081-channels-subcrate-decomposition.md +++ b/docs/architecture/decisions/081-channels-subcrate-decomposition.md @@ -41,23 +41,27 @@ separable from the call-protocol orchestration (`channels-call`). ## Decision -### Three crates +### Two crates, not four ``` alknet-channels-core — the pure multiplexer (wire format, demux/mux, │ ChannelBidiStreamSource, ChannelManager). Depends │ on alknet-core only. ALPN-blind, call-protocol-blind, │ transport-blind. -├── alknet-channels-call — channel 0 pre-negotiation + lifecycle op -│ │ registrations on the call protocol's -│ │ OperationRegistry. Depends on channels-core + -│ │ alknet-call. -│ ├── alknet-channels-hub — the relay (ADR-079). Depends on -│ │ channels-call. The hub-side role. -│ └── alknet-channels-worker — ChannelClient (ADR-080). Depends on -│ channels-call. The client/worker-side role. +└── alknet-channels-call — channel 0 pre-negotiation + lifecycle op + registrations on the call protocol's + OperationRegistry. Depends on channels-core + + alknet-call. ``` +There are no `channels-hub` or `channels-worker` sub-crates. The hub and +worker are **consumers** of channels, not sub-crates of it. The existing +`alknet-hub` crate (`docs/architecture/crates/hub/README.md`) IS the hub — +it depends on `channels-call` and uses the channels protocol as its +substrate. A worker is just a worker — it depends on `channels-call` and +uses `ChannelClient` (ADR-080) to dial. The hub relay logic (ADR-079) lives +in `alknet-hub`, not in a channels sub-crate. + ### `alknet-channels-core` The pure multiplexer. Contains: @@ -95,31 +99,38 @@ The call-protocol coupling. Contains: registered on the call protocol's `OperationRegistry` at assembly time. - `ChannelOperations` (the registration helper that closes over a `ChannelManager` clone). +- `ChannelClient` (ADR-080) — the client-side type that dials a transport, + establishes the channels connection, and exposes `open_channel(alpn, + params) -> Channel`. This is the worker/client entry point; it lives here + because it needs channel 0 pre-negotiation (which is in `channels-call`). Depends on `channels-core` + `alknet-call`. This is where the call-protocol coupling lives, isolated from the pure multiplexer. -### `alknet-channels-hub` and `alknet-channels-worker` +### Hub and worker are consumers, not sub-crates -The two roles. These may be sub-crates or feature-gated modules within -`channels-call`; the exact packaging is a two-way-door implementation -detail. The contract is: +The hub and worker are architectural roles, not channels sub-crates: -- **`channels-hub`** (or the hub role): the relay (ADR-079). Translates - `channel/open` on channel 0, byte-forwards data channels with - `channel_id` rewrite. Depends on `channels-call` (for the call-protocol - translation) + the hub's own `CallAdapter` / `from_call` machinery. -- **`channels-worker`** (or the worker/client role): `ChannelClient` - (ADR-080). Dials a transport, establishes the channels connection, runs - the demux/mux, exposes `open_channel(alpn, params) -> Channel`. Depends - on `channels-call` (for channel 0 pre-negotiation) + `alknet-call`'s - `CallClient`-equivalent. +- **The hub** is the existing `alknet-hub` crate. It depends on + `channels-call` and uses the channels protocol as its substrate. The hub + relay logic (ADR-079 — translate `channel/open` on channel 0, + byte-forward data channels with `channel_id` rewrite) lives in + `alknet-hub`, alongside its existing peer lifecycle, aggregated env, and + service discovery responsibilities. There is no `channels-hub` sub-crate; + `alknet-hub` IS the channels hub. -The naming (hub/worker vs server/client) is a two-way-door detail. The -user noted "server/client" is muddy because a hub/worker can act as both -depending on the use case (bidirectionality — ADR-073 §direction -semantics). The roles are "the side that relays" (hub) and "the side that -dials" (worker/client), but both can open channels in either direction. +- **A worker** is any crate that uses `ChannelClient` (ADR-080, in + `channels-call`) to dial a hub. There is no `channels-worker` sub-crate; + a worker depends on `channels-call` and uses `ChannelClient` directly. + The worker may be a CLI binary, a docker-side connector, an SSH-side + connector, or any other role that dials into a hub's channels connection. + +This means the channels crate provides the substrate (`channels-core` + +`channels-call`); the hub and worker crates are consumers that build on it. +The dependency direction is: `alknet-hub` → `channels-call` → +`channels-core` → `alknet-core`; a worker → `channels-call` → +`channels-core` → `alknet-core`. The channels crate has no dependency on +`alknet-hub` or any worker crate. ### What moves where @@ -134,22 +145,24 @@ dials" (worker/client), but both can open channels in either direction. | Channel 0 pre-negotiation | `alknet-channels` (ADR-072) | `channels-call` | | `channel/open`/`close`/`control`/`resources/subscribe` ops | `alknet-channels` (ADR-073) | `channels-call` | | `ChannelOperations` registration helper | `alknet-channels` | `channels-call` | -| Hub relay (ADR-079) | `alknet-channels` (spec) / `alknet-hub` (impl) | `channels-hub` (or `alknet-hub` — see below) | -| `ChannelClient` (ADR-080) | `alknet-channels` | `channels-worker` | +| `ChannelClient` (ADR-080) | `alknet-channels` | `channels-call` | +| Hub relay (ADR-079) | `alknet-channels` (spec) | `alknet-hub` (the existing hub crate, consuming `channels-call`) | ### Relationship to `alknet-hub` The existing `alknet-hub` crate (`docs/architecture/crates/hub/README.md`) -is the hub pattern: peer lifecycle, aggregated env, service discovery. The -channels hub relay (ADR-079) is a channels-specific concern that the hub -crate consumes. The split: `channels-hub` (or the hub role within -`channels-call`) provides the relay logic; `alknet-hub` wires it into the -hub runtime alongside the call-protocol peer management. Whether the relay -lives in a `channels-hub` sub-crate or directly in `alknet-hub` is a -packaging decision — the contract (ADR-079) is the same either way. The -user's preference for decomposing channels into core/hub/worker suggests a -`channels-hub` sub-crate that `alknet-hub` depends on, but this is not -one-way and can be revisited during implementation. +is the hub pattern: peer lifecycle, aggregated env, service discovery. With +channels as the substrate, `alknet-hub` gains a dependency on +`channels-call` and incorporates the relay logic (ADR-079). The hub spec +(`crates/hub/README.md`) will be updated to reflect that the hub uses +channels as its transport substrate — one channels connection per leg +(browser↔hub, hub↔spoke), with the relay translating `channel/open` and +byte-forwarding data channels. The hub's existing responsibilities (peer +lifecycle, aggregated env, service discovery, worker supervision) are +unchanged; channels is the substrate they run on. + +This makes "channels hub" and "hub" the same thing — the hub IS built on +channels. There is no separate channels-hub concept. ## Consequences @@ -162,20 +175,22 @@ one-way and can be revisited during implementation. `channels-core` doesn't know about `alknet-call`, `alknet-tty`, or any handler crate. The call-protocol coupling is a channels-crate concern, not a downstream-crate concern. -- The sub-crate split matches the existing pattern (`alknet-tty` + - `alknet-tty-local`, `alknet-docker` + its `tty` feature) — core in one - crate, consumer-specific wiring in another. -- The hub and worker roles are separated, matching the user's - decomposition preference and the bidirectionality of the channels - protocol (both sides can open channels; the roles are about who relays - vs who dials, not about request/response direction). +- Hub and worker are consumers, not sub-crates. The existing `alknet-hub` + crate IS the channels hub — it depends on `channels-call` and uses + channels as its substrate. A worker depends on `channels-call` and uses + `ChannelClient`. The channels crate has no dependency on `alknet-hub` or + any worker crate. This is the cleanest dependency direction: channels + provides the substrate; hub and worker consume it. +- The WASM and cross-platform story gets easier: `channels-core` is + WASM-compatible by construction (pure byte manipulation, no platform + deps); `channels-call` inherits the call protocol's WASM constraints; the + hub and worker crates are platform-specific as needed. **Negative:** -- Three crates instead of one. The assembly layer must depend on - `channels-core` + `channels-call` (and optionally `channels-hub` or - `channels-worker`) instead of one `alknet-channels`. This is the cost of - the clean separation; the assembly layer already wires multiple crates, - so this is consistent with the existing pattern. +- Two channels crates instead of one. The assembly layer must depend on + `channels-core` + `channels-call` instead of one `alknet-channels`. This + is the cost of the clean separation; the assembly layer already wires + multiple crates, so this is consistent with the existing pattern. - The `ChannelsAdapter` in `channels-core` doesn't preinstall channel 0 — the consumer does. This means `channels-core`'s `ChannelsAdapter::handle` exposes a hook (callback or trait method) for the consumer to install @@ -186,12 +201,11 @@ one-way and can be revisited during implementation. ## Door type -**One-way (crate structure) + two-way (role packaging).** The three-crate -split (`channels-core` / `channels-call` / roles) is one-way — once -consumers depend on `channels-core` without `channels-call`, re-merging -them is a breaking change. The hub/worker packaging (separate sub-crates -vs feature-gated modules within `channels-call`) is two-way — an -implementation detail that can change without breaking the contract. +**One-way (crate structure).** The two-crate split (`channels-core` / +`channels-call`) is one-way — once consumers depend on `channels-core` +without `channels-call`, re-merging them is a breaking change. The hub and +worker being consumers (not sub-crates) is also one-way — it establishes +the dependency direction (hub/worker → channels, not channels → hub/worker). ## References