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

@@ -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