findings: reframe — channels is call with a binary data plane, not 'call + data channels'; retire 'call++' for 'binary-stream call apps'; two modes: default call (JSON) and channels (binary framing)

This commit is contained in:
deepseek-v4-pro committed 2026-07-19 17:09:16 +00:00
1 parent 8ca2d0632d
commit 90a8fa7328
1 file changed
+99 -81
@@ -27,9 +27,10 @@ ADR-093 and is not re-litigated here.
**Origin:** An outside review surfaced three high-value gaps in the channels control plane **Origin:** An outside review surfaced three high-value gaps in the channels control plane
after ADR-094 (per-identity channel cap) landed. Working through the after ADR-094 (per-identity channel cap) landed. Working through the
first gap — `channel/open` ACL granularity — surfaced a larger first gap — `channel/open` ACL granularity — surfaced a larger
unification: channels is "call + data channels" ("call++"), and the unification: channels is call with a binary data plane — the same
ALPN crates served under channels are call-consuming apps in the same protocol, two chunk formats (JSON vs binary). The ALPN crates served
shape alknet-docker is a call-consuming app. This doc records both the under channels are call apps in the same shape alknet-docker is a call
app. This doc records both the
gaps and the unification. gaps and the unification.
--- ---
@@ -40,13 +41,14 @@ The previous framing — "channel lifecycle goes through one generic
`channel/open` operation, and `AccessControl::check` on that op is `channel/open` operation, and `AccessControl::check` on that op is
the ACL" — was a symptom. The actual question is the separation of the ACL" — was a symptom. The actual question is the separation of
concerns between the call protocol (which already has per-op ACL, concerns between the call protocol (which already has per-op ACL,
identity, composition, ownership) and the channels layer (which routes identity, composition, ownership, and subscriptions) and the channels
bytes by `channel_id`), and the resolution is **an openable ALPN is an layer (which provides binary framing for ops whose response isn't
operation**: each openable ALPN registers its own open op JSON), and the resolution is **an openable ALPN is an operation**:
each openable ALPN registers its own open op
(`channels/tty/open`, `channels/tunnel/open`, etc.) on the call (`channels/tty/open`, `channels/tunnel/open`, etc.) on the call
`OperationRegistry`, with its own `access_control`, `input_schema`, `OperationRegistry`, with its own `access_control`, `input_schema`,
`resource_id_path`, and a `channel_open` marker that tells `resource_id_path`, and a `channel_open` marker that tells the call
channels-call "this op produces a data channel." adapter "this op's response is a binary stream, not a JSON response."
This dissolves the `channel/open` ACL granularity gap (each ALPN has This dissolves the `channel/open` ACL granularity gap (each ALPN has
its own ACL — checked by the existing `OperationRegistry::invoke` its own ACL — checked by the existing `OperationRegistry::invoke`
@@ -61,15 +63,27 @@ them — a worker exposing a resource and a consumer opening it are
matched by `(op_name, params_hash)` on the hub, which proxies the matched by `(op_name, params_hash)` on the hub, which proxies the
data plane between them. data plane between them.
**Channels is call with a binary data plane.** The call protocol
already has the subscription model, ACL, identity, composition, and
ownership. Channels adds one thing: binary framing (8-byte headers,
channel multiplexing) for ops whose response isn't JSON. A call
connection has two modes:
- **Default call** (`alknet/call`): all ops return JSON on a single
stream. The subscription model works — events are JSON chunks.
- **Channels** (`alknet/channels`): call on channel 0 (JSON control
plane), binary streams on channels 1..N (data plane). The
subscription model works identically — the only difference is the
chunk format on the data-plane channels.
The ALPN crates served under channels (tty, tunnel, socks5, fs, sftp) The ALPN crates served under channels (tty, tunnel, socks5, fs, sftp)
stop being "just ALPNs" and become **call-consuming apps with a stop being "just ALPNs" and become **call apps with binary-stream
data-channel data plane** — the same shape as alknet-docker (a ops** — the same shape as alknet-docker (a call app with JSON ops),
call-consuming app with a JSON data plane), but where some ops but where some ops carry the `channel_open` marker and produce a
produce a data channel alongside the JSON response. We call this binary stream instead of a JSON response. The hub/worker composition
"call++" — channels is call + data channels. The hub/worker story unifies: a hub composes call apps, full stop; some ops return
composition story unifies: a hub composes call apps, full stop; some JSON, some ops carry the `channel_open` marker and produce a binary
ops return JSON, some ops carry the `channel_open` marker and stream.
produce a data channel.
Three further gaps fall out of this framing: Three further gaps fall out of this framing:
@@ -97,12 +111,13 @@ Three further gaps fall out of this framing:
3. **ALPN category reframe (ADR-086 amendment).** ADR-086 §4 split 3. **ALPN category reframe (ADR-086 amendment).** ADR-086 §4 split
the foundational handlers into "channels data-channel ALPNs" and the foundational handlers into "channels data-channel ALPNs" and
"SSH (endpoint ALPN wrapping channels)." Under "call++", the first "SSH (endpoint ALPN wrapping channels)." Under the unified model,
category is reframed: they're not "ALPNs gated by channels" — the first category is reframed: they're not "ALPNs gated by
they're **call apps that produce data channels**. They inherit channels" — they're **call apps with binary-stream ops**. They
call's auth/composition/identity by construction (they *are* call inherit call's auth/composition/identity by construction (they
apps); the data-channel part is the `channel_open` marker on the *are* call apps); the binary-stream part is the `channel_open`
op spec. The "flat ALPN list" re-implementation problem dissolves marker on the op spec. The "flat ALPN list" re-implementation
problem dissolves
because they're call apps, not a separate category that each because they're call apps, not a separate category that each
re-implements auth. SSH stays distinct (endpoint ALPN wrapping re-implements auth. SSH stays distinct (endpoint ALPN wrapping
channels). channels).
@@ -153,10 +168,10 @@ adds:
|-------|-------------------|-------|--------| |-------|-------------------|-------|--------|
| Endpoint accept | May this identity hold N connections? | `alknet-endpoint` | **Unowned (Gap 3).** ADR-094 explicitly says connections are unbounded. New OQ against `alknet-endpoint`. | | Endpoint accept | May this identity hold N connections? | `alknet-endpoint` | **Unowned (Gap 3).** ADR-094 explicitly says connections are unbounded. New OQ against `alknet-endpoint`. |
| `channels-core` | Byte routing; per-connection memory bounds | ADR-075/076 | Decided, coherent. Auth-blind by design (ADR-075). | | `channels-core` | Byte routing; per-connection memory bounds | ADR-075/076 | Decided, coherent. Auth-blind by design (ADR-075). |
| `channels-call`, op-level | May you call `channel/open` at all? | `AccessControl::check` | Decided. (Under "call++": per-ALPN ops, each with its own ACL — Gap 1 resolved.) | | `channels-call`, op-level | May you call `channel/open` at all? | `AccessControl::check` | Decided. (Under the unified model: per-ALPN ops, each with its own ACL — Gap 1 resolved.) |
| `channels-call`, quota | How many slots may you hold? | ADR-094 | Decided, lifecycle buggy (Gap 2). Trait shape survives; the change is where `on_close` is called from and where the opener identity comes from. | | `channels-call`, quota | How many slots may you hold? | ADR-094 | Decided, lifecycle buggy (Gap 2). Trait shape survives; the change is where `on_close` is called from and where the opener identity comes from. |
| `channels-call`, per-ALPN/direction | May you open this ALPN, this direction? | Per-ALPN op's `access_control` (this doc) | **Resolved by "call++".** Each openable ALPN is its own op; the op's `access_control` is the per-ALPN/direction ACL. | | `channels-call`, per-ALPN/direction | May you open this ALPN, this direction? | Per-ALPN op's `access_control` (this doc) | **Resolved.** Each openable ALPN is its own op; the op's `access_control` is the per-ALPN/direction ACL. |
| ALPN handler (data plane) | May you touch container abc123? | ADR-050 ownership, handler-internal | Decided (call/docker land). Unchanged under "call++." | | ALPN handler (data plane) | May you touch container abc123? | ADR-050 ownership, handler-internal | Decided (call/docker land). Unchanged under the unified model. |
The tangle was that layers 5 and 6 got blurred ("ACL is checked on The tangle was that layers 5 and 6 got blurred ("ACL is checked on
`channel/open`" is true but only for layer 3, because the single `channel/open`" is true but only for layer 3, because the single
@@ -256,26 +271,26 @@ ALPNs" (gated by channels, opened via `channel/open`, inherit ACL +
bidirectionality) and "SSH" (endpoint ALPN wrapping channels). The bidirectionality) and "SSH" (endpoint ALPN wrapping channels). The
"channels data-channel ALPNs" framing implies they're a separate kind "channels data-channel ALPNs" framing implies they're a separate kind
of thing that channels happens to gate — and each would re-implement of thing that channels happens to gate — and each would re-implement
auth in a flat ALPN list. Under "call++," they're not a separate auth in a flat ALPN list. Under the unified model, they're not a
category: they're call apps that produce data channels. They inherit separate category: they're call apps with binary-stream ops. They
call's auth by construction (they *are* call apps). The gating is a inherit call's auth by construction (they *are* call apps). The gating
consequence, not the definition. is a consequence, not the definition.
This also touches the HTTP/ WebSocket path: ADR-048 says "WebSocket This also touches the HTTP/ WebSocket path: ADR-048 says "WebSocket
carries the native call-protocol session." Under "call++," a browser carries the native call-protocol session." Under the unified model, a
that wants to open a TTY channel needs to call `channels/tty/open`, browser that wants to open a TTY channel needs to call
which lives on channel 0 inside a *channels* connection. So WebSocket `channels/tty/open`, which lives on channel 0 inside a *channels*
may carry either `alknet/call` (bare, for call-only clients) or connection. So WebSocket may carry either `alknet/call` (bare, for
`alknet/channels` (8-byte chunk framing, with call on channel 0 call-only clients) or `alknet/channels` (8-byte chunk framing, with
inside). Channels framing is required for data-channel clients; a call on channel 0 inside). Channels framing is required for
call-only client (e.g. a dashboard doing docker JSON ops) can use binary-stream clients; a call-only client (e.g. a dashboard doing
bare `alknet/call` over WebSocket. OQ-65 ("WebSocket carrying docker JSON ops) can use bare `alknet/call` over WebSocket. OQ-65
channels, not just call?") is resolved by "call++": WebSocket may ("WebSocket carrying channels, not just call?") is resolved: WebSocket
carry either; channels required for data channels. may carry either; channels required for binary streams.
--- ---
## The resolution: openable ALPNs are operations ("call++") ## The resolution: openable ALPNs are operations
The core observation: ADR-073's claim is "no new auth machinery — The core observation: ADR-073's claim is "no new auth machinery —
channel lifecycle goes through the existing `AccessControl::check`." channel lifecycle goes through the existing `AccessControl::check`."
@@ -307,8 +322,8 @@ pub struct OperationSpec {
// ... existing fields ... // ... existing fields ...
pub access_control: AccessControl, pub access_control: AccessControl,
pub resource_id_path: Option<String>, pub resource_id_path: Option<String>,
/// Marker consumed by layers that manage data channels. When set, /// Marker consumed by layers that manage binary streams. When set,
/// the op produces a data channel alongside (or instead of) the JSON /// the op produces a binary stream alongside (or instead of) the JSON
/// response. The marker is registry metadata, not auth machinery — /// response. The marker is registry metadata, not auth machinery —
/// it's how layers like channels-call know an op is a channel-open /// it's how layers like channels-call know an op is a channel-open
/// op, parallel to how `resource_id_path` is how ADR-050 knows where /// op, parallel to how `resource_id_path` is how ADR-050 knows where
@@ -358,7 +373,8 @@ forwarding stub. ADR-022's provenance table (leaves are forwarding
stubs, no composition authority) gets a note for this case: a stubs, no composition authority) gets a note for this case: a
`FromCall`-imported marked op is a leaf for composition purposes but `FromCall`-imported marked op is a leaf for composition purposes but
carries relay machinery that allocates channels and spawns byte-forward carries relay machinery that allocates channels and spawns byte-forward
tasks. This is the load-bearing piece of the relay under call++. tasks. This is the load-bearing piece of the relay under the unified
model.
**Marked ops invoked outside a channels session.** `channels/tty/open` **Marked ops invoked outside a channels session.** `channels/tty/open`
is registered on the call registry — which means it's also is registered on the call registry — which means it's also
@@ -367,7 +383,7 @@ there is no `ChannelManager` and no data plane. The wrapper resolves
`OperationEnv::channel_manager()` at invocation time; if it returns `OperationEnv::channel_manager()` at invocation time; if it returns
`None`, the wrapper returns `channel:no_channels_session`. Relatedly, `None`, the wrapper returns `channel:no_channels_session`. Relatedly,
the HTTP-side adapters (`to_openapi`, `to_mcp`) must exclude marked the HTTP-side adapters (`to_openapi`, `to_mcp`) must exclude marked
ops — "produces a data channel" is not expressible over a ops — "produces a binary stream" is not expressible over a
request/response export. request/response export.
### Two verbs: `open` and `expose` ### Two verbs: `open` and `expose`
@@ -466,7 +482,7 @@ correctly.
### The discovery split ### The discovery split
Under "call++", the discovery question splits cleanly: Under the unified model, the discovery question splits cleanly:
- **"What may I open"** (static, per-op): `services/list` (visibility- - **"What may I open"** (static, per-op): `services/list` (visibility-
filtered + `AccessControl::check(calling_peer_identity)` server-side, filtered + `AccessControl::check(calling_peer_identity)` server-side,
@@ -499,34 +515,35 @@ lives in the ALPN crate.
## The ALPN three-category reframe (ADR-086 amendment) ## The ALPN three-category reframe (ADR-086 amendment)
ADR-086 §4 split the foundational handlers into two categories. Under ADR-086 §4 split the foundational handlers into two categories. Under
"call++", the first category is reframed. The three categories become: the unified model, the first category is reframed. The three categories
become:
| Category | TLS-layer? | Identity at TLS? | How they're reached | Examples | | Category | TLS-layer? | Identity at TLS? | How they're reached | Examples |
|----------|-----------|------------------|---------------------|----------| |----------|-----------|------------------|---------------------|----------|
| **Entry points** | yes | no | TLS ALPN negotiation | `h2`, `http/1.1`, `alknet/register` | | **Entry points** | yes | no | TLS ALPN negotiation | `h2`, `http/1.1`, `alknet/register` |
| **Endpoints** | yes | yes | TLS ALPN negotiation | `alknet/channels`, `alknet/call`, `alknet/ssh` | | **Endpoints** | yes | yes | TLS ALPN negotiation | `alknet/channels`, `alknet/call`, `alknet/ssh` |
| **Call++ apps** (was "channels data-channel ALPNs") | no | n/a (inside channels) | `channels/<alpn>/open` / `expose` op on channel 0 | `alknet/tty`, `alknet/tunnel`, `alknet/socks5`, `alknet/fs`, `alknet/sftp` | | **Binary-stream call apps** (was "channels data-channel ALPNs") | no | n/a (inside channels) | `channels/<alpn>/open` / `expose` op on channel 0 | `alknet/tty`, `alknet/tunnel`, `alknet/socks5`, `alknet/fs`, `alknet/sftp` |
The third category changes description. Previously "ALPNs gated by The third category changes description. Previously "ALPNs gated by
channels" (implying they're a separate kind of thing that channels channels" (implying they're a separate kind of thing that channels
happens to gate, and each re-implements auth in a flat ALPN list). happens to gate, and each re-implements auth in a flat ALPN list).
Now "call apps that produce data channels" (they *are* call apps; Now "call apps with binary-stream ops" (they *are* call apps; they
they inherit call's auth/composition/identity by construction; the inherit call's auth/composition/identity by construction; the
data-channel part is the `channel_open` marker). The gating is a binary-stream part is the `channel_open` marker). The gating is a
consequence, not the definition. consequence, not the definition.
**Direct registration remains possible.** The category table says **Direct registration remains possible.** The category table says
call++ apps are not TLS-layer ALPNs, but the `ProtocolHandler` is binary-stream call apps are not TLS-layer ALPNs, but the
still usable by both direct connections (`HandlerRegistry` → `ProtocolHandler` is still usable by both direct connections
`ProtocolHandler` → `BiStream`) and channels-opened sessions. ADR-077's (`HandlerRegistry` → `ProtocolHandler` → `BiStream`) and
two-mode survives at the mechanism level; the canonical composition is channels-opened sessions. ADR-077's two-mode survives at the mechanism
call++. The table describes the canonical path, not a prohibition on level; the canonical composition is through the call protocol. The
direct use. table describes the canonical path, not a prohibition on direct use.
**Naming.** "Call++ apps" is the mental model. "Channels-served **Naming.** "Binary-stream call apps" is the working name.
ALPNs" is descriptive. The final naming is a separate (cosmetic) "Channels-served ALPNs" is descriptive. The final naming is a separate
decision tracked as an OQ; this doc uses "call++ apps" as the working (cosmetic) decision tracked as an OQ; this doc uses "binary-stream
name. call apps" as the working name.
### What this means for the ALPN crates (the lineage) ### What this means for the ALPN crates (the lineage)
@@ -537,7 +554,7 @@ docker surfaced TTY (exec needs a terminal). Working TTY surfaced
channels (terminal output isn't JSON). The loop closes: the ALPN channels (terminal output isn't JSON). The loop closes: the ALPN
crates that channels serves become channels-consuming apps in the crates that channels serves become channels-consuming apps in the
same shape docker is a call-consuming app. The only difference is same shape docker is a call-consuming app. The only difference is
that some ops produce a data channel instead of (or alongside) a that some ops produce a binary stream instead of (or alongside) a
JSON response. JSON response.
The dependency split parallels `channels-core` / `channels-call` The dependency split parallels `channels-core` / `channels-call`
@@ -559,8 +576,8 @@ point is the dependency boundary.
### SSH stays distinct ### SSH stays distinct
SSH is an endpoint ALPN that wraps channels. Under "call++", the SSH is an endpoint ALPN that wraps channels. Under the unified model,
channels inside SSH have call on channel 0, and the call registry the channels inside SSH have call on channel 0, and the call registry
has the open ops. An SSH client opens an SSH channel; the SSH server has the open ops. An SSH client opens an SSH channel; the SSH server
translates that to `channels/<alpn>/open` on channel 0 internally translates that to `channels/<alpn>/open` on channel 0 internally
(SSH server as translator, same shape as the hub relay — ADR-079). (SSH server as translator, same shape as the hub relay — ADR-079).
@@ -574,8 +591,8 @@ holds.
## The hub-relay 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 the unified
to verify it holds. model to verify it holds.
### Consumer → hub → producer, "open me a TTY" (the common case) ### Consumer → hub → producer, "open me a TTY" (the common case)
@@ -710,13 +727,13 @@ is always by the responder" invariant.
| ADR | Scope | Status | | ADR | Scope | Status |
|-----|-------|--------| |-----|-------|--------|
| **ADR-095 (new)** | "Openable ALPNs are operations" — the call++ design. The mental model, the `channel_open` marker on `OperationSpec`, the `ChannelCore` seam (wrapper shape, POC-flagged), the two-verb split, the discovery split, the three-category ALPN reframe. The unifying ADR. | **Ready to draft.** | | **ADR-095 (new)** | "Openable ALPNs are operations" — channels is call with a binary data plane. The mental model, the `channel_open` marker on `OperationSpec`, the `ChannelCore` seam (wrapper shape, POC-flagged), the two-verb split, the subscription-based expose/open matching, the discovery split, the three-category ALPN reframe. The unifying ADR. | **Ready to draft.** |
| **ADR-073 amendment** | `channel/open` dissolves into per-ALPN ops in `channels/<alpn>/open` and `channels/<alpn>/expose`. `channel/close`, `channel/control`, `channel/resources/subscribe` stay generic (keyed by `channel_id`). The `direction` field is removed (becomes the verb). Error codes: `channel:unknown_alpn` becomes "operation not found"; `channel:invalid_params` becomes ordinary schema rejection. | **Ready to draft.** | | **ADR-073 amendment** | `channel/open` dissolves into per-ALPN ops in `channels/<alpn>/open` and `channels/<alpn>/expose`. `channel/close`, `channel/control`, `channel/resources/subscribe` stay generic (keyed by `channel_id`). The `direction` field is removed (becomes the verb). Error codes: `channel:unknown_alpn` becomes "operation not found"; `channel:invalid_params` becomes ordinary schema rejection. | **Ready to draft.** |
| **ADR-094 amendment (Gap 2)** | The per-connection opener ledger in `channels-call`. The decrement is keyed by the opener (from the ledger), not the closer. The decrement is called from every teardown path (close received, close sent, handler exit, connection drop), not just `channel/close`. The trait shape (`check_open`, `on_close`) survives. The teardown hooks (connection-drop, handler-exit) are new structural requirements on `channels-call`. | **Ready to draft.** | | **ADR-094 amendment (Gap 2)** | The per-connection opener ledger in `channels-call`. The decrement is keyed by the opener (from the ledger), not the closer. The decrement is called from every teardown path (close received, close sent, handler exit, connection drop), not just `channel/close`. The trait shape (`check_open`, `on_close`) survives. The teardown hooks (connection-drop, handler-exit) are new structural requirements on `channels-call`. | **Ready to draft.** |
| **ADR-086 §4 amendment** | "Channels data-channel ALPNs" → "call++ apps" (or whatever the naming OQ settles). The category distinction holds (them vs SSH); the description changes from "gated by channels" to "call apps that produce data channels." | **Ready to draft.** | | **ADR-086 §4 amendment** | "Channels data-channel ALPNs" → "binary-stream call apps" (or whatever the naming OQ settles). The category distinction holds (them vs SSH); the description changes from "gated by channels" to "call apps with binary-stream ops." | **Ready to draft.** |
| **ADR-048 amendment + OQ-65 resolution** | WebSocket may carry either `alknet/call` (bare, for call-only clients) or `alknet/channels` (8-byte chunk framing, call on channel 0 inside). Channels framing required for data-channel clients. OQ-65 resolved. "Native session, not gateway" survives (the decision). | **Ready to draft.** | | **ADR-048 amendment + OQ-65 resolution** | WebSocket may carry either `alknet/call` (bare, for call-only clients) or `alknet/channels` (8-byte chunk framing, call on channel 0 inside). Channels framing required for binary-stream clients. OQ-65 resolved. "Native session, not gateway" survives (the decision). | **Ready to draft.** |
| **ADR-057 amendment** | TTY data plane stays call-free; control plane (open/expose ops) depends on call. "Self-contained negotiation framing" becomes the data-plane negotiation (the 5-byte format's negotiation frame); the control-plane negotiation is the call op. | **Ready to draft.** | | **ADR-057 amendment** | TTY data plane stays call-free; control plane (open/expose ops) depends on call. "Self-contained negotiation framing" becomes the data-plane negotiation (the 5-byte format's negotiation frame); the control-plane negotiation is the call op. | **Ready to draft.** |
| **ADR-058 clarification** | The boundary criterion (EventEnvelope-compatible → call op; incompatible → data channel with call control plane) is preserved and sharpened by "call++". Probably a note, not a full amendment. | **Ready to draft.** | | **ADR-058 clarification** | The boundary criterion (EventEnvelope-compatible → call op; incompatible → binary stream with call control plane) is preserved and sharpened. Probably a note, not a full amendment. | **Ready to draft.** |
| **New OQ (Gap 3)** | Per-identity connection cap against `alknet-endpoint`. Named, `deferred(scope)` — the deployment shape that needs it isn't concrete yet. Owned by `alknet-endpoint`, not channels. | **Ready to open.** | | **New OQ (Gap 3)** | Per-identity connection cap against `alknet-endpoint`. Named, `deferred(scope)` — the deployment shape that needs it isn't concrete yet. Owned by `alknet-endpoint`, not channels. | **Ready to open.** |
--- ---
@@ -758,7 +775,7 @@ is always by the responder" invariant.
design and stays that way. The per-connection opener ledger lives design and stays that way. The per-connection opener ledger lives
in `channels-call`, not `channels-core`. in `channels-call`, not `channels-core`.
### `alknet-tty` (the first call++ app) ### `alknet-tty` (the first binary-stream call app)
- Data plane (the `TtyAdapter`, the 5-byte wire format, the - Data plane (the `TtyAdapter`, the 5-byte wire format, the
`TtyBackend` trait) is unchanged. Used by both direct connections `TtyBackend` trait) is unchanged. Used by both direct connections
@@ -793,17 +810,17 @@ is always by the responder" invariant.
plane (open/expose ops) new. Each registers its open ops on the plane (open/expose ops) new. Each registers its open ops on the
call registry at assembly time. call registry at assembly time.
- These crates are not yet specced (per ADR-085). When specced, they - These crates are not yet specced (per ADR-085). When specced, they
follow the call++ pattern from the start. follow the binary-stream call app pattern from the start.
### `alknet-hub` ### `alknet-hub`
- Unchanged in shape. The hub composes call apps — docker (JSON ops), - Unchanged in shape. The hub composes call apps — docker (JSON ops),
tty (open op + data channel), tunnel (open op + data channel), tty (open op + binary stream), tunnel (open op + binary stream),
agent, etc. All register ops on the call registry. The hub's agent, etc. All register ops on the call registry. The hub's
assembly layer wires them uniformly. The hub doesn't distinguish assembly layer wires them uniformly. The hub doesn't distinguish
"call app" from "channels app" — both are just apps with ops "call app" from "channels app" — both are just apps with ops
registered. Some ops return JSON; some ops carry the `channel_open` registered. Some ops return JSON; some ops carry the `channel_open`
marker and produce a data channel. The composition model is marker and produce a binary stream. The composition model is
uniform. uniform.
- The relay contract (ADR-079) is unchanged in shape. The op name - The relay contract (ADR-079) is unchanged in shape. The op name
changes (from generic `channel/open` to per-ALPN changes (from generic `channel/open` to per-ALPN
@@ -831,12 +848,12 @@ is always by the responder" invariant.
- WebSocket may carry either `alknet/call` (bare, for call-only - WebSocket may carry either `alknet/call` (bare, for call-only
clients) or `alknet/channels` (8-byte chunk framing, call on clients) or `alknet/channels` (8-byte chunk framing, call on
channel 0 inside). Channels framing required for data-channel channel 0 inside). Channels framing required for binary-stream
clients. ADR-048 amended; OQ-65 resolved. clients. ADR-048 amended; OQ-65 resolved.
- The HTTP adapter's call-protocol surface (registration, browser - The HTTP adapter's call-protocol surface (registration, browser
API routes) is unchanged — it's call ops, not channels ops. API routes) is unchanged — it's call ops, not channels ops.
- The MCP/OpenAPI adapters (`to_openapi`, `to_mcp`) must exclude - The MCP/OpenAPI adapters (`to_openapi`, `to_mcp`) must exclude
marked ops — "produces a data channel" is not expressible over a marked ops — "produces a binary stream" is not expressible over a
request/response export. request/response export.
--- ---
@@ -870,9 +887,10 @@ is always by the responder" invariant.
is a POC-worthy detail. The architectural point is that the data is a POC-worthy detail. The architectural point is that the data
source lives in the ALPN crate. source lives in the ALPN crate.
- **Naming the third category.** "Call++ apps" (the mental model) vs - **Naming the third category.** "Binary-stream call apps" (the
"channels-served ALPNs" (descriptive) vs "data-channel call apps." working name) vs "channels-served ALPNs" (descriptive) vs
Cosmetic but in a lot of tables. Tracked as an OQ. "data-channel call apps." Cosmetic but in a lot of tables. Tracked
as an OQ.
- **Op naming vs OQ-13.** `channels/tty/open` implies the ops belong - **Op naming vs OQ-13.** `channels/tty/open` implies the ops belong
to the channels service, but the *TTY crate* registers them — the to the channels service, but the *TTY crate* registers them — the
@@ -931,15 +949,15 @@ is always by the responder" invariant.
- ADR-029: peer-graph routing (the existing `AccessControl::check` - ADR-029: peer-graph routing (the existing `AccessControl::check`
path this resolution preserves) path this resolution preserves)
- ADR-086: endpoint types and entry points (§4 amended — the - ADR-086: endpoint types and entry points (§4 amended — the
"channels data-channel ALPNs" category reframed to "call++ apps") "channels data-channel ALPNs" category reframed to "binary-stream call apps")
- ADR-048: WebSocket native session (amended — WebSocket may carry - ADR-048: WebSocket native session (amended — WebSocket may carry
either `alknet/call` or `alknet/channels`; channels framing required either `alknet/call` or `alknet/channels`; channels framing required
for data-channel clients; OQ-65 resolved) for binary-stream clients; OQ-65 resolved)
- ADR-057: alknet-tty does not depend on call (amended — the data - ADR-057: alknet-tty does not depend on call (amended — the data
plane stays call-free; the control plane depends on call) plane stays call-free; the control plane depends on call)
- ADR-058: alknet-docker on alknet/call (the boundary criterion - ADR-058: alknet-docker on alknet/call (the boundary criterion
preserved and sharpened — EventEnvelope-compatible → call; preserved and sharpened — EventEnvelope-compatible → call;
incompatible → data channel with call control plane) incompatible → binary stream with call control plane)
- ADR-075: ChannelsAdapter and ChannelManager (the auth-blindness - ADR-075: ChannelsAdapter and ChannelManager (the auth-blindness
this resolution preserves — the ledger lives in channels-call, not this resolution preserves — the ledger lives in channels-call, not
channels-core) channels-core)