feat: rename ALPN prefix from alknet/ to alk/ (v0.1.1)

- CHANNELS_ALPN: b"alknet/channels" → b"alk/channels"
- CallAdapter::alpn(): b"alknet/call" → b"alk/call"
- derive_alpn_from_op_name: alknet/ prefix → alk/ prefix
- All ALPN string literals in src/ and docs/ updated
- ADR-004 amended with prefix rename rationale
- AGENTS.md, README.md updated
- Version bumped to 0.1.1

Review: docs/reviews/003-alpn-prefix-rename.md

Verification:
- cargo test: 542 passed, 0 failed
- cargo clippy --all-targets -- -D warnings: clean
- cargo fmt --check: clean
- cargo doc --no-deps: clean
This commit is contained in:
deepseek-v4-pro committed 2026-08-14 13:55:28 +00:00
1 parent 3cce1410c4
commit 08e7df2aa0
60 files changed
+837 -328

No files matched your search

+9 -9
View File
@@ -7,13 +7,13 @@ last_updated: 2026-08-12
The call + channels RPC crate. Structured JSON RPC (operations, streaming
subscriptions, service discovery) and N-channel multiplexing over one
transport stream (channel 0 pre-negotiated as `alknet/call`).
transport stream (channel 0 pre-negotiated as `alk/call`).
This crate unifies `alknet-call` and `alknet-channels` from the alknet
mono-repo, plus the vendored core types formerly in `alknet-core`. The
source architecture docs were ported from
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall
ADRs (ADR-001..045). The ALPN strings (`alknet/call`, `alknet/channels`)
ADRs (ADR-001..045). The ALPN strings (`alk/call`, `alk/channels`)
are wire-stable and unchanged — see ADR-004.
## Documents
@@ -41,7 +41,7 @@ are wire-stable and unchanged — see ADR-004.
| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | HandlerRegistry, ALPN routing |
| [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | The trait every handler implements |
| [003](decisions/003-auth-as-shared-core.md) | Auth as Shared Core | IdentityProvider, Identity, AuthToken |
| [004](decisions/004-alpn-convention-and-connection-model.md) | ALPN String Convention | `alknet/` prefix, one ALPN per connection |
| [004](decisions/004-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/` prefix, one ALPN per connection |
| [005](decisions/005-bistream-type-definition.md) | BiStream Type Definition | BiStream, handlers receive Connection |
| [006](decisions/006-authcontext-structure.md) | AuthContext Structure | AuthContext fields, hybrid resolution |
| [007](decisions/007-connection-from-stream-generic-single-stream.md) | Connection::from_stream | Generic single-stream connections |
@@ -88,7 +88,7 @@ are wire-stable and unchanged — see ADR-004.
|-----|-------|-----------|
| [034](decisions/034-channels-wire-format.md) | Channels Wire Format | 8-byte chunk header; one-way door |
| [035](decisions/035-channels-pure-channel-multiplexing.md) | Pure Channel Multiplexing | No stream_type; BiStream-only; handler owns sub-mux |
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alknet/call |
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alk/call |
| [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations | channel/open, close, control, resources/subscribe |
| [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel BidiStreamSource; yield-once accept_bi |
| [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Demux/mux; ALPN-blind, auth-blind |
@@ -119,7 +119,7 @@ questions affecting this crate:
## Key Design Principles
1. **One connection, full access**: An `alknet/call` connection gives
1. **One connection, full access**: An `alk/call` connection gives
access to the entire operation registry.
2. **Protocol is symmetric**: Both sides can initiate calls. Producer/
consumer, not server/client.
@@ -135,7 +135,7 @@ questions affecting this crate:
See ADR-024.
8. **Streams are streams**: Every channel is a BiStream. The handler
owns its sub-stream multiplexing. See ADR-035.
9. **Channel 0 is alknet/call**: Channel lifecycle is call operations on
9. **Channel 0 is alk/call**: Channel lifecycle is call operations on
channel 0. See ADR-036, ADR-037.
10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk
header are one-way doors. See ADR-014, ADR-034.
@@ -157,7 +157,7 @@ Downstream crates compose on top of it in a layered dependency chain.
A single process can be a producer of some ops, a consumer of others, a
channel opener for TTY, and a channel acceptor for tunnels — all on the
same `alknet/channels` connection. The types don't encode the system
same `alk/channels` connection. The types don't encode the system
role; they just don't prevent any combination.
Within a single connection, direction is independent of role:
@@ -225,7 +225,7 @@ alkcall. They provide two halves:
(e.g. `trader_client.status().await` instead of
`call_connection.call("trader/status", ...).await`).
**alknet/alknode** depends on alkcall + whichever protocol crates are
**alk/alknode** depends on alkcall + whichever protocol crates are
needed. It's the composition layer — the only place that knows about
network topology, peer routing, and which protocol crates are wired in.
Protocol crates never import a QUIC or TLS dependency.
@@ -250,7 +250,7 @@ A protocol crate that uses channels (e.g. alktty) follows this pattern:
│ │
┌────────┴────────┐ ┌───────┴──────────────┐
│ Direct ALPN │ │ Through channels │
│ (alknet/tty) │ │ (alknet/channels) │
│ (alk/tty) │ │ (alk/channels) │
│ │ │ │
│ TtyAdapter │ │ OpenHandler │
│ impl Protocol │ │ (registered via │
+4 -4
View File
@@ -6,7 +6,7 @@ review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/
# alknet-call
Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alknet/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-007), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps (the dial is in `alknet-client`, the TLS config is in `alknet-tls`).
Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alk/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-007), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps (the dial is in `alknet-client`, the TLS config is in `alknet-tls`).
## Documents
@@ -20,7 +20,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
| ADR | Title | Relevance |
|-----|-------|-----------|
| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alknet/call` |
| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alk/call` |
| [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | CallAdapter implements ProtocolHandler |
| [003](decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core (no irpc — ADR-014) |
| [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation |
@@ -28,7 +28,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | ~~irpc as Call Protocol Foundation~~ | ~~Accepted~~ → **Superseded** by [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) (irpc was never integrated; framing is hand-rolled) |
| [064](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alknet-call; supersedes ADR-013 |
| [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch |
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alknet/call` ALPN, one ALPN per connection |
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/call` ALPN, one ALPN per connection |
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | CallAdapter receives Connection, not BiStream |
| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Vault accessed at assembly layer, not on the wire |
| [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Static handler registration |
@@ -73,7 +73,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
## Key Design Principles
1. **One connection, full access**: An `alknet/call` connection gives access to the entire operation registry — calls, subscriptions, batch, schema.
1. **One connection, full access**: An `alk/call` connection gives access to the entire operation registry — calls, subscriptions, batch, schema.
2. **Protocol is symmetric**: Both sides can initiate calls. The server calling a client uses the same EventEnvelope format and correlation.
3. **Stream-agnostic correlation**: PendingRequestMap correlates by request ID, not by stream. The protocol works with any stream arrangement.
4. **Operation registry is layered**: The curated layer (`Local` provenance) is static — registered at startup by the CLI binary, immutable for the process lifetime. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic overlays at their respective scopes (per-session, per-connection). The registry supports JSON Schema discovery. See ADR-019.
+7 -7
View File
@@ -5,13 +5,13 @@ last_updated: 2026-07-09
# Call Protocol
The wire protocol, stream model, framing, and adapter that alknet-call implements on ALPN `alknet/call`.
The wire protocol, stream model, framing, and adapter that alknet-call implements on ALPN `alk/call`.
## What
The call protocol is a bidirectional, transport-agnostic RPC protocol that runs over any ordered, reliable bidirectional stream within a single `alknet/call` connection. It supports request/response calls, streaming subscriptions, batch operations, and service discovery — all using the same EventEnvelope wire format.
The call protocol is a bidirectional, transport-agnostic RPC protocol that runs over any ordered, reliable bidirectional stream within a single `alk/call` connection. It supports request/response calls, streaming subscriptions, batch operations, and service discovery — all using the same EventEnvelope wire format.
The `CallAdapter` implements `ProtocolHandler` for ALPN `alknet/call`. It receives a `Connection` from the endpoint (QUIC-native, or TCP+TLS/WebTransport/SSH via `Connection::from_stream` — ADR-007), accepts bidirectional streams, and dispatches incoming `EventEnvelope` messages to the operation registry.
The `CallAdapter` implements `ProtocolHandler` for ALPN `alk/call`. It receives a `Connection` from the endpoint (QUIC-native, or TCP+TLS/WebTransport/SSH via `Connection::from_stream` — ADR-007), accepts bidirectional streams, and dispatches incoming `EventEnvelope` messages to the operation registry.
## Why
@@ -104,13 +104,13 @@ below.
### CallConnection
A `CallConnection` represents an established `alknet/call` connection,
A `CallConnection` represents an established `alk/call` connection,
regardless of which side opened it (ADR-022). It holds the connection's
imported-ops overlay (Layer 2, ADR-019) — the set of `from_call`-imported
operations discovered when the connection was established.
```rust
/// An established alknet/call connection (either direction — accepted or
/// An established alk/call connection (either direction — accepted or
/// opened). Holds the connection's Layer 2 overlay (imported ops).
pub struct CallConnection {
/// The underlying transport Connection (from endpoint.accept,
@@ -197,7 +197,7 @@ The call protocol uses bidirectional streams with EventEnvelope framing (transpo
- **Either side can open streams**: The client opens a stream to call a server operation. The server opens a stream to call a client operation. Both use `open_bi()` and `accept_bi()`.
- **Correlation by request ID**: The `id` field in `EventEnvelope` correlates requests with responses. A response arriving on stream N can fulfill a request sent on stream M. The `PendingRequestMap` is keyed by ID, not by stream.
- **Stream usage is the client's choice**: A client may open one stream per operation, one stream for all operations, or any mix. The server processes EventEnvelopes regardless of stream origin.
- **One connection, full access**: A single `alknet/call` connection provides access to all operations (call, subscribe, batch, schema). No need for multiple connections or multiple ALPNs.
- **One connection, full access**: A single `alk/call` connection provides access to all operations (call, subscribe, batch, schema). No need for multiple connections or multiple ALPNs.
### Wire Format: EventEnvelope
@@ -569,7 +569,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is
|----------|-----|---------|
| Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled length-prefixed JSON framing, operation registry, dispatch; supersedes ADR-013 (irpc was never imported) |
| Call protocol stream model | [ADR-015](decisions/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation |
| ALPN per connection | [ADR-004](decisions/006-alpn-convention-and-connection-model.md) | `alknet/call` is a distinct ALPN, one connection per ALPN |
| ALPN per connection | [ADR-004](decisions/006-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN |
| ProtocolHandler receives Connection | [ADR-005](decisions/007-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams |
| Vault integration point | [ADR-008](decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time |
| Secret material flow | [ADR-010](decisions/014-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer |
+3 -3
View File
@@ -30,13 +30,13 @@ pub struct ChannelClient {
impl ChannelClient {
/// Construct a `ChannelClient` over a pre-established transport
/// `Connection` on ALPN `alknet/channels`. This is the
/// `Connection` on ALPN `alk/channels`. This is the
/// transport-agnostic primary constructor: the caller (or a
/// transport-specific dial helper) produces the `Connection` —
/// via `Connection::from_bidi` (TCP+TLS, WebTransport, SSH
/// `direct-tcpip`), a quinn connection, or any other `AsyncRead +
/// AsyncWrite` source — and this method takes over: installs
/// channel 0 (`alknet/call`), spawns the demux/mux, and returns
/// channel 0 (`alk/call`), spawns the demux/mux, and returns
/// the client. Mirrors the server side's transport-agnostic
/// `ChannelsAdapter::handle(Connection)` and
/// `CallClient::spawn_dispatch(Connection)`.
@@ -111,7 +111,7 @@ the one-way-door API surface. It takes a pre-established `Connection` and
takes over channels establishment. The transport is the caller's concern:
`Connection::from_bidi(tls_stream, ...)` for TCP+TLS, a quinn `Connection`,
a WebTransport `BiStream`, an SSH `direct-tcpip` channel wrapped via
`from_bidi`, a WebSocket carrying `alknet/channels` (the browser path per
`from_bidi`, a WebSocket carrying `alk/channels` (the browser path per
ADR-044) — all produce a `Connection` that `from_connection` accepts
unchanged. This mirrors the server side's `ChannelsAdapter::handle(Connection)`, which is substrate-agnostic by the same mechanism.
+9 -9
View File
@@ -25,7 +25,7 @@ and/or `channels/<alpn>/pub`:
| `channels/<alpn>/sub` | `Sub` | consumer (subscribes) | producer (streams) | server → client |
| `channels/<alpn>/pub` | `Pub` | producer (publishes) | consumer (receives) | client → server |
The `channels/<alpn>/...` path segment is the ALPN with the `alknet/`
The `channels/<alpn>/...` path segment is the ALPN with the `alk/`
prefix stripped (ADR-047 §"Negative"). An ALPN may register one or both
ops; separate op types → separate ACLs. The op spec carries the
`channel_open: Option<ChannelOpenSpec>` marker (ADR-047 §2) — the
@@ -44,8 +44,8 @@ Request (`call.requested` on channel 0):
```
The `input` is ALPN-specific params (the former `params` field, now the
op's `input_schema`). For `alknet/tty` this is the `NegotiateRequest`;
for `alknet/tunnel` this is the target resource. The channels layer does
op's `input_schema`). For `alk/tty` this is the `NegotiateRequest`;
for `alk/tunnel` this is the target resource. The channels layer does
not interpret `input`.
Response (`call.responded`):
@@ -174,11 +174,11 @@ The responder registers a `StreamingHandler` that emits a
"output": {
"resources": [
{
"alpn": "alknet/tty",
"alpn": "alk/tty",
"backends": ["docker", "local"]
},
{
"alpn": "alknet/tunnel",
"alpn": "alk/tunnel",
"targets": ["container:*", "service:postgres"]
}
]
@@ -394,7 +394,7 @@ This is not a flaw — it is the same shape as any per-peer ACL.
### Recursive channels do not bypass the cap
A recursive `alknet/channels`-inside-`alknet/channels` channel runs a
A recursive `alk/channels`-inside-`alk/channels` channel runs a
new `ChannelsAdapter` with a new `ChannelManager`. If the same
`ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`,
the inner channels are counted against the same identity. Recursion
@@ -422,8 +422,8 @@ JSON payload; the hub's `CallAdapter` translates these too (rewrites
`channel/control` — it's a call operation, translated, not
byte-forwarded.
The hub never runs a handler for `alknet/tty`, `alknet/ssh`, or
`alknet/tunnel`. It runs `alknet/channels` (the relay) and `alknet/call`
The hub never runs a handler for `alk/tty`, `alk/ssh`, or
`alk/tunnel`. It runs `alk/channels` (the relay) and `alk/call`
(for its own hub-level operations + translation). The `channel_open`
marker (ADR-047 §2) is how the hub recognizes a channel-open op during
`from_call` discovery (ADR-047 §1, Gap C) — the `from_call` relay
@@ -437,7 +437,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|-----|----------|---------|
| [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations | The generic ops; `direction` pinned (amended by ADR-047 — `channel/open` dissolves; `direction` removed) |
| [047](decisions/047-openable-alpns-are-operations.md) | Openable ALPNs Are Operations | Per-ALPN open ops; `channel_open` marker; `ChannelCore` wrapper; opener ledger |
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alknet/call |
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alk/call |
| [042](decisions/042-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels |
| [041](decisions/041-per-identity-channel-cap.md) | Per-Identity Channel Cap | 256 per PeerId, enforced via ChannelLifecyclePolicy in channels-call (amended by ADR-047 §7 — opener ledger, every teardown path) |
| [035](decisions/035-channels-pure-channel-multiplexing.md) | Pure Channel Multiplexing | No stream_types; handler owns sub-mux |
+6 -6
View File
@@ -5,9 +5,9 @@ last_updated: 2026-07-18
# alknet-channels
A multiplexing proxy: a `ProtocolHandler` on `alknet/channels` that
A multiplexing proxy: a `ProtocolHandler` on `alk/channels` that
decomposes a single bidirectional transport stream into N logical channels,
each carrying a different ALPN. Channel 0 is pre-negotiated as `alknet/call`
each carrying a different ALPN. Channel 0 is pre-negotiated as `alk/call`
(ADR-036); every other channel is opened dynamically via call operations on
channel 0 and routed through the same `HandlerRegistry` as top-level
connections. The channels layer is a re-framing proxy — it converts between
@@ -23,7 +23,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives.
| [overview.md](overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates |
| [channels-wire.md](channels-wire.md) | draft | The 8-byte chunk format (`[channel_id:u32 be][length:u32 be][payload]`), the add/strip composition, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) |
| [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-008/074, as amended by ADR-035), `accept_bi` yields one `BiStream` per channel, recursive composition |
| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alknet/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) |
| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alk/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) |
| [channel-operations.md](channel-operations.md) | draft | `channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe` — call-protocol operations on channel 0, ACL flow, `direction` semantics, the hub relay contract (ADR-042) |
| [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045); bidirectionality preserved |
@@ -33,7 +33,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives.
|-----|-------|-----------|
| [071](decisions/071-channels-wire-format.md) | channels Wire Format — 8-Byte Chunk Header | The chunk format; channels layer has no `stream_type` concept (amended by ADR-035); substrate-agnostic; one-way door |
| [093](decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte |
| [072](decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alknet/call` | Channel 0 = call protocol; no special control plane |
| [072](decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alk/call` | Channel 0 = call protocol; no special control plane |
| [073](decisions/073-channel-lifecycle-operations.md) | Channel Lifecycle Operations on the Call Protocol | `channel/open`/`close`/`control`/`resources/subscribe`; `direction` semantics; subscribe not poll |
| [074](decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `accept_bi` yields `BiStream` (amended by ADR-035 — `into_sub_streams` removed) |
| [075](decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts |
@@ -72,9 +72,9 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives.
`HandlerRegistry`. See [overview.md](overview.md) and ADR-034 (as
amended by ADR-035).
2. **Channel 0 is `alknet/call` pre-negotiated, not a special control
2. **Channel 0 is `alk/call` pre-negotiated, not a special control
plane.** The call protocol runs on channel 0 exactly as on a top-level
`alknet/call` connection. Channel lifecycle operations
`alk/call` connection. Channel lifecycle operations
(`channel/open`, `channel/close`, `channel/control`,
`channel/resources/subscribe`) are call operations on channel 0's
`OperationRegistry`, gated by the existing `AccessControl::check`. No
+2 -2
View File
@@ -16,7 +16,7 @@ per channel (not per `(channel_id, stream_type)`).
| Component | Role | What it knows |
|-----------|------|---------------|
| `ChannelsAdapter` | `ProtocolHandler` on `alknet/channels`; reads 8-byte chunk headers off every bidi stream the transport yields and routes to `ChannelManager`. Substrate-agnostic (ADR-034 §substrate modes, as amended by ADR-035). | The transport stream(s); the `ChannelManager` handle. ALPN-blind. |
| `ChannelsAdapter` | `ProtocolHandler` on `alk/channels`; reads 8-byte chunk headers off every bidi stream the transport yields and routes to `ChannelManager`. Substrate-agnostic (ADR-034 §substrate modes, as amended by ADR-035). | The transport stream(s); the `ChannelManager` handle. ALPN-blind. |
| `ChannelManager` | Shared state; holds `channel_id → ChannelState`, `HandlerRegistry`. Constructs `ChannelBidiStreamSource` per channel. What `channel/open` closes over (in `channels-call`). | The channel map; the handler registry for ALPN lookup. ALPN-blind (looks up ALPNs, doesn't parse their protocols). |
The split mirrors the TTY crate's `ChunkReader`/`ChunkWriter` + adapter
@@ -28,7 +28,7 @@ channel 0 is special only in that it's pre-allocated (by `channels-call`).
```rust
#[async_trait]
impl ProtocolHandler for ChannelsAdapter {
fn alpn(&self) -> &'static [u8] { b"alknet/channels" }
fn alpn(&self) -> &'static [u8] { b"alk/channels" }
async fn handle(&self, connection: Connection, auth: &AuthContext)
-> Result<(), HandlerError>
+2 -2
View File
@@ -130,8 +130,8 @@ validated shape and the `StreamBidiStreamSource` yield-once contract
A `ChannelBidiStreamSource` is a `BidiStreamSource`, and
`Connection::from_source` wraps it. A handler that is itself
`alknet/channels` can open a sub-channels connection on a data channel —
`alknet/channels` inside `alknet/channels`. The outer layer strips its
`alk/channels` can open a sub-channels connection on a data channel —
`alk/channels` inside `alk/channels`. The outer layer strips its
8-byte header; the inner layer parses its own 8-byte header from the
payload. Each level is the same shape: `BiStream → accept_bi → N
BiStreams`. The recursion is unbounded and uniform at every level.
+16 -16
View File
@@ -8,14 +8,14 @@ last_updated: 2026-07-18
## What
`alknet-channels` is a multiplexing proxy crate. It implements
`ProtocolHandler` for the `alknet/channels` ALPN: it receives one
`ProtocolHandler` for the `alk/channels` ALPN: it receives one
bidirectional transport stream, reads 8-byte chunk headers, and routes each
chunk's payload to the right logical channel. Each channel is reassembled
into a `BiStream` (a concrete `AsyncRead + AsyncWrite` newtype, per
ADR-009) and presented to its handler as a `Connection` — the handler
doesn't know it's inside a channels connection.
Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Every other channel
Channel 0 is pre-negotiated as `alk/call` (ADR-036). Every other channel
is opened dynamically via `channel/open` on channel 0 (ADR-037) and routed
through the same `HandlerRegistry` as top-level connections. The channels
layer does no protocol work itself — it is a re-framing proxy that converts
@@ -45,11 +45,11 @@ and per-ALPN connection management.
### The collapse: one multiplexing model, one connection per leg
With `alknet/channels`, one connection carries everything:
With `alk/channels`, one connection carries everything:
```
Browser ──WebTransport──► Hub ──QUIC──► Spoke
alknet/channels alknet/channels
alk/channels alk/channels
┌─────────────┐ ┌─────────────┐
│ ch0: call │ │ ch0: call │
│ ch1: tty │ relay │ ch1: tty │
@@ -69,7 +69,7 @@ The collapse is at three levels:
2. **One multiplexing model, not three.** Connection-level, stream-level,
and sub-stream-level all become channels chunks.
3. **The call protocol orchestrates from inside.** Channel 0 is
`alknet/call` on both legs. The call protocol's `OperationRegistry`,
`alk/call` on both legs. The call protocol's `OperationRegistry`,
`AccessControl`, and `forwarded_for` machinery govern channel lifecycle
with no new auth.
@@ -92,7 +92,7 @@ on the `BiStream` the channels layer gives them (ADR-035).
`STREAM_CTRL_OUT` — ADR-052 amended by Phase 7). The channels layer
doesn't carry control.
- **Recursive composition is literal.** A channel with ALPN
`alknet/channels` runs another channels demux on its `BiStream`. The
`alk/channels` runs another channels demux on its `BiStream`. The
outer layer strips its 8-byte header; the inner layer parses its own
8-byte header from the payload.
@@ -101,7 +101,7 @@ on the `BiStream` the channels layer gives them (ADR-035).
The crate has two internal components (ADR-039):
- **`ChannelsAdapter`** — implements `ProtocolHandler` for
`alknet/channels`. Its `handle()` receives one `Connection`, reads 8-byte
`alk/channels`. Its `handle()` receives one `Connection`, reads 8-byte
chunk headers, and routes chunks to the `ChannelManager`. The read/demux
half.
- **`ChannelManager`** — the shared state. Holds `channel_id →
@@ -128,7 +128,7 @@ The channels subsystem is split into two modules:
call-protocol-blind, transport-blind. This is where the "streams are
streams" insight lives.
- **`channels` (call coupling)** — channel 0 pre-negotiation as
`alknet/call` (ADR-036), the lifecycle operations (ADR-037) registered
`alk/call` (ADR-036), the lifecycle operations (ADR-037) registered
on the call protocol's `OperationRegistry`, `ChannelCore` (ADR-047 §3),
and `ChannelClient` (ADR-043). This is where the call-protocol coupling
lives, isolated from the pure multiplexer.
@@ -155,7 +155,7 @@ A worker is any crate that uses `ChannelClient` to dial. There are no
## ALPN
`alknet/channels` — the ALPN the `ChannelsAdapter` registers on. One ALPN
`alk/channels` — the ALPN the `ChannelsAdapter` registers on. One ALPN
per channels connection; the connection carries N logical channels, each
with its own ALPN (negotiated via `channel/open`).
@@ -166,11 +166,11 @@ stream:
| Transport | How |
|-----------|-----|
| QUIC bidi stream | `alknet/channels` ALPN on a QUIC connection; one bidi stream carries all channels |
| TCP+TLS | `alknet/channels` ALPN on a TLS connection; the TCP stream carries all channels |
| WebTransport | `alknet/channels` session (deferred per ADR-044; the browser path uses WebSocket carrying `alknet/channels`) |
| QUIC bidi stream | `alk/channels` ALPN on a QUIC connection; one bidi stream carries all channels |
| TCP+TLS | `alk/channels` ALPN on a TLS connection; the TCP stream carries all channels |
| WebTransport | `alk/channels` session (deferred per ADR-044; the browser path uses WebSocket carrying `alk/channels`) |
| SSH channel | channels connection riding inside an SSH `direct-tcpip` channel (channels-over-SSH) |
| Another channels connection | recursive composition (channel type `alknet/channels` inside `alknet/channels`) |
| Another channels connection | recursive composition (channel type `alk/channels` inside `alk/channels`) |
The same wire format, the same chunk reassembly, the same `Connection`
abstraction. The transport is a parameter, not a design constraint.
@@ -219,7 +219,7 @@ A protocol crate can support two paths for its wire format:
| Path | How it connects | Wire format |
|------|----------------|-------------|
| Direct ALPN | `ProtocolHandler` on its own ALPN (e.g. `alknet/tty`), gets a `Connection`, loops `accept_bi` | Own wire format (e.g. TTY's 5-byte header) |
| Direct ALPN | `ProtocolHandler` on its own ALPN (e.g. `alk/tty`), gets a `Connection`, loops `accept_bi` | Own wire format (e.g. TTY's 5-byte header) |
| Through channels | Registered via `ChannelCore::register_openable`, gets a `BiStream` per session | Own wire format rides inside channels 8-byte payload |
The protocol crate doesn't know which path it's on — it takes a
@@ -230,7 +230,7 @@ feature flags) or in the downstream alknet crate.
### alknet-call
The call protocol runs on channel 0 exactly as on a top-level
`alknet/call` connection. The `Dispatcher` receives a `Connection`
`alk/call` connection. The `Dispatcher` receives a `Connection`
backed by channel-0 chunk reassembly and dispatches operations — it
doesn't know it's inside channels. The call protocol's `EventEnvelope`
framing (ADR-014) is the channels payload; the channels layer carries
@@ -254,7 +254,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|-----|----------|---------|
| [034](decisions/034-channels-wire-format.md) | channels Wire Format | 8-byte chunk header (amended by ADR-035); channels layer has no `stream_type` concept; one-way door |
| [035](decisions/035-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte |
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = `alknet/call` |
| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = `alk/call` |
| [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations | `channel/open`/`close`/`control`/`resources/subscribe`; subscribe not poll; `direction` pinned |
| [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; yield-once `accept_bi` (amended by ADR-035 — `into_sub_streams` removed) |
| [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 |
+5 -5
View File
@@ -5,7 +5,7 @@ last_updated: 2026-07-18
# channels-wire.md — The 8-Byte Chunk Format
The wire format for `alknet/channels`: an 8-byte chunk header that
The wire format for `alk/channels`: an 8-byte chunk header that
multiplexes N logical channels over a single ordered, reliable
bidirectional transport stream. ADR-034 (amended by ADR-035) is the
decision; this doc specifies the format and the wire-level invariants.
@@ -23,7 +23,7 @@ multiplexing on the `BiStream` the channels layer gives it.
| field | offset | width | meaning |
|-------|--------|-------|---------|
| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). |
| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). |
| `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN`. |
The payload is opaque to the channels layer. The handler parses its own
@@ -70,10 +70,10 @@ stream — the demux drops the chunk and continues. The header is always
exactly 8 bytes, so the demux can always resync by reading the next
8-byte header.
## Channel 0 — pre-negotiated `alknet/call`
## Channel 0 — pre-negotiated `alk/call`
Channel 0 is not a special "control plane" with its own framing. It is
`alknet/call` pre-negotiated (ADR-036): both sides know `channel_id = 0`
`alk/call` pre-negotiated (ADR-036): both sides know `channel_id = 0`
is routed to the `CallAdapter` without an explicit `channel/open`
exchange.
@@ -230,7 +230,7 @@ a `channel_id` was stripped before it saw the bytes.
The composition is uniform — the same shape at every level. This is SSH's
model (layered headers, each layer strips its own at its boundary),
applied to channels. A `alknet/channels`-inside-`alknet/channels`
applied to channels. A `alk/channels`-inside-`alk/channels`
recursive composition is the outer layer stripping its 8-byte header, the
inner layer parsing its own 8-byte header from the payload — same code,
same shape, each level.
+5 -5
View File
@@ -21,7 +21,7 @@ client-side connection-establishment half and the adapter surface.
This document specifies three components, all in `alknet-call`:
1. **`CallClient`** — takes over an established transport `Connection`
on ALPN `alknet/call`, spawns the shared dispatch loop, and produces
on ALPN `alk/call`, spawns the shared dispatch loop, and produces
a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary;
dial lives in `AlknetClient` per ADR-045); the dispatch loop is
shared with the server-side `CallAdapter`
@@ -88,7 +88,7 @@ cross-peer dissolved / same-peer stays, DC-4→OQ-26).
### CallClient
`CallClient` takes over an established transport `Connection` on ALPN
`alknet/call`, spawns the shared dispatch loop, and produces a
`alk/call`, spawns the shared dispatch loop, and produces a
`CallConnection`. The `CallConnection` type is already implemented
(`call-protocol.md` §"CallConnection") — it wraps an established
`Connection` and holds the Layer 2 imported-ops overlay. `CallClient`
@@ -115,7 +115,7 @@ impl CallClient {
pub fn new(registry: Arc<OperationRegistry>, idp: Arc<dyn IdentityProvider>) -> Self;
/// Transport-agnostic primary constructor. Takes a pre-established
/// `Connection` on ALPN `alknet/call` (any transport — QUIC via
/// `Connection` on ALPN `alk/call` (any transport — QUIC via
/// `from_quinn`, TCP+TLS via `from_bidi`, WebTransport, SSH
/// `direct-tcpip`, a WebSocket), spawns the shared dispatch loop,
/// and returns a live `CallConnection`. Mirrors the server-side
@@ -609,7 +609,7 @@ Container service (runs on a vast.ai/docker instance):
Connects to hub as a CallClient (outbound connection — runner pattern)
Hub (central server):
Runs CallAdapter (server) on alknet/call (already implemented)
Runs CallAdapter (server) on alk/call (already implemented)
When the container service connects:
hub runs from_call → discovers /container/* via services/list + services/schema
registers them as FromCall provenance (leaf, forwarding handlers) in the
@@ -634,7 +634,7 @@ Bilateral: the container service ALSO runs from_call against the hub,
**Why the container service doesn't need alknet-ssh**: under the call
protocol, the container service is a `CallClient` that dials the hub's
`alknet/call` ALPN (over QUIC, TCP+TLS, or any transport) — no SSH in
`alk/call` ALPN (over QUIC, TCP+TLS, or any transport) — no SSH in
the loop. SSH port
forwarding becomes the *transitional* mechanism for targets that can't run a
call-protocol client (the `alknet-ssh` phase-0 findings document this
@@ -32,9 +32,9 @@ The endpoint advertises the union of all registered handlers' ALPN strings. When
- WASM story is clean: handlers receive byte streams, protocol parsers that operate on bytes compile to WASM
**Negative:**
- ALPN is negotiated per-connection, not per-stream — a client that wants to use multiple ALPNs (e.g., SSH and call protocol) opens separate QUIC connections for each. QUIC connections are cheap (multiplexed over the same UDP flow), so this is acceptable, but it means `alknet/call` cannot serve as a multiplexer for other ALPNs within a single connection unless explicitly designed to do so (see ADR-004).
- ALPN is negotiated per-connection, not per-stream — a client that wants to use multiple ALPNs (e.g., SSH and call protocol) opens separate QUIC connections for each. QUIC connections are cheap (multiplexed over the same UDP flow), so this is acceptable, but it means `alk/call` cannot serve as a multiplexer for other ALPNs within a single connection unless explicitly designed to do so (see ADR-004).
- All protocols must be registered at endpoint creation time (or use hot-reload via ArcSwap for dynamic addition)
- Custom protocols require reserving ALPN strings — we own the `alknet/` namespace
- Custom protocols require reserving ALPN strings — we own the `alk/` namespace
- Debugging requires knowing which ALPN was negotiated (mitigated by logging at the endpoint level)
## References
@@ -24,7 +24,7 @@ A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInt
```rust
#[async_trait]
pub trait ProtocolHandler: Send + Sync + 'static {
/// The ALPN string this handler claims (e.g. b"alknet/ssh")
/// The ALPN string this handler claims (e.g. b"alk/ssh")
fn alpn(&self) -> &'static [u8];
/// Handle an incoming connection (revised by ADR-005 to receive
@@ -18,25 +18,25 @@ The iroh reference project uses the same model: each `ProtocolHandler` claims an
### ALPN String Convention
Custom ALPN strings use the `alknet/` prefix:
Custom ALPN strings use the `alk/` prefix:
| ALPN | Handler | Type |
|------|---------|------|
| `alknet/ssh` | SshAdapter | Custom |
| `alknet/call` | CallAdapter | Custom |
| `alknet/git` | GitAdapter | Custom |
| `alknet/sftp` | SftpAdapter | Custom |
| `alknet/msg` | MessageAdapter | Custom |
| `alknet/http` | HttpAdapter | Custom |
| `alknet/dns` | DnsAdapter | Custom |
| `h3` | WebTransport → alknet/http | Standard (IANA) |
| `h2` | HTTP/2 → alknet/http | Standard (IANA) |
| `http/1.1` | HTTP/1.1 → alknet/http | Standard (IANA) |
| `alk/ssh` | SshAdapter | Custom |
| `alk/call` | CallAdapter | Custom |
| `alk/git` | GitAdapter | Custom |
| `alk/sftp` | SftpAdapter | Custom |
| `alk/msg` | MessageAdapter | Custom |
| `alk/http` | HttpAdapter | Custom |
| `alk/dns` | DnsAdapter | Custom |
| `h3` | WebTransport → alk/http | Standard (IANA) |
| `h2` | HTTP/2 → alk/http | Standard (IANA) |
| `http/1.1` | HTTP/1.1 → alk/http | Standard (IANA) |
Rules:
- Custom ALPNs use the format `alknet/<name>` — lowercase, no version number
- Custom ALPNs use the format `alk/<name>` — lowercase, no version number
- Standard ALPNs (`h2`, `http/1.1`, `h3`) use their IANA-registered strings and are handled by the HTTP adapter
- No version numbers in ALPN strings initially. If protocol compatibility breaks, a new ALPN string is registered (e.g., `alknet/call/v2`). This is simpler than version negotiation and follows the QUIC convention that ALPN mismatch means connection failure
- No version numbers in ALPN strings initially. If protocol compatibility breaks, a new ALPN string is registered (e.g., `alk/call/v2`). This is simpler than version negotiation and follows the QUIC convention that ALPN mismatch means connection failure
- ALPN strings are compile-time constants in each handler's `alpn()` method — no runtime registration of new ALPN strings
### Connection Model
@@ -44,23 +44,23 @@ Rules:
**One ALPN per connection.** A client that wants to use multiple ALPNs opens one QUIC connection per ALPN. All connections from the same client are multiplexed over the same UDP flow (QUIC's natural connection multiplexing), so the overhead is minimal.
This means:
- `alknet/call` is a distinct ALPN with its own connection — not a multiplexer for other ALPNs
- `alk/call` is a distinct ALPN with its own connection — not a multiplexer for other ALPNs
- A client interacting with both SSH and call protocol has two QUIC connections
- Within an `alknet/call` connection, multiple QUIC streams can carry independent operations (see ADR-013)
- Within an `alk/call` connection, multiple QUIC streams can carry independent operations (see ADR-013)
- The endpoint logs the negotiated ALPN for each connection for observability
## Consequences
**Positive:**
- Simple model: one connection, one protocol — no multiplexing layer needed inside a connection
- ALPN strings are predictable and discoverable — `alknet/<name>` is a clear namespace
- ALPN strings are predictable and discoverable — `alk/<name>` is a clear namespace
- No version negotiation complexity — incompatible versions get new ALPN strings
- QUIC connection multiplexing means multiple ALPN connections share the same UDP flow
**Negative:**
- Multiple ALPNs require multiple connections — a full-featured client might have 3-5 QUIC connections open simultaneously
- No version negotiation — an incompatible change requires a new ALPN string, which means old and new clients can coexist only if the server registers both ALPNs
- The `alknet/` namespace is owned by this project — third-party extensions need their own prefix
- The `alk/` namespace is owned by this project — third-party extensions need their own prefix
## References
@@ -68,4 +68,16 @@ This means:
- ADR-002: ProtocolHandler trait
- OQ-03: ALPN string naming convention (resolved by this ADR)
- OQ-06: Server-side ALPN vs client-side ALPN (resolved by this ADR)
- iroh reference: `docs/research/references/iroh/`
- iroh reference: `docs/research/references/iroh/`
## Amendment 1 (2026-08-14): Prefix shortened from `alknet/` to `alk/`
The original decision used `alknet/` as the prefix. Before the first
published release (v0.1.1), the prefix was shortened to `alk/` for
brevity and readability. The first downstream consumer (alktty) uses
`alk/tty`; the shorter prefix is cleaner and avoids unnecessary
verbosity.
The convention otherwise remains: one ALPN per connection, the prefix
identifies the alk protocol family, no version numbers in ALPN strings,
and ALPN strings are compile-time constants.
@@ -186,7 +186,7 @@ WebTransport stream).
- `alknet-ssh` is unblocked: the SSH handler wraps each russh channel via
`from_stream` and dispatches by channel-type (treated as the ALPN string)
through `HandlerRegistry`. One SSH connection carries heterogeneous
channels (`alknet/tty`, `alknet/call`, `h2`, ...) — a multiplexing power
channels (`alk/tty`, `alk/call`, `h2`, ...) — a multiplexing power
QUIC's per-connection ALPN doesn't give natively.
- WebTransport stream dispatch is unblocked: the WT handler wraps each WT
stream via `from_stream` and dispatches through `HandlerRegistry` (the
@@ -345,7 +345,7 @@ pump halves, the same idiom it would use over `TcpStream`.
### `BiStream` over WebSocket enables "VPN-like without being a VPN" in v1
The `webtransport.md` spec describes the "VPN-like without being a VPN"
path: a browser opens a WebTransport session to `/alknet/ssh`, the h3
path: a browser opens a WebTransport session to `/alk/ssh`, the h3
handler hands each bidi stream to `SshAdapter::handle` as a
`Connection`, the browser's WASM SSH parser speaks SSH over the
stream. WebTransport is deferred per ADR-044.
@@ -52,7 +52,7 @@ identity is unavailable:
- **Browsers** — no raw-key support, no client cert the hub can
fingerprint; the browser authenticates via a bearer token over
HTTP/WebSocket, and the hub's `IdentityProvider` resolves it.
- **`alknet/register`** — a native worker that hasn't been enrolled dials
- **`alk/register`** — a native worker that hasn't been enrolled dials
in with no prior peer relationship; a registration token (or open
registration) establishes identity, not a TLS fingerprint.
@@ -106,7 +106,7 @@ dimensions every dial consumes:
/// (fingerprint, driving verifier selection per ADR-034).
///
/// This is NOT the call-protocol credential bundle. The call-protocol
/// `auth_token` (hub-correlated bearer for browsers / `alknet/register`)
/// `auth_token` (hub-correlated bearer for browsers / `alk/register`)
/// is a per-request field on `call.requested` payloads, not a
/// transport credential. It stays in the call-protocol layer.
pub struct ConnectionCredentials {
@@ -235,7 +235,7 @@ credential bundle is needed):**
bearer token to an `Identity` via `IdentityProvider::resolve_from_token`
at the HTTP boundary. The call protocol receives the `Identity`, not
the token.
2. **Registration** (`alknet/register` native ALPN, `/register` HTTP
2. **Registration** (`alk/register` native ALPN, `/register` HTTP
endpoint) — a client not yet associated with a hub presents a
one-time registration token; the hub creates a `PeerEntry` (a new
identity based on the fingerprint). Outbound, the vault manages the
@@ -33,7 +33,7 @@ The call protocol is derived from a TypeScript implementation (`@alkdev/operatio
## Decision
alknet-call uses irpc as its foundation. The `CallAdapter` implements `ProtocolHandler` on ALPN `alknet/call` and delegates to irpc's operation registry, framing, and dispatch.
alknet-call uses irpc as its foundation. The `CallAdapter` implements `ProtocolHandler` on ALPN `alk/call` and delegates to irpc's operation registry, framing, and dispatch.
irpc is not replaced or wrapped in an abstraction layer — it IS the call protocol's core. The relationship is:
- irpc provides: operation registry, schema discovery, frame encoding/decoding, request/response routing, streaming
@@ -6,7 +6,7 @@ Accepted
## Context
The call protocol (alknet-call) operates on a QUIC connection with ALPN `alknet/call`. Within that connection, QUIC provides bidirectional streams. The question is how the call protocol uses those streams and how it correlates requests with responses — especially when both sides can initiate calls.
The call protocol (alknet-call) operates on a QUIC connection with ALPN `alk/call`. Within that connection, QUIC provides bidirectional streams. The question is how the call protocol uses those streams and how it correlates requests with responses — especially when both sides can initiate calls.
The reference implementation used `EventEnvelope` framing with a `PendingRequestMap` that correlates `call.requested` events to `call.responded` events by request ID, regardless of which stream carries them. This works well but the relationship between streams and operations was underspecified.
@@ -16,7 +16,7 @@ OQ-07 asked: "What is the scope of the call protocol within a connection? Should
The call protocol uses **bidirectional QUIC streams with EventEnvelope framing and ID-based correlation**. The protocol does not prescribe a stream usage pattern — it works with any arrangement:
1. **EventEnvelope on every stream** — every bidirectional stream opened on the `alknet/call` connection carries length-prefixed JSON `EventEnvelope` messages. The five event types (`call.requested`, `call.responded`, `call.completed`, `call.aborted`, `call.error`) are the protocol primitives.
1. **EventEnvelope on every stream** — every bidirectional stream opened on the `alk/call` connection carries length-prefixed JSON `EventEnvelope` messages. The five event types (`call.requested`, `call.responded`, `call.completed`, `call.aborted`, `call.error`) are the protocol primitives.
2. **PendingRequestMap correlates by ID, not by stream** — the `id` field in `EventEnvelope` correlates requests with responses. A response on stream 5 can fulfill a request sent on stream 3. The PendingRequestMap is keyed by request ID.
@@ -30,7 +30,7 @@ The call protocol uses **bidirectional QUIC streams with EventEnvelope framing a
5. **Stream usage is the client's choice** — a client may open one stream per operation, one stream for all operations, or any mix. The protocol is stream-agnostic. The server accepts streams and processes EventEnvelopes regardless of which stream they arrive on.
This resolves OQ-07: the call protocol's scope within a connection is the full operation registry. One `alknet/call` connection gives access to all operations (call, subscribe, batch, schema). QUIC's built-in stream multiplexing handles concurrency — the protocol doesn't need to impose additional multiplexing.
This resolves OQ-07: the call protocol's scope within a connection is the full operation registry. One `alk/call` connection gives access to all operations (call, subscribe, batch, schema). QUIC's built-in stream multiplexing handles concurrency — the protocol doesn't need to impose additional multiplexing.
## Consequences
@@ -17,9 +17,9 @@ as sharing one immutability argument:
2. **The call protocol's `OperationRegistry`** (operation name →
`HandlerRegistration`). This lives *inside* the `CallAdapter`, which is one
`ProtocolHandler` behind the single ALPN `alknet/call`. Adding an operation
`ProtocolHandler` behind the single ALPN `alk/call`. Adding an operation
to the `OperationRegistry` does **not** touch the TLS `ServerConfig` — the
ALPN is already `alknet/call`, registered once at startup.
ALPN is already `alk/call`, registered once at startup.
`operation-registry.md` stated the operation registry "is immutable after
construction… consistent with OQ-04 and ADR-010." That inheritance was by
@@ -45,7 +45,7 @@ architecture.
### 1. `CallClient` opens connections and shares the dispatch loop
`CallClient` opens a QUIC connection to a remote node with ALPN `alknet/call`.
`CallClient` opens a QUIC connection to a remote node with ALPN `alk/call`.
Once connected, the connection is symmetric — both sides can send and receive
`call.requested`. The `CallClient` is not just a caller; it is also a callee.
It has its own operation registry to dispatch incoming calls from the remote
@@ -20,7 +20,7 @@ identified the gap.
## Context
ADR-022 §1 established that a `CallClient` — which opens an outbound
`alknet/call` connection — "has its own operation registry to dispatch incoming
`alk/call` connection — "has its own operation registry to dispatch incoming
calls from the remote side." The ADR left the *registry scope* as an explicit
two-way door in its Consequences:
@@ -86,7 +86,7 @@ crate depends on another handler crate" rule were written before
`HandlerRegistration`, and `OperationAdapter` trait) was specced.
**Clarification:** `alknet-call` is both a handler crate (it implements
`ProtocolHandler` on ALPN `alknet/call`) *and* the protocol-foundation
`ProtocolHandler` on ALPN `alk/call`) *and* the protocol-foundation
crate that `alknet-agent`, `alknet-napi`, and `alknet-http` consume for
the operation registry, adapter contract, and call client. The "no
handler crate depends on another handler crate" rule applies to peer
@@ -38,7 +38,7 @@ rationale and the cross-ADR impacts.
## Context
`alknet-channels` is a multiplexing proxy: a `ProtocolHandler` on
`alknet/channels` that carries N logical channels, each with a different
`alk/channels` that carries N logical channels, each with a different
ALPN, over transport stream(s). The wire format is the substrate that makes
this work.
@@ -105,7 +105,7 @@ preserving the framing-disambiguation soundness property.
| field | width | meaning |
|-------|-------|---------|
| `channel_id` | u32 BE | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open`. |
| `channel_id` | u32 BE | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open`. |
| `stream_type` | u8 | The unidirectional sub-stream within the channel. See "Stream types" below. |
| `length` | u32 BE | The payload length in bytes. 0 = EOF sentinel (same convention as TTY — ADR-052 §Sentinels). |
@@ -163,10 +163,10 @@ stream_types: the channels layer routes bytes, the handler interprets them.
| ALPN | Active stream_types | Why |
|------|---------------------|-----|
| `alknet/call` (channel 0) | [0, 1] | call frames bidirectional via 0=in, 1=out |
| `alknet/tty` | [0, 1, 2, 3, 4] | data in/out/err + control in/out |
| `alknet/tunnel` | [0, 1] | data in/out only (no channels-layer control needed) |
| `alknet/ssh` | [0, 1] | SSH multiplexes internally, including its own control |
| `alk/call` (channel 0) | [0, 1] | call frames bidirectional via 0=in, 1=out |
| `alk/tty` | [0, 1, 2, 3, 4] | data in/out/err + control in/out |
| `alk/tunnel` | [0, 1] | data in/out only (no channels-layer control needed) |
| `alk/ssh` | [0, 1] | SSH multiplexes internally, including its own control |
The active set is declared at `channel/open` time (ADR-037 `stream_types`
field) and fixed for the channel's lifetime. A tunnel that wants keepalive
@@ -80,7 +80,7 @@ on the `BiStream` the channels layer gives them.
doesn't carry control. The "control isn't actually bidirectional" flaw
is fixed at the TTY layer, not the channels layer.
- **Recursive composition is literal.** A channel with ALPN
`alknet/channels` runs another channels demux on its `BiStream`. The
`alk/channels` runs another channels demux on its `BiStream`. The
outer layer strips its 8-byte header; the inner layer parses its own
8-byte header from the payload. Each level is the same shape —
`BiStream → accept_bi → N BiStreams`.
@@ -122,7 +122,7 @@ a `channel_id` was stripped before it saw the bytes.
The composition is uniform — the same shape at every level. This is SSH's
model (layered headers, each layer strips its own at its boundary),
applied to channels. A `alknet/channels`-inside-`alknet/channels`
applied to channels. A `alk/channels`-inside-`alk/channels`
recursive composition is the outer layer stripping its 8-byte header, the
inner layer parsing its own 8-byte header from the payload — same code,
same shape, each level.
@@ -193,7 +193,7 @@ handler's framing, carried transparently.
| field | offset | width | meaning |
|-------|--------|-------|---------|
| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). |
| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). |
| `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN` (16 MiB, matching TTY's cap — ADR-052 §5). |
The `stream_type` byte is **removed** from the channels header. The
@@ -231,8 +231,8 @@ ADR-077's two-mode TTY design (direct vs inside-channels) is reversed.
TTY's 5-byte format (`[stream_type:u8][length:u32][payload]`, ADR-052) is
TTY's internal format, used in *both* direct mode and inside-channels
mode. The two modes differ only in *where the `BiStream` comes from*
(a top-level `alknet/tty` connection vs a `channel/open` with ALPN
`alknet/tty`), not in *how TTY parses it*. The same `wire.rs` code runs
(a top-level `alk/tty` connection vs a `channel/open` with ALPN
`alk/tty`), not in *how TTY parses it*. The same `wire.rs` code runs
in both modes.
When TTY is inside channels, the channels layer strips its 8-byte header
@@ -272,7 +272,7 @@ TTY inside channels:
```
The composition is uniform — the same shape at every level. A
`alknet/channels`-inside-`alknet/channels` recursive composition is the
`alk/channels`-inside-`alk/channels` recursive composition is the
outer layer stripping its 8-byte header, the inner layer parsing its own
8-byte header from the payload — same code, same shape, each level.
@@ -284,7 +284,7 @@ outer layer stripping its 8-byte header, the inner layer parsing its own
transport leaf; this ADR settles the multiplexing layer above it).
- **`ProtocolHandler` trait shape** (ADR-002) — unchanged. Handlers
receive a `Connection` and call `accept_bi()`.
- **Channel 0 pre-negotiated as `alknet/call`** (ADR-036) — unchanged.
- **Channel 0 pre-negotiated as `alk/call`** (ADR-036) — unchanged.
Channel 0's chunks have `channel_id = 0` in the 8-byte header. The call
protocol's `EventEnvelope` framing is the payload; the channels layer
carries it transparently.
@@ -345,7 +345,7 @@ outer layer stripping its 8-byte header, the inner layer parsing its own
byte it doesn't use; no handler needs a second accessor
(`into_sub_streams`) to reach its sub-streams. The channels layer's
API surface is `accept_bi -> BiStream`, period.
- **Recursive composition is literal.** A `alknet/channels` channel runs
- **Recursive composition is literal.** A `alk/channels` channel runs
another channels demux on its `BiStream`. The outer layer strips its
8-byte header; the inner layer parses its own 8-byte header from the
payload. Same code, same shape, each level. This is a property, not a
@@ -452,7 +452,7 @@ breaking the wire format or the handler contract.
public stream constructor)
- ADR-008: `BidiStreamSource` trait (the extension point
`ChannelBidiStreamSource` implements; `accept_bi` yields `BiStream`)
- ADR-036: channel 0 pre-negotiated `alknet/call` (unchanged — channel 0's
- ADR-036: channel 0 pre-negotiated `alk/call` (unchanged — channel 0's
chunks have `channel_id = 0` in the 8-byte header; the call protocol's
framing is the payload)
- ADR-037: channel lifecycle operations (amended — `stream_types` field
@@ -1,4 +1,4 @@
# ADR-036: Channel 0 Is Pre-Negotiated `alknet/call`
# ADR-036: Channel 0 Is Pre-Negotiated `alk/call`
## Status
@@ -20,7 +20,7 @@ not a channels-layer concern; the channels layer routes by `channel_id`
only and yields a `BiStream` to the `CallAdapter`. The `CallAdapter`'s
`accept_bi()` returns one `BiStream` (per ADR-009); the call protocol
reads/writes `EventEnvelope` frames on it, exactly as on a top-level
`alknet/call` connection.
`alk/call` connection.
The body below describes the **original** (with `stream_types`) shape;
the amendment above is the operative decision. See ADR-035 for the
@@ -81,7 +81,7 @@ accept side (the dispatch loop has no stream to accept).
accept-side black hole: channel 0's `Connection` is actually driven
by a call dispatch loop.
5. **Top-level `alknet/call` connections are unchanged.**
5. **Top-level `alk/call` connections are unchanged.**
`CallConnection::new` and `Dispatcher::run_loop` keep the
stream-per-request model. Single-stream mode is only for channel 0
(and any future single-stream substrate that multiplexes call
@@ -115,13 +115,13 @@ A channels connection carries N logical channels. One of them must carry the
call protocol — the JSON-RPC layer that orchestrates channel lifecycle
(`channel/open`, `channel/close`, `channel/control`, `channel/resources`).
The question is how channel 0 relates to the call protocol: is it a special
"control plane" with its own framing, or is it just `alknet/call` pre-
"control plane" with its own framing, or is it just `alk/call` pre-
negotiated?
The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md`
§DP-2) recommends channel 0 is `alknet/call` pre-negotiated — no special
§DP-2) recommends channel 0 is `alk/call` pre-negotiated — no special
framing, no separate control-plane wire format. The call protocol runs on
channel 0 exactly as it runs on a top-level `alknet/call` QUIC connection.
channel 0 exactly as it runs on a top-level `alk/call` QUIC connection.
This matters because the alternative (a special control plane) would mean
the channels layer has its own JSON protocol for channel lifecycle, parallel
@@ -133,11 +133,11 @@ collapse.
## Decision
**Channel 0 is `alknet/call`, pre-negotiated.** Both sides of a channels
**Channel 0 is `alk/call`, pre-negotiated.** Both sides of a channels
connection know that `channel_id = 0` is routed to the `CallAdapter` without
an explicit `channel/open` exchange. The `CallAdapter` receives a
`Connection` backed by channel-0 chunk reassembly and dispatches operations
exactly as it does on a top-level `alknet/call` connection.
exactly as it does on a top-level `alk/call` connection.
### What this means concretely
@@ -163,7 +163,7 @@ exactly as it does on a top-level `alknet/call` connection.
`ChannelsAdapter` constructs channel 0's reassembly buffers, wraps them
as a `Connection` (via `Connection::from_source` with a
`ChannelBidiStreamSource` — ADR-008/074), and hands that `Connection` to
the `CallAdapter` — exactly as if `alknet/call` had been the top-level
the `CallAdapter` — exactly as if `alk/call` had been the top-level
ALPN. The `CallAdapter` is looked up in the same `HandlerRegistry` as
every other ALPN.
@@ -217,7 +217,7 @@ sub-streams.
## Door type
**One-way.** Channel 0's role as `alknet/call` pre-negotiated is a wire-
**One-way.** Channel 0's role as `alk/call` pre-negotiated is a wire-
format and protocol-structure commitment. Changing it after deployments
exist (e.g., to a special control plane) requires a version migration and
re-architecting the channel lifecycle operations. The reservation of
@@ -107,7 +107,7 @@ Request (`call.requested` on channel 0):
{
"operation": "channel/open",
"input": {
"alpn": "alknet/tty",
"alpn": "alk/tty",
"stream_types": [0, 1, 2, 3, 4],
"params": { "backend": "docker", "cmd": ["bash"], "container": "abc123" },
"direction": "initiator-to-responder"
@@ -119,7 +119,7 @@ Request (`call.requested` on channel 0):
|-------|------|---------|
| `alpn` | string | The ALPN the channel will carry. The responder looks this up in its `HandlerRegistry`. |
| `stream_types` | `[u8]` | Which sub-stream types this channel will use. E.g. `[0,1,2,3,4]` for TTY (data in/out/err + control in/out), `[0,1]` for a tunnel, `[0,1]` for channel 0 (call frames). See ADR-034 §stream_type decomposition. |
| `params` | object | ALPN-specific parameters. For `alknet/tty` this is the `NegotiateRequest`. For `alknet/tunnel` this is the target resource. The channels layer does not interpret `params`. |
| `params` | object | ALPN-specific parameters. For `alk/tty` this is the `NegotiateRequest`. For `alk/tunnel` this is the target resource. The channels layer does not interpret `params`. |
| `direction` | string | `initiator-to-responder` or `responder-to-initiator`. See "Direction semantics" below. |
Response (`call.responded`):
@@ -223,12 +223,12 @@ The responder registers a `StreamingHandler` that emits a
"output": {
"resources": [
{
"alpn": "alknet/tty",
"alpn": "alk/tty",
"backends": ["docker", "local"],
"access": { "required_scopes": ["tty:open"] }
},
{
"alpn": "alknet/tunnel",
"alpn": "alk/tunnel",
"targets": ["container:*", "service:postgres"],
"access": { "required_scopes_any": ["tunnel:open", "admin"] }
}
@@ -355,7 +355,7 @@ the underlying one-way commitment.
- ADR-035: channels pure channel multiplexing (amends this ADR —
`stream_types` field removed from `channel/open`; `stream_type` field
removed from `channel/control`; handler owns sub-stream multiplexing)
- ADR-036: channel 0 is pre-negotiated `alknet/call`
- ADR-036: channel 0 is pre-negotiated `alk/call`
- ADR-021: StreamingHandler for subscriptions (the machinery
`channel/resources/subscribe` uses — implemented and tested)
- ADR-020: abort cascade (subscription cancellation)
@@ -172,9 +172,9 @@ that both paths are available and the handler crate chooses.
### Recursive composition
A `ChannelBidiStreamSource` is a `BidiStreamSource`, and `Connection::
from_source` wraps it. A handler that is itself `alknet/channels` can open a
from_source` wraps it. A handler that is itself `alk/channels` can open a
sub-channels connection on a data channel. This is recursive composition:
`alknet/channels` inside `alknet/channels`. It is allowed (the `Connection`
`alk/channels` inside `alk/channels`. It is allowed (the `Connection`
abstraction permits it) but not a feature designed for — the primary use
case is one level of multiplexing. Recursive composition is a natural
consequence of the abstraction, not a goal.
@@ -30,7 +30,7 @@ The channels crate has two internal components, split by responsibility
Connection Internals):
1. **`ChannelsAdapter`** — implements `ProtocolHandler` for
`alknet/channels`. Its `handle()` receives one `Connection` (the
`alk/channels`. Its `handle()` receives one `Connection` (the
transport), reads 9-byte chunk headers, and routes each chunk. It is the
read/demux half.
@@ -53,12 +53,12 @@ contracts.
```rust
#[async_trait]
impl ProtocolHandler for ChannelsAdapter {
fn alpn(&self) -> &'static [u8] { b"alknet/channels" }
fn alpn(&self) -> &'static [u8] { b"alk/channels" }
async fn handle(&self, connection: Connection, auth: &AuthContext)
-> Result<(), HandlerError>
{
// 1. Channel 0 is pre-negotiated as alknet/call (ADR-036).
// 1. Channel 0 is pre-negotiated as alk/call (ADR-036).
// The first bidi stream the transport yields is channel 0.
let (send, recv) = connection.accept_bi().await?;
self.manager.preinstall_channel_0(send, recv, auth).await?;
@@ -79,7 +79,7 @@ The `preinstall_channel_0` step constructs the reassembly buffers for
`channel_id = 0` using stream_types [0, 1] (ADR-036), wraps them as a
`Connection` (via `Connection::from_source` with a `ChannelBidiStreamSource`
— ADR-038), and hands that `Connection` to the `CallAdapter` — exactly as if
`alknet/call` had been the top-level ALPN. The `CallAdapter` is looked up in
`alk/call` had been the top-level ALPN. The `CallAdapter` is looked up in
the same `HandlerRegistry` as every other ALPN.
`run_demux_loop` continues accepting bidi streams from the transport. For
@@ -212,7 +212,7 @@ per-peer policy).
### 6. Recursive channels do not bypass the cap
A recursive `alknet/channels`-inside-`alknet/channels` channel runs a
A recursive `alk/channels`-inside-`alk/channels` channel runs a
new `ChannelsAdapter` with a new `ChannelManager`. If the same
`ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`,
the inner channels are counted against the same identity. If a
@@ -91,8 +91,8 @@ spoke leg with `spoke_id` in the payload. The relay does not touch
| Spoke leg | `ChannelsAdapter` + `CallAdapter` (same) |
| Relay | Per-channel byte-forward tasks with `channel_id` rewrite |
The hub never runs a handler for `alknet/tty`, `alknet/ssh`, or
`alknet/tunnel`. It runs `alknet/channels` (the relay) and `alknet/call`
The hub never runs a handler for `alk/tty`, `alk/ssh`, or
`alk/tunnel`. It runs `alk/channels` (the relay) and `alk/call`
(for its own hub-level operations + translation). The endpoints at each end
do the protocol work.
@@ -102,7 +102,7 @@ do the protocol work.
registry / ownership store (ADR-011), queried via call operations on
channel 0. Channels doesn't touch this.
- **ACL at the hub:** does this browser's identity have `channel:open` scope
for `alknet/ssh` to `spoke-X`? `AccessControl::check` on `channel/open`,
for `alk/ssh` to `spoke-X`? `AccessControl::check` on `channel/open`,
run by the hub's `CallAdapter` before it forwards. Channels doesn't touch
this.
- **Relay lifecycle:** when a browser disconnects, the hub tears down the
@@ -168,7 +168,7 @@ auth path. The `channel_id` mapping strategy (`HashMap` per pair) is two-way
- ADR-034: outgoing-only X.509 and the three peer roles (browser identity
resolution)
- ADR-011: dynamic resource ownership (the ownership store the hub queries)
- ADR-036: channel 0 is pre-negotiated `alknet/call` (what the hub
- ADR-036: channel 0 is pre-negotiated `alk/call` (what the hub
terminates on each leg)
- ADR-037: channel lifecycle operations (what the hub translates)
- ADR-039: ChannelsAdapter and ChannelManager (the interface the relay uses)
@@ -62,7 +62,7 @@ surface:
primary constructor and the one-way-door API. Takes a pre-established
`Connection` (produced by any transport — TCP+TLS via `from_bidi`,
WebTransport `BiStream`, SSH `direct-tcpip`, a quinn connection, a
WebSocket carrying `alknet/channels` per ADR-044), installs channel 0,
WebSocket carrying `alk/channels` per ADR-044), installs channel 0,
spawns the demux/mux, returns the client. Mirrors the server-side
`ChannelsAdapter::handle(Connection)` (substrate-agnostic) and the
existing `CallClient::spawn_dispatch(Connection)` pattern.
@@ -130,7 +130,7 @@ impl ChannelClient {
/// Transport-agnostic primary constructor. Takes a pre-established
/// `Connection` (any transport — TCP+TLS via `from_bidi`,
/// WebTransport BiStream, SSH direct-tcpip, a quinn connection, a
/// WebSocket per ADR-044), installs channel 0 (alknet/call), spawns
/// WebSocket per ADR-044), installs channel 0 (alk/call), spawns
/// the demux/mux, and returns the client. Mirrors the server-side
/// `ChannelsAdapter::handle(Connection)`. This is the one-way-door
/// API surface — it must not be coupled to a transport (ADR-034,
@@ -139,7 +139,7 @@ impl ChannelClient {
-> Result<Self, ChannelError>;
/// QUIC convenience constructor. Dials a QUIC connection to `addr`
/// on ALPN `alknet/channels` (credentials → TLS handshake,
/// on ALPN `alk/channels` (credentials → TLS handshake,
/// ADR-034 verifier selection), then calls `from_connection`.
/// Additive and two-way-door — `connect_tcp_tls`,
/// `connect_webtransport`, etc. join it as transports are added.
@@ -29,7 +29,7 @@ single crate depending on both `alknet-core` and `alknet-call`. The
dependency on `alknet-call` arises because channel lifecycle operations
(`channel/open`, `channel/close`, `channel/control`,
`channel/resources/subscribe`) register on the call protocol's
`OperationRegistry`, and channel 0 is pre-negotiated as `alknet/call`
`OperationRegistry`, and channel 0 is pre-negotiated as `alk/call`
(ADR-036).
This creates two issues:
@@ -41,7 +41,7 @@ This creates two issues:
op registrations are the call-protocol coupling. Baking both into one
crate means any consumer that wants the multiplexer also pulls in the
call-protocol coupling, even if they don't need channel 0 to be
`alknet/call`.
`alk/call`.
2. **The "no special-casing for downstream crates" principle is
violated.** The user's constraint: "we don't want to be doing anything
@@ -91,24 +91,24 @@ The pure multiplexer. Contains:
- `ChannelManager` (the shared state — channel_id → ChannelState, but
**without** the `call_ops: Arc<OperationRegistry>` field; the manager is
ALPN-blind and call-protocol-blind)
- `ChannelsAdapter` (the `ProtocolHandler` on `alknet/channels` — the
- `ChannelsAdapter` (the `ProtocolHandler` on `alk/channels` — the
read/demux loop, substrate-agnostic per ADR-034 revised)
Depends on `alknet-core` only. No `alknet-call` dependency. No opinion about
what channel 0 carries — that's the consumer's concern.
The `ChannelsAdapter::handle` in `channels-core` does NOT preinstall channel
0 as `alknet/call`. It runs the demux loop and routes chunks by
0 as `alk/call`. It runs the demux loop and routes chunks by
`channel_id`. Channel 0 is just another channel; what ALPN it carries is
determined by the consumer (the `channels-call` crate pre-negotiates it as
`alknet/call`; a hypothetical other consumer could pre-negotiate it
`alk/call`; a hypothetical other consumer could pre-negotiate it
differently).
### `alknet-channels-call`
The call-protocol coupling. Contains:
- Channel 0 pre-negotiation as `alknet/call` (ADR-036) — the
- Channel 0 pre-negotiation as `alk/call` (ADR-036) — the
`preinstall_channel_0` logic that constructs channel 0's reassembly
buffers with stream_types [0, 1] and hands the `Connection` to the
`CallAdapter`.
@@ -222,7 +222,7 @@ in the type.
> `ConnectionCredentials`, not `CallCredentials`. `CallCredentials`
> couples the dial to the call protocol (its `auth_token` field is a
> call-protocol / hub-layer concept — bearer-token identity correlation
> for browsers and `alknet/register`, not a transport credential). The
> for browsers and `alk/register`, not a transport credential). The
> dial uses only the transport-identity dimensions (`local_identity` +
> `remote_identity`); those move to `ConnectionCredentials` in
> `alknet-core`. All three dial signatures unify on
@@ -350,9 +350,9 @@ removed rather than left as a vestigial enum. If `spawn_dispatch` ever
gains a failure path, a fresh error type is cleaner than retrofitting
this one.
### 6. `alknet/register` is a dialable ALPN (entry point, wire protocol deferred)
### 6. `alk/register` is a dialable ALPN (entry point, wire protocol deferred)
`AlknetClient::dial_quic` / `dial_tcp_tls` can dial the `alknet/register`
`AlknetClient::dial_quic` / `dial_tcp_tls` can dial the `alk/register`
ALPN — the native registration entry point, parallel to HTTP
registration (OQ-58) but without the HTTP layer. The connection is an
**entry point** (ADR-086 §2): accepted without an established peer
@@ -364,7 +364,7 @@ Two registration cases, both hub concerns and both optional:
- **Token registration** — a freshly-provisioned worker (docker,
vast.ai, runpod) generates its local identity, dials the hub on
`alknet/register`, presents the one-time registration token, and
`alk/register`, presents the one-time registration token, and
enrolls its key. The hub creates a `PeerEntry` and returns a session
credential.
- **No-token (open) registration** — a hub that hosts public services
@@ -372,13 +372,13 @@ Two registration cases, both hub concerns and both optional:
token. The enrollment creates a `PeerEntry` with no token
requirement.
The `alknet/register` **wire protocol** (the handshake on the
The `alk/register` **wire protocol** (the handshake on the
`Connection` after the dial — what frames the client sends, what the
hub returns) ties into the call crate's ACL and the OQ-58 enrollment
model. It is **deferred** to a dedicated ADR — this ADR names the ALPN
and its entry-point role; it does not specify the wire protocol. The
HTTP registration endpoint (OQ-58) remains the first implementation;
`alknet/register` is the native analogue that removes the HTTP
`alk/register` is the native analogue that removes the HTTP
dependency for workers that have no HTTP client.
### 7. OQ-55 is resolved for the native dial
@@ -467,7 +467,7 @@ several possible native clients sharing the same wire protocols.
on connection failure. `AlknetClient` provides both dials; the
fallback policy is a caller concern (or a future `dial_with_fallback`
helper — two-way-door).
- **`alknet/register` is named.** The native registration entry point
- **`alk/register` is named.** The native registration entry point
has a home in the ALPN registry, parallel to HTTP registration. The
wire protocol is deferred, but the ALPN and its entry-point role are
decided — a worker that has no HTTP client can register natively.
@@ -496,7 +496,7 @@ several possible native clients sharing the same wire protocols.
consistency is in the rule, not in the type. This is the same
exception as the server side (ADR-082, ADR-087 §3) — unavoidable,
and isolated to one dial method.
- **The `alknet/register` wire protocol is still deferred.** This ADR
- **The `alk/register` wire protocol is still deferred.** This ADR
names the ALPN and its role; the handshake protocol (token/no-token,
the frames, the `PeerEntry` creation, the session credential return)
is a separate ADR tied to OQ-58. A worker cannot register natively
@@ -520,7 +520,7 @@ boilerplate. The three-dial API (`dial_quic` / `dial_tcp_tls` /
`dial_iroh`) is one-way — changing the signatures after consumers exist
is a rewrite. The internal implementation (how `ConnectionCredentials`
feeds `TlsClientConfig::new`, how the iroh dial maps the
`Ed25519SecretKey`) is two-way. The `alknet/register` ALPN name is
`Ed25519SecretKey`) is two-way. The `alk/register` ALPN name is
one-way (wire compatibility); its wire protocol is two-way until the
dedicated ADR lands.
@@ -551,7 +551,7 @@ dedicated ADR lands.
(the take-over `AlknetClient` feeds)
- [ADR-022](022-call-protocol-client-and-adapter-contract.md) —
`CallClient::spawn_dispatch` (the take-over `AlknetClient` feeds)
- OQ-58 — worker registration flow (the HTTP path; `alknet/register`
- OQ-58 — worker registration flow (the HTTP path; `alk/register`
is the native analogue)
- `docs/architecture/crates/channels/channel-client.md` §"Relationship
to `AlknetClient`" — the deferral this ADR resolves
@@ -48,7 +48,7 @@ This preserves every invariant ADR-047 §4 was written to protect:
registered handler (Layer 0) is not on this path.
- **"Marked ops invoked outside a channels session" (ADR-047 §2):**
unchanged. A `channels/<alpn>/sub` op registered only on a channels
connection's overlay is not reachable on a bare `alknet/call`
connection's overlay is not reachable on a bare `alk/call`
connection (the overlay isn't attached there) — the dispatch path
returns `NOT_FOUND`, which is the correct behavior for "no channels
session" (the `channel:no_channels_session` error code from the
@@ -164,7 +164,7 @@ amendment is the normal mode here.
- **Gap F** (`channel_open` marker wire format) — the marker is a boolean
field `"channel_open": true` on the `services/schema` payload. The ALPN
is derivable from the op name (`channels/<alpn>/sub` → ALPN
`alknet/<alpn>`); the marker is the dispatch hint, not a carrier for
`alk/<alpn>`); the marker is the dispatch hint, not a carrier for
the ALPN string. `spec_to_json` emits it; `rebuild_spec_for` parses it.
- **Gap G** (`resource_id_path` ACL vs handler ownership) — complementary,
not redundant. The ACL check (via `resource_id_path` +
@@ -215,7 +215,7 @@ pub struct OperationSpec {
}
pub struct ChannelOpenSpec {
pub alpn: Cow<'static, str>, // e.g., "alknet/tty"; Cow so from_call can supply an owned String without Box::leak
pub alpn: Cow<'static, str>, // e.g., "alk/tty"; Cow so from_call can supply an owned String without Box::leak
}
```
@@ -234,7 +234,7 @@ machinery (Gap C).
**Marked ops invoked outside a channels session.** `channels/tty/sub` is
registered on the call registry — which means it's also visible/invocable
on a bare top-level `alknet/call` connection, where there is no
on a bare top-level `alk/call` connection, where there is no
`ChannelManager`. The open-op wrapper resolves `channel_manager()` at
invocation time via the extension trait (Gap E); if it returns `None`,
the wrapper returns `channel:no_channels_session`.
@@ -423,10 +423,10 @@ stays distinct (endpoint ALPN wrapping channels).
defaults `None`). Every spec-constructing site adds the field,
defaulting to `None`. This is a mechanical, additive change — the
`Option`/`None`-default keeps it non-breaking.
- The ALPN→path-segment mapping (`alknet/tty` → `tty`) needs pinning.
- The ALPN→path-segment mapping (`alk/tty` → `tty`) needs pinning.
The op name `channels/tty/sub` implies the path segment is `tty`;
non-`alknet/*` ALPNs need a rule. The rule: the path segment is the
ALPN with the `alknet/` prefix stripped; ALPNs without that prefix
non-`alk/*` ALPNs need a rule. The rule: the path segment is the
ALPN with the `alk/` prefix stripped; ALPNs without that prefix
use their full ALPN string as the path segment (rare case, two-way-
door).
- The `from_call` relay wrapper (Gap C) is a consumer concern, not in
+2 -2
View File
@@ -81,7 +81,7 @@ is the load-bearing piece the broker composes on.
| OQ-35 | `OperationEnv::channel_manager()` coupling | resolved | high | ADR-047 §4 — extension trait `ChannelOperationEnv` in `channels-call`; call crate stays free of channels types |
| OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" |
| OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it |
| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alknet/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) |
| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) |
| OQ-39 | `channel/control` control-handle surface | open | medium | The `channel/control` handler currently returns `channel:control_not_implemented`. The control-handle surface (per-channel control callbacks registered by ALPN crates, routing `message` to the handler's control handle for `channel_id`) is real design work — each ALPN crate needs a way to register a control callback, and the channels layer needs a control-handle registry keyed by `channel_id`. Deferred until an ALPN crate (TTY, tunnel) needs out-of-band control. |
| OQ-40 | `channel/resources/subscribe` live subscription | open | medium | The `channel/resources/subscribe` handler currently returns `channel:resources_not_implemented`. The live subscription aggregated from ALPN-crate resource enumerators (ADR-047 §6) requires each ALPN crate to provide a resource enumerator, and the channels layer to aggregate them into a live `Stream` that emits on any change. The current stub is a one-shot error; the real implementation is deferred until a consumer (hub, dashboard) needs live resource discovery. |
| OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). |
@@ -92,7 +92,7 @@ is the load-bearing piece the broker composes on.
|----|-------|--------|----------|------------|
| OQ-25 | BiStream type definition | resolved | high | ADR-005 — trait, Connection parameter |
| OQ-26 | AuthContext resolution timing | resolved | high | ADR-003 — hybrid resolution |
| OQ-27 | ALPN string naming convention | resolved | medium | ADR-004 — alknet/ prefix |
| OQ-27 | ALPN string naming convention | resolved | medium | ADR-004 — alk/ prefix |
| OQ-28 | Dynamic handler registration | resolved | low | ADR-019 — curated static, overlays dynamic |
| OQ-29 | Handler-level auth resolution observability | resolved | medium | set_identity() on Connection for observability |
| OQ-30 | Dynamic resource ownership | resolved | high | ADR-011 — OwnershipProvider, resource_id_path |
+2 -2
View File
@@ -59,7 +59,7 @@ pub struct OperationSpec {
}
pub struct ChannelOpenSpec {
pub alpn: &'static str, // e.g., "alknet/tty" — derivable from op name, carried for convenience
pub alpn: &'static str, // e.g., "alk/tty" — derivable from op name, carried for convenience
}
pub enum OperationType {
@@ -940,7 +940,7 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe
## Constraints
- The registry is **layered by trust boundary** (ADR-019). The curated layer (`Local` provenance) is immutable after construction — adding a `Local` op requires restarting the process, which re-enters the startup trust boundary. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic at their respective scopes (per-session, per-connection). The pre-ADR-019 blanket immutability claim was inherited by analogy from ADR-010's `HandlerRegistry` (ALPN-level) and did not apply to the operation registry — the TLS-config argument that justifies `HandlerRegistry` immutability does not touch the operation registry, which lives behind the single ALPN `alknet/call`.
- The registry is **layered by trust boundary** (ADR-019). The curated layer (`Local` provenance) is immutable after construction — adding a `Local` op requires restarting the process, which re-enters the startup trust boundary. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic at their respective scopes (per-session, per-connection). The pre-ADR-019 blanket immutability claim was inherited by analogy from ADR-010's `HandlerRegistry` (ALPN-level) and did not apply to the operation registry — the TLS-config argument that justifies `HandlerRegistry` immutability does not touch the operation registry, which lives behind the single ALPN `alk/call`.
- Operation specs use JSON Schema. The call protocol's external interface is always JSON. Internal handler dispatch is via `Handler` / `StreamingHandler` trait objects (ADR-021), not a binary RPC framework.
- `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer — not a prefix added to operation paths.
- The call protocol does not depend on any database. Operation specs are in-memory, populated at startup.