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

+6 -5
View File
@@ -50,7 +50,7 @@ This is the call + channels RPC crate — the unification of
`alknet-call` (structured JSON RPC: operations, streaming subscriptions, `alknet-call` (structured JSON RPC: operations, streaming subscriptions,
service discovery) and `alknet-channels` (multiplexing proxy: N logical service discovery) and `alknet-channels` (multiplexing proxy: N logical
channels over one transport stream, channel 0 pre-negotiated as channels over one transport stream, channel 0 pre-negotiated as
`alknet/call`). The conventions below apply to all work in `src/` and `alk/call`). The conventions below apply to all work in `src/` and
`tests/`. They mirror `.opencode/agents/implementation-specialist.md` `tests/`. They mirror `.opencode/agents/implementation-specialist.md`
§Project Conventions and are repeated here so they apply to every §Project Conventions and are repeated here so they apply to every
session, not just spawned implementation agents. session, not just spawned implementation agents.
@@ -205,8 +205,9 @@ If feature flags are added, also run `cargo test --all-features` and
alknet mono-repo (`/workspace/@alkdev/alknet`). The source alknet mono-repo (`/workspace/@alkdev/alknet`). The source
architecture docs were ported from architecture docs were ported from
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as `/workspace/@alkdev/alknet/docs/architecture/` and renumbered as
alkcall ADRs (001..047). The ALPN strings (`alknet/call`, alkcall ADRs (001..047). The ALPN strings (`alk/call`,
`alknet/channels`) are wire-stable and unchanged. `alk/channels`) are wire-stable going forward (renamed from
`alknet/` to `alk/` in v0.1.1, before the first published consumer).
- Key ADRs that inform this crate's design: - Key ADRs that inform this crate's design:
**Call protocol:** **Call protocol:**
@@ -240,7 +241,7 @@ If feature flags are added, also run `cargo test --all-features` and
- ADR-034 — channels wire format (8-byte chunk header; one-way door) - ADR-034 — channels wire format (8-byte chunk header; one-way door)
- ADR-035 — channels pure channel multiplexing (no `stream_type`, - ADR-035 — channels pure channel multiplexing (no `stream_type`,
`BiStream`-only, handler owns sub-stream multiplexing) `BiStream`-only, handler owns sub-stream multiplexing)
- ADR-036 — channel 0 pre-negotiated as `alknet/call` - ADR-036 — channel 0 pre-negotiated as `alk/call`
- ADR-037 — channel lifecycle operations (`channel/open`/`close`/ - ADR-037 — channel lifecycle operations (`channel/open`/`close`/
`control`/`resources/subscribe` on channel 0's call registry) `control`/`resources/subscribe` on channel 0's call registry)
- ADR-039 — `ChannelsAdapter` and `ChannelManager` (substrate-agnostic - ADR-039 — `ChannelsAdapter` and `ChannelManager` (substrate-agnostic
@@ -260,7 +261,7 @@ If feature flags are added, also run `cargo test --all-features` and
- ADR-002 — `ProtocolHandler` trait - ADR-002 — `ProtocolHandler` trait
- ADR-003 — auth as shared core (`IdentityProvider` in core, - ADR-003 — auth as shared core (`IdentityProvider` in core,
handlers extract credentials) handlers extract credentials)
- ADR-004 — ALPN string convention (`alknet/` prefix, one ALPN per - ADR-004 — ALPN string convention (`alk/` prefix, one ALPN per
connection) connection)
- ADR-005 — `BiStream` type definition (handlers receive - ADR-005 — `BiStream` type definition (handlers receive
`Connection`, not `BiStream`) `Connection`, not `BiStream`)
Generated
+1 -1
View File
@@ -27,7 +27,7 @@ dependencies = [
[[package]] [[package]]
name = "alkcall" name = "alkcall"
version = "0.1.0" version = "0.1.1"
dependencies = [ dependencies = [
"alktype", "alktype",
"async-trait", "async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "alkcall" name = "alkcall"
version = "0.1.0" version = "0.1.1"
edition = "2021" edition = "2021"
rust-version = "1.85" rust-version = "1.85"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+3 -3
View File
@@ -57,7 +57,7 @@ use alkcall::protocol::connection::CallConnection;
let connection = Connection::from_bidi( let connection = Connection::from_bidi(
transport_stream, transport_stream,
b"alknet/call".to_vec(), b"alk/call".to_vec(),
Some(remote_addr), Some(remote_addr),
); );
let conn = CallConnection::new(connection); let conn = CallConnection::new(connection);
@@ -74,7 +74,7 @@ use alkcall::core::Connection;
let connection = Connection::from_bidi( let connection = Connection::from_bidi(
transport_stream, transport_stream,
b"alknet/channels".to_vec(), b"alk/channels".to_vec(),
Some(remote_addr), Some(remote_addr),
); );
let client = ChannelClient::from_connection(connection).await?; let client = ChannelClient::from_connection(connection).await?;
@@ -113,7 +113,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 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 channel opener for TTY, and a channel acceptor for tunnels — all on the
same `alknet/channels` connection. same `alk/channels` connection.
## Documentation ## Documentation
+9 -9
View File
@@ -7,13 +7,13 @@ last_updated: 2026-08-12
The call + channels RPC crate. Structured JSON RPC (operations, streaming The call + channels RPC crate. Structured JSON RPC (operations, streaming
subscriptions, service discovery) and N-channel multiplexing over one 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 This crate unifies `alknet-call` and `alknet-channels` from the alknet
mono-repo, plus the vendored core types formerly in `alknet-core`. The mono-repo, plus the vendored core types formerly in `alknet-core`. The
source architecture docs were ported from source architecture docs were ported from
`/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall `/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. are wire-stable and unchanged — see ADR-004.
## Documents ## 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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 ## 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. access to the entire operation registry.
2. **Protocol is symmetric**: Both sides can initiate calls. Producer/ 2. **Protocol is symmetric**: Both sides can initiate calls. Producer/
consumer, not server/client. consumer, not server/client.
@@ -135,7 +135,7 @@ questions affecting this crate:
See ADR-024. See ADR-024.
8. **Streams are streams**: Every channel is a BiStream. The handler 8. **Streams are streams**: Every channel is a BiStream. The handler
owns its sub-stream multiplexing. See ADR-035. 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. channel 0. See ADR-036, ADR-037.
10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk 10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk
header are one-way doors. See ADR-014, ADR-034. 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 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 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. role; they just don't prevent any combination.
Within a single connection, direction is independent of role: 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 (e.g. `trader_client.status().await` instead of
`call_connection.call("trader/status", ...).await`). `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 needed. It's the composition layer — the only place that knows about
network topology, peer routing, and which protocol crates are wired in. network topology, peer routing, and which protocol crates are wired in.
Protocol crates never import a QUIC or TLS dependency. 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 │ │ Direct ALPN │ │ Through channels │
│ (alknet/tty) │ │ (alknet/channels) │ │ (alk/tty) │ │ (alk/channels) │
│ │ │ │ │ │ │ │
│ TtyAdapter │ │ OpenHandler │ │ TtyAdapter │ │ OpenHandler │
│ impl Protocol │ │ (registered via │ │ 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 # 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 ## Documents
@@ -20,7 +20,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi
| ADR | Title | Relevance | | 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 | | [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) | | [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 | | [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) | | [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 | | [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 | | [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 | | [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 | | [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 | | [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 ## 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. 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. 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. 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 # 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 ## 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 ## Why
@@ -104,13 +104,13 @@ below.
### CallConnection ### 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 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 imported-ops overlay (Layer 2, ADR-019) — the set of `from_call`-imported
operations discovered when the connection was established. operations discovered when the connection was established.
```rust ```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). /// opened). Holds the connection's Layer 2 overlay (imported ops).
pub struct CallConnection { pub struct CallConnection {
/// The underlying transport Connection (from endpoint.accept, /// 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()`. - **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. - **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. - **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 ### 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) | | 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 | | 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 | | 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 | | 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 | | 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 { impl ChannelClient {
/// Construct a `ChannelClient` over a pre-established transport /// 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-agnostic primary constructor: the caller (or a
/// transport-specific dial helper) produces the `Connection` — /// transport-specific dial helper) produces the `Connection` —
/// via `Connection::from_bidi` (TCP+TLS, WebTransport, SSH /// via `Connection::from_bidi` (TCP+TLS, WebTransport, SSH
/// `direct-tcpip`), a quinn connection, or any other `AsyncRead + /// `direct-tcpip`), a quinn connection, or any other `AsyncRead +
/// AsyncWrite` source — and this method takes over: installs /// 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 /// the client. Mirrors the server side's transport-agnostic
/// `ChannelsAdapter::handle(Connection)` and /// `ChannelsAdapter::handle(Connection)` and
/// `CallClient::spawn_dispatch(Connection)`. /// `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: takes over channels establishment. The transport is the caller's concern:
`Connection::from_bidi(tls_stream, ...)` for TCP+TLS, a quinn `Connection`, `Connection::from_bidi(tls_stream, ...)` for TCP+TLS, a quinn `Connection`,
a WebTransport `BiStream`, an SSH `direct-tcpip` channel wrapped via 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 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. 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>/sub` | `Sub` | consumer (subscribes) | producer (streams) | server → client |
| `channels/<alpn>/pub` | `Pub` | producer (publishes) | consumer (receives) | client → server | | `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 prefix stripped (ADR-047 §"Negative"). An ALPN may register one or both
ops; separate op types → separate ACLs. The op spec carries the ops; separate op types → separate ACLs. The op spec carries the
`channel_open: Option<ChannelOpenSpec>` marker (ADR-047 §2) — 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 The `input` is ALPN-specific params (the former `params` field, now the
op's `input_schema`). For `alknet/tty` this is the `NegotiateRequest`; op's `input_schema`). For `alk/tty` this is the `NegotiateRequest`;
for `alknet/tunnel` this is the target resource. The channels layer does for `alk/tunnel` this is the target resource. The channels layer does
not interpret `input`. not interpret `input`.
Response (`call.responded`): Response (`call.responded`):
@@ -174,11 +174,11 @@ The responder registers a `StreamingHandler` that emits a
"output": { "output": {
"resources": [ "resources": [
{ {
"alpn": "alknet/tty", "alpn": "alk/tty",
"backends": ["docker", "local"] "backends": ["docker", "local"]
}, },
{ {
"alpn": "alknet/tunnel", "alpn": "alk/tunnel",
"targets": ["container:*", "service:postgres"] "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 ### 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 new `ChannelsAdapter` with a new `ChannelManager`. If the same
`ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`, `ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`,
the inner channels are counted against the same identity. Recursion 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 `channel/control` — it's a call operation, translated, not
byte-forwarded. byte-forwarded.
The hub never runs a handler for `alknet/tty`, `alknet/ssh`, or The hub never runs a handler for `alk/tty`, `alk/ssh`, or
`alknet/tunnel`. It runs `alknet/channels` (the relay) and `alknet/call` `alk/tunnel`. It runs `alk/channels` (the relay) and `alk/call`
(for its own hub-level operations + translation). The `channel_open` (for its own hub-level operations + translation). The `channel_open`
marker (ADR-047 §2) is how the hub recognizes a channel-open op during 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 `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) | | [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 | | [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 | | [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) | | [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 | | [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 # 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, 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 (ADR-036); every other channel is opened dynamically via call operations on
channel 0 and routed through the same `HandlerRegistry` as top-level channel 0 and routed through the same `HandlerRegistry` as top-level
connections. The channels layer is a re-framing proxy — it converts between 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 | | [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-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-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-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 | | [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 | | [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 | | [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 | | [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) | | [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 | | [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 `HandlerRegistry`. See [overview.md](overview.md) and ADR-034 (as
amended by ADR-035). 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 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/open`, `channel/close`, `channel/control`,
`channel/resources/subscribe`) are call operations on channel 0's `channel/resources/subscribe`) are call operations on channel 0's
`OperationRegistry`, gated by the existing `AccessControl::check`. No `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 | | 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). | | `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 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 ```rust
#[async_trait] #[async_trait]
impl ProtocolHandler for ChannelsAdapter { 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) async fn handle(&self, connection: Connection, auth: &AuthContext)
-> Result<(), HandlerError> -> Result<(), HandlerError>
+2 -2
View File
@@ -130,8 +130,8 @@ validated shape and the `StreamBidiStreamSource` yield-once contract
A `ChannelBidiStreamSource` is a `BidiStreamSource`, and A `ChannelBidiStreamSource` is a `BidiStreamSource`, and
`Connection::from_source` wraps it. A handler that is itself `Connection::from_source` wraps it. A handler that is itself
`alknet/channels` can open a sub-channels connection on a data channel — `alk/channels` can open a sub-channels connection on a data channel —
`alknet/channels` inside `alknet/channels`. The outer layer strips its `alk/channels` inside `alk/channels`. The outer layer strips its
8-byte header; the inner layer parses its own 8-byte header from the 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 payload. Each level is the same shape: `BiStream → accept_bi → N
BiStreams`. The recursion is unbounded and uniform at every level. BiStreams`. The recursion is unbounded and uniform at every level.
+16 -16
View File
@@ -8,14 +8,14 @@ last_updated: 2026-07-18
## What ## What
`alknet-channels` is a multiplexing proxy crate. It implements `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 bidirectional transport stream, reads 8-byte chunk headers, and routes each
chunk's payload to the right logical channel. Each channel is reassembled chunk's payload to the right logical channel. Each channel is reassembled
into a `BiStream` (a concrete `AsyncRead + AsyncWrite` newtype, per into a `BiStream` (a concrete `AsyncRead + AsyncWrite` newtype, per
ADR-009) and presented to its handler as a `Connection` — the handler ADR-009) and presented to its handler as a `Connection` — the handler
doesn't know it's inside a channels connection. 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 is opened dynamically via `channel/open` on channel 0 (ADR-037) and routed
through the same `HandlerRegistry` as top-level connections. The channels through the same `HandlerRegistry` as top-level connections. The channels
layer does no protocol work itself — it is a re-framing proxy that converts 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 ### 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 Browser ──WebTransport──► Hub ──QUIC──► Spoke
alknet/channels alknet/channels alk/channels alk/channels
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ ch0: call │ │ ch0: call │ │ ch0: call │ │ ch0: call │
│ ch1: tty │ relay │ ch1: tty │ │ 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, 2. **One multiplexing model, not three.** Connection-level, stream-level,
and sub-stream-level all become channels chunks. and sub-stream-level all become channels chunks.
3. **The call protocol orchestrates from inside.** Channel 0 is 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 `AccessControl`, and `forwarded_for` machinery govern channel lifecycle
with no new auth. 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 `STREAM_CTRL_OUT` — ADR-052 amended by Phase 7). The channels layer
doesn't carry control. doesn't carry control.
- **Recursive composition is literal.** A channel with ALPN - **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 outer layer strips its 8-byte header; the inner layer parses its own
8-byte header from the payload. 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): The crate has two internal components (ADR-039):
- **`ChannelsAdapter`** — implements `ProtocolHandler` for - **`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 chunk headers, and routes chunks to the `ChannelManager`. The read/demux
half. half.
- **`ChannelManager`** — the shared state. Holds `channel_id → - **`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 call-protocol-blind, transport-blind. This is where the "streams are
streams" insight lives. streams" insight lives.
- **`channels` (call coupling)** — channel 0 pre-negotiation as - **`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), on the call protocol's `OperationRegistry`, `ChannelCore` (ADR-047 §3),
and `ChannelClient` (ADR-043). This is where the call-protocol coupling and `ChannelClient` (ADR-043). This is where the call-protocol coupling
lives, isolated from the pure multiplexer. lives, isolated from the pure multiplexer.
@@ -155,7 +155,7 @@ A worker is any crate that uses `ChannelClient` to dial. There are no
## ALPN ## 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 per channels connection; the connection carries N logical channels, each
with its own ALPN (negotiated via `channel/open`). with its own ALPN (negotiated via `channel/open`).
@@ -166,11 +166,11 @@ stream:
| Transport | How | | Transport | How |
|-----------|-----| |-----------|-----|
| QUIC bidi stream | `alknet/channels` ALPN on a QUIC connection; one bidi stream carries all channels | | QUIC bidi stream | `alk/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 | | TCP+TLS | `alk/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`) | | 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) | | 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` The same wire format, the same chunk reassembly, the same `Connection`
abstraction. The transport is a parameter, not a design constraint. 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 | | 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 | | 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 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 ### alknet-call
The call protocol runs on channel 0 exactly as on a top-level 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 backed by channel-0 chunk reassembly and dispatches operations — it
doesn't know it's inside channels. The call protocol's `EventEnvelope` doesn't know it's inside channels. The call protocol's `EventEnvelope`
framing (ADR-014) is the channels payload; the channels layer carries 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 | | [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 | | [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 | | [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) | | [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 | | [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 # 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 multiplexes N logical channels over a single ordered, reliable
bidirectional transport stream. ADR-034 (amended by ADR-035) is the bidirectional transport stream. ADR-034 (amended by ADR-035) is the
decision; this doc specifies the format and the wire-level invariants. 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 | | 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`. | | `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 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 exactly 8 bytes, so the demux can always resync by reading the next
8-byte header. 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 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` is routed to the `CallAdapter` without an explicit `channel/open`
exchange. 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 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), 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 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, inner layer parsing its own 8-byte header from the payload — same code,
same shape, each level. 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`: This document specifies three components, all in `alknet-call`:
1. **`CallClient`** — takes over an established transport `Connection` 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; a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary;
dial lives in `AlknetClient` per ADR-045); the dispatch loop is dial lives in `AlknetClient` per ADR-045); the dispatch loop is
shared with the server-side `CallAdapter` shared with the server-side `CallAdapter`
@@ -88,7 +88,7 @@ cross-peer dissolved / same-peer stays, DC-4→OQ-26).
### CallClient ### CallClient
`CallClient` takes over an established transport `Connection` on ALPN `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 `CallConnection`. The `CallConnection` type is already implemented
(`call-protocol.md` §"CallConnection") — it wraps an established (`call-protocol.md` §"CallConnection") — it wraps an established
`Connection` and holds the Layer 2 imported-ops overlay. `CallClient` `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; pub fn new(registry: Arc<OperationRegistry>, idp: Arc<dyn IdentityProvider>) -> Self;
/// Transport-agnostic primary constructor. Takes a pre-established /// 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 /// `from_quinn`, TCP+TLS via `from_bidi`, WebTransport, SSH
/// `direct-tcpip`, a WebSocket), spawns the shared dispatch loop, /// `direct-tcpip`, a WebSocket), spawns the shared dispatch loop,
/// and returns a live `CallConnection`. Mirrors the server-side /// 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) Connects to hub as a CallClient (outbound connection — runner pattern)
Hub (central server): Hub (central server):
Runs CallAdapter (server) on alknet/call (already implemented) Runs CallAdapter (server) on alk/call (already implemented)
When the container service connects: When the container service connects:
hub runs from_call → discovers /container/* via services/list + services/schema hub runs from_call → discovers /container/* via services/list + services/schema
registers them as FromCall provenance (leaf, forwarding handlers) in the 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 **Why the container service doesn't need alknet-ssh**: under the call
protocol, the container service is a `CallClient` that dials the hub's 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 the loop. SSH port
forwarding becomes the *transitional* mechanism for targets that can't run a forwarding becomes the *transitional* mechanism for targets that can't run a
call-protocol client (the `alknet-ssh` phase-0 findings document this 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 - WASM story is clean: handlers receive byte streams, protocol parsers that operate on bytes compile to WASM
**Negative:** **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) - 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) - Debugging requires knowing which ALPN was negotiated (mitigated by logging at the endpoint level)
## References ## References
@@ -24,7 +24,7 @@ A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInt
```rust ```rust
#[async_trait] #[async_trait]
pub trait ProtocolHandler: Send + Sync + 'static { 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]; fn alpn(&self) -> &'static [u8];
/// Handle an incoming connection (revised by ADR-005 to receive /// 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 ### ALPN String Convention
Custom ALPN strings use the `alknet/` prefix: Custom ALPN strings use the `alk/` prefix:
| ALPN | Handler | Type | | ALPN | Handler | Type |
|------|---------|------| |------|---------|------|
| `alknet/ssh` | SshAdapter | Custom | | `alk/ssh` | SshAdapter | Custom |
| `alknet/call` | CallAdapter | Custom | | `alk/call` | CallAdapter | Custom |
| `alknet/git` | GitAdapter | Custom | | `alk/git` | GitAdapter | Custom |
| `alknet/sftp` | SftpAdapter | Custom | | `alk/sftp` | SftpAdapter | Custom |
| `alknet/msg` | MessageAdapter | Custom | | `alk/msg` | MessageAdapter | Custom |
| `alknet/http` | HttpAdapter | Custom | | `alk/http` | HttpAdapter | Custom |
| `alknet/dns` | DnsAdapter | Custom | | `alk/dns` | DnsAdapter | Custom |
| `h3` | WebTransport → alknet/http | Standard (IANA) | | `h3` | WebTransport → alk/http | Standard (IANA) |
| `h2` | HTTP/2 → alknet/http | Standard (IANA) | | `h2` | HTTP/2 → alk/http | Standard (IANA) |
| `http/1.1` | HTTP/1.1 → alknet/http | Standard (IANA) | | `http/1.1` | HTTP/1.1 → alk/http | Standard (IANA) |
Rules: 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 - 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 - ALPN strings are compile-time constants in each handler's `alpn()` method — no runtime registration of new ALPN strings
### Connection Model ### 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. **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: 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 - 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 - The endpoint logs the negotiated ALPN for each connection for observability
## Consequences ## Consequences
**Positive:** **Positive:**
- Simple model: one connection, one protocol — no multiplexing layer needed inside a connection - 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 - No version negotiation complexity — incompatible versions get new ALPN strings
- QUIC connection multiplexing means multiple ALPN connections share the same UDP flow - QUIC connection multiplexing means multiple ALPN connections share the same UDP flow
**Negative:** **Negative:**
- Multiple ALPNs require multiple connections — a full-featured client might have 3-5 QUIC connections open simultaneously - 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 - 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 ## References
@@ -68,4 +68,16 @@ This means:
- ADR-002: ProtocolHandler trait - ADR-002: ProtocolHandler trait
- OQ-03: ALPN string naming convention (resolved by this ADR) - OQ-03: ALPN string naming convention (resolved by this ADR)
- OQ-06: Server-side ALPN vs client-side ALPN (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 - `alknet-ssh` is unblocked: the SSH handler wraps each russh channel via
`from_stream` and dispatches by channel-type (treated as the ALPN string) `from_stream` and dispatches by channel-type (treated as the ALPN string)
through `HandlerRegistry`. One SSH connection carries heterogeneous 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. QUIC's per-connection ALPN doesn't give natively.
- WebTransport stream dispatch is unblocked: the WT handler wraps each WT - WebTransport stream dispatch is unblocked: the WT handler wraps each WT
stream via `from_stream` and dispatches through `HandlerRegistry` (the 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 ### `BiStream` over WebSocket enables "VPN-like without being a VPN" in v1
The `webtransport.md` spec describes the "VPN-like without being a VPN" 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 handler hands each bidi stream to `SshAdapter::handle` as a
`Connection`, the browser's WASM SSH parser speaks SSH over the `Connection`, the browser's WASM SSH parser speaks SSH over the
stream. WebTransport is deferred per ADR-044. 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 - **Browsers** — no raw-key support, no client cert the hub can
fingerprint; the browser authenticates via a bearer token over fingerprint; the browser authenticates via a bearer token over
HTTP/WebSocket, and the hub's `IdentityProvider` resolves it. 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 in with no prior peer relationship; a registration token (or open
registration) establishes identity, not a TLS fingerprint. registration) establishes identity, not a TLS fingerprint.
@@ -106,7 +106,7 @@ dimensions every dial consumes:
/// (fingerprint, driving verifier selection per ADR-034). /// (fingerprint, driving verifier selection per ADR-034).
/// ///
/// This is NOT the call-protocol credential bundle. The call-protocol /// 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 /// is a per-request field on `call.requested` payloads, not a
/// transport credential. It stays in the call-protocol layer. /// transport credential. It stays in the call-protocol layer.
pub struct ConnectionCredentials { pub struct ConnectionCredentials {
@@ -235,7 +235,7 @@ credential bundle is needed):**
bearer token to an `Identity` via `IdentityProvider::resolve_from_token` bearer token to an `Identity` via `IdentityProvider::resolve_from_token`
at the HTTP boundary. The call protocol receives the `Identity`, not at the HTTP boundary. The call protocol receives the `Identity`, not
the token. 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 endpoint) — a client not yet associated with a hub presents a
one-time registration token; the hub creates a `PeerEntry` (a new one-time registration token; the hub creates a `PeerEntry` (a new
identity based on the fingerprint). Outbound, the vault manages the 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 ## 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 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 - irpc provides: operation registry, schema discovery, frame encoding/decoding, request/response routing, streaming
@@ -6,7 +6,7 @@ Accepted
## Context ## 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. 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: 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. 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. 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 ## Consequences
@@ -17,9 +17,9 @@ as sharing one immutability argument:
2. **The call protocol's `OperationRegistry`** (operation name → 2. **The call protocol's `OperationRegistry`** (operation name →
`HandlerRegistration`). This lives *inside* the `CallAdapter`, which is one `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 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 `operation-registry.md` stated the operation registry "is immutable after
construction… consistent with OQ-04 and ADR-010." That inheritance was by 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 ### 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 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. `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 It has its own operation registry to dispatch incoming calls from the remote
@@ -20,7 +20,7 @@ identified the gap.
## Context ## Context
ADR-022 §1 established that a `CallClient` — which opens an outbound 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 calls from the remote side." The ADR left the *registry scope* as an explicit
two-way door in its Consequences: 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. `HandlerRegistration`, and `OperationAdapter` trait) was specced.
**Clarification:** `alknet-call` is both a handler crate (it implements **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 crate that `alknet-agent`, `alknet-napi`, and `alknet-http` consume for
the operation registry, adapter contract, and call client. The "no the operation registry, adapter contract, and call client. The "no
handler crate depends on another handler crate" rule applies to peer handler crate depends on another handler crate" rule applies to peer
@@ -38,7 +38,7 @@ rationale and the cross-ADR impacts.
## Context ## Context
`alknet-channels` is a multiplexing proxy: a `ProtocolHandler` on `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 ALPN, over transport stream(s). The wire format is the substrate that makes
this work. this work.
@@ -105,7 +105,7 @@ preserving the framing-disambiguation soundness property.
| field | width | meaning | | 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. | | `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). | | `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 | | ALPN | Active stream_types | Why |
|------|---------------------|-----| |------|---------------------|-----|
| `alknet/call` (channel 0) | [0, 1] | call frames bidirectional via 0=in, 1=out | | `alk/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 | | `alk/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) | | `alk/tunnel` | [0, 1] | data in/out only (no channels-layer control needed) |
| `alknet/ssh` | [0, 1] | SSH multiplexes internally, including its own control | | `alk/ssh` | [0, 1] | SSH multiplexes internally, including its own control |
The active set is declared at `channel/open` time (ADR-037 `stream_types` 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 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 doesn't carry control. The "control isn't actually bidirectional" flaw
is fixed at the TTY layer, not the channels layer. is fixed at the TTY layer, not the channels layer.
- **Recursive composition is literal.** A channel with ALPN - **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 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 — 8-byte header from the payload. Each level is the same shape —
`BiStream → accept_bi → N BiStreams`. `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 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), 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 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, inner layer parsing its own 8-byte header from the payload — same code,
same shape, each level. same shape, each level.
@@ -193,7 +193,7 @@ handler's framing, carried transparently.
| field | offset | width | meaning | | 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). | | `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 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 5-byte format (`[stream_type:u8][length:u32][payload]`, ADR-052) is
TTY's internal format, used in *both* direct mode and inside-channels TTY's internal format, used in *both* direct mode and inside-channels
mode. The two modes differ only in *where the `BiStream` comes from* mode. The two modes differ only in *where the `BiStream` comes from*
(a top-level `alknet/tty` connection vs a `channel/open` with ALPN (a top-level `alk/tty` connection vs a `channel/open` with ALPN
`alknet/tty`), not in *how TTY parses it*. The same `wire.rs` code runs `alk/tty`), not in *how TTY parses it*. The same `wire.rs` code runs
in both modes. in both modes.
When TTY is inside channels, the channels layer strips its 8-byte header 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 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 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. 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). transport leaf; this ADR settles the multiplexing layer above it).
- **`ProtocolHandler` trait shape** (ADR-002) — unchanged. Handlers - **`ProtocolHandler` trait shape** (ADR-002) — unchanged. Handlers
receive a `Connection` and call `accept_bi()`. 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 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 protocol's `EventEnvelope` framing is the payload; the channels layer
carries it transparently. 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 byte it doesn't use; no handler needs a second accessor
(`into_sub_streams`) to reach its sub-streams. The channels layer's (`into_sub_streams`) to reach its sub-streams. The channels layer's
API surface is `accept_bi -> BiStream`, period. 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 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 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 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) public stream constructor)
- ADR-008: `BidiStreamSource` trait (the extension point - ADR-008: `BidiStreamSource` trait (the extension point
`ChannelBidiStreamSource` implements; `accept_bi` yields `BiStream`) `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 chunks have `channel_id = 0` in the 8-byte header; the call protocol's
framing is the payload) framing is the payload)
- ADR-037: channel lifecycle operations (amended — `stream_types` field - 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 ## 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 only and yields a `BiStream` to the `CallAdapter`. The `CallAdapter`'s
`accept_bi()` returns one `BiStream` (per ADR-009); the call protocol `accept_bi()` returns one `BiStream` (per ADR-009); the call protocol
reads/writes `EventEnvelope` frames on it, exactly as on a top-level 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 body below describes the **original** (with `stream_types`) shape;
the amendment above is the operative decision. See ADR-035 for the 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 accept-side black hole: channel 0's `Connection` is actually driven
by a call dispatch loop. 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 `CallConnection::new` and `Dispatcher::run_loop` keep the
stream-per-request model. Single-stream mode is only for channel 0 stream-per-request model. Single-stream mode is only for channel 0
(and any future single-stream substrate that multiplexes call (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 call protocol — the JSON-RPC layer that orchestrates channel lifecycle
(`channel/open`, `channel/close`, `channel/control`, `channel/resources`). (`channel/open`, `channel/close`, `channel/control`, `channel/resources`).
The question is how channel 0 relates to the call protocol: is it a special 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? negotiated?
The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md` 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 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 This matters because the alternative (a special control plane) would mean
the channels layer has its own JSON protocol for channel lifecycle, parallel the channels layer has its own JSON protocol for channel lifecycle, parallel
@@ -133,11 +133,11 @@ collapse.
## Decision ## 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 connection know that `channel_id = 0` is routed to the `CallAdapter` without
an explicit `channel/open` exchange. The `CallAdapter` receives a an explicit `channel/open` exchange. The `CallAdapter` receives a
`Connection` backed by channel-0 chunk reassembly and dispatches operations `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 ### 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 `ChannelsAdapter` constructs channel 0's reassembly buffers, wraps them
as a `Connection` (via `Connection::from_source` with a as a `Connection` (via `Connection::from_source` with a
`ChannelBidiStreamSource` — ADR-008/074), and hands that `Connection` to `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 ALPN. The `CallAdapter` is looked up in the same `HandlerRegistry` as
every other ALPN. every other ALPN.
@@ -217,7 +217,7 @@ sub-streams.
## Door type ## 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 format and protocol-structure commitment. Changing it after deployments
exist (e.g., to a special control plane) requires a version migration and exist (e.g., to a special control plane) requires a version migration and
re-architecting the channel lifecycle operations. The reservation of re-architecting the channel lifecycle operations. The reservation of
@@ -107,7 +107,7 @@ Request (`call.requested` on channel 0):
{ {
"operation": "channel/open", "operation": "channel/open",
"input": { "input": {
"alpn": "alknet/tty", "alpn": "alk/tty",
"stream_types": [0, 1, 2, 3, 4], "stream_types": [0, 1, 2, 3, 4],
"params": { "backend": "docker", "cmd": ["bash"], "container": "abc123" }, "params": { "backend": "docker", "cmd": ["bash"], "container": "abc123" },
"direction": "initiator-to-responder" "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`. | | `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. | | `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. | | `direction` | string | `initiator-to-responder` or `responder-to-initiator`. See "Direction semantics" below. |
Response (`call.responded`): Response (`call.responded`):
@@ -223,12 +223,12 @@ The responder registers a `StreamingHandler` that emits a
"output": { "output": {
"resources": [ "resources": [
{ {
"alpn": "alknet/tty", "alpn": "alk/tty",
"backends": ["docker", "local"], "backends": ["docker", "local"],
"access": { "required_scopes": ["tty:open"] } "access": { "required_scopes": ["tty:open"] }
}, },
{ {
"alpn": "alknet/tunnel", "alpn": "alk/tunnel",
"targets": ["container:*", "service:postgres"], "targets": ["container:*", "service:postgres"],
"access": { "required_scopes_any": ["tunnel:open", "admin"] } "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 — - ADR-035: channels pure channel multiplexing (amends this ADR —
`stream_types` field removed from `channel/open`; `stream_type` field `stream_types` field removed from `channel/open`; `stream_type` field
removed from `channel/control`; handler owns sub-stream multiplexing) 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 - ADR-021: StreamingHandler for subscriptions (the machinery
`channel/resources/subscribe` uses — implemented and tested) `channel/resources/subscribe` uses — implemented and tested)
- ADR-020: abort cascade (subscription cancellation) - ADR-020: abort cascade (subscription cancellation)
@@ -172,9 +172,9 @@ that both paths are available and the handler crate chooses.
### Recursive composition ### Recursive composition
A `ChannelBidiStreamSource` is a `BidiStreamSource`, and `Connection:: 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: 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 abstraction permits it) but not a feature designed for — the primary use
case is one level of multiplexing. Recursive composition is a natural case is one level of multiplexing. Recursive composition is a natural
consequence of the abstraction, not a goal. consequence of the abstraction, not a goal.
@@ -30,7 +30,7 @@ The channels crate has two internal components, split by responsibility
Connection Internals): Connection Internals):
1. **`ChannelsAdapter`** — implements `ProtocolHandler` for 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 transport), reads 9-byte chunk headers, and routes each chunk. It is the
read/demux half. read/demux half.
@@ -53,12 +53,12 @@ contracts.
```rust ```rust
#[async_trait] #[async_trait]
impl ProtocolHandler for ChannelsAdapter { 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) async fn handle(&self, connection: Connection, auth: &AuthContext)
-> Result<(), HandlerError> -> 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. // The first bidi stream the transport yields is channel 0.
let (send, recv) = connection.accept_bi().await?; let (send, recv) = connection.accept_bi().await?;
self.manager.preinstall_channel_0(send, recv, auth).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 `channel_id = 0` using stream_types [0, 1] (ADR-036), wraps them as a
`Connection` (via `Connection::from_source` with a `ChannelBidiStreamSource` `Connection` (via `Connection::from_source` with a `ChannelBidiStreamSource`
— ADR-038), and hands that `Connection` to the `CallAdapter` — exactly as if — 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. the same `HandlerRegistry` as every other ALPN.
`run_demux_loop` continues accepting bidi streams from the transport. For `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 ### 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 new `ChannelsAdapter` with a new `ChannelManager`. If the same
`ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`, `ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`,
the inner channels are counted against the same identity. If a 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) | | Spoke leg | `ChannelsAdapter` + `CallAdapter` (same) |
| Relay | Per-channel byte-forward tasks with `channel_id` rewrite | | Relay | Per-channel byte-forward tasks with `channel_id` rewrite |
The hub never runs a handler for `alknet/tty`, `alknet/ssh`, or The hub never runs a handler for `alk/tty`, `alk/ssh`, or
`alknet/tunnel`. It runs `alknet/channels` (the relay) and `alknet/call` `alk/tunnel`. It runs `alk/channels` (the relay) and `alk/call`
(for its own hub-level operations + translation). The endpoints at each end (for its own hub-level operations + translation). The endpoints at each end
do the protocol work. do the protocol work.
@@ -102,7 +102,7 @@ do the protocol work.
registry / ownership store (ADR-011), queried via call operations on registry / ownership store (ADR-011), queried via call operations on
channel 0. Channels doesn't touch this. channel 0. Channels doesn't touch this.
- **ACL at the hub:** does this browser's identity have `channel:open` scope - **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 run by the hub's `CallAdapter` before it forwards. Channels doesn't touch
this. this.
- **Relay lifecycle:** when a browser disconnects, the hub tears down the - **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 - ADR-034: outgoing-only X.509 and the three peer roles (browser identity
resolution) resolution)
- ADR-011: dynamic resource ownership (the ownership store the hub queries) - 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) terminates on each leg)
- ADR-037: channel lifecycle operations (what the hub translates) - ADR-037: channel lifecycle operations (what the hub translates)
- ADR-039: ChannelsAdapter and ChannelManager (the interface the relay uses) - 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 primary constructor and the one-way-door API. Takes a pre-established
`Connection` (produced by any transport — TCP+TLS via `from_bidi`, `Connection` (produced by any transport — TCP+TLS via `from_bidi`,
WebTransport `BiStream`, SSH `direct-tcpip`, a quinn connection, a 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 spawns the demux/mux, returns the client. Mirrors the server-side
`ChannelsAdapter::handle(Connection)` (substrate-agnostic) and the `ChannelsAdapter::handle(Connection)` (substrate-agnostic) and the
existing `CallClient::spawn_dispatch(Connection)` pattern. existing `CallClient::spawn_dispatch(Connection)` pattern.
@@ -130,7 +130,7 @@ impl ChannelClient {
/// Transport-agnostic primary constructor. Takes a pre-established /// Transport-agnostic primary constructor. Takes a pre-established
/// `Connection` (any transport — TCP+TLS via `from_bidi`, /// `Connection` (any transport — TCP+TLS via `from_bidi`,
/// WebTransport BiStream, SSH direct-tcpip, a quinn connection, a /// 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 /// the demux/mux, and returns the client. Mirrors the server-side
/// `ChannelsAdapter::handle(Connection)`. This is the one-way-door /// `ChannelsAdapter::handle(Connection)`. This is the one-way-door
/// API surface — it must not be coupled to a transport (ADR-034, /// API surface — it must not be coupled to a transport (ADR-034,
@@ -139,7 +139,7 @@ impl ChannelClient {
-> Result<Self, ChannelError>; -> Result<Self, ChannelError>;
/// QUIC convenience constructor. Dials a QUIC connection to `addr` /// 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`. /// ADR-034 verifier selection), then calls `from_connection`.
/// Additive and two-way-door — `connect_tcp_tls`, /// Additive and two-way-door — `connect_tcp_tls`,
/// `connect_webtransport`, etc. join it as transports are added. /// `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 dependency on `alknet-call` arises because channel lifecycle operations
(`channel/open`, `channel/close`, `channel/control`, (`channel/open`, `channel/close`, `channel/control`,
`channel/resources/subscribe`) register on the call protocol's `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). (ADR-036).
This creates two issues: This creates two issues:
@@ -41,7 +41,7 @@ This creates two issues:
op registrations are the call-protocol coupling. Baking both into one op registrations are the call-protocol coupling. Baking both into one
crate means any consumer that wants the multiplexer also pulls in the 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 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 2. **The "no special-casing for downstream crates" principle is
violated.** The user's constraint: "we don't want to be doing anything 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 - `ChannelManager` (the shared state — channel_id → ChannelState, but
**without** the `call_ops: Arc<OperationRegistry>` field; the manager is **without** the `call_ops: Arc<OperationRegistry>` field; the manager is
ALPN-blind and call-protocol-blind) 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) read/demux loop, substrate-agnostic per ADR-034 revised)
Depends on `alknet-core` only. No `alknet-call` dependency. No opinion about Depends on `alknet-core` only. No `alknet-call` dependency. No opinion about
what channel 0 carries — that's the consumer's concern. what channel 0 carries — that's the consumer's concern.
The `ChannelsAdapter::handle` in `channels-core` does NOT preinstall channel 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 `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 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). differently).
### `alknet-channels-call` ### `alknet-channels-call`
The call-protocol coupling. Contains: 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 `preinstall_channel_0` logic that constructs channel 0's reassembly
buffers with stream_types [0, 1] and hands the `Connection` to the buffers with stream_types [0, 1] and hands the `Connection` to the
`CallAdapter`. `CallAdapter`.
@@ -222,7 +222,7 @@ in the type.
> `ConnectionCredentials`, not `CallCredentials`. `CallCredentials` > `ConnectionCredentials`, not `CallCredentials`. `CallCredentials`
> couples the dial to the call protocol (its `auth_token` field is a > couples the dial to the call protocol (its `auth_token` field is a
> call-protocol / hub-layer concept — bearer-token identity correlation > 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` + > dial uses only the transport-identity dimensions (`local_identity` +
> `remote_identity`); those move to `ConnectionCredentials` in > `remote_identity`); those move to `ConnectionCredentials` in
> `alknet-core`. All three dial signatures unify on > `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 gains a failure path, a fresh error type is cleaner than retrofitting
this one. 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 ALPN — the native registration entry point, parallel to HTTP
registration (OQ-58) but without the HTTP layer. The connection is an registration (OQ-58) but without the HTTP layer. The connection is an
**entry point** (ADR-086 §2): accepted without an established peer **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, - **Token registration** — a freshly-provisioned worker (docker,
vast.ai, runpod) generates its local identity, dials the hub on 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 enrolls its key. The hub creates a `PeerEntry` and returns a session
credential. credential.
- **No-token (open) registration** — a hub that hosts public services - **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 token. The enrollment creates a `PeerEntry` with no token
requirement. 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 `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 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 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 and its entry-point role; it does not specify the wire protocol. The
HTTP registration endpoint (OQ-58) remains the first implementation; 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. dependency for workers that have no HTTP client.
### 7. OQ-55 is resolved for the native dial ### 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 on connection failure. `AlknetClient` provides both dials; the
fallback policy is a caller concern (or a future `dial_with_fallback` fallback policy is a caller concern (or a future `dial_with_fallback`
helper — two-way-door). 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 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 wire protocol is deferred, but the ALPN and its entry-point role are
decided — a worker that has no HTTP client can register natively. 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 consistency is in the rule, not in the type. This is the same
exception as the server side (ADR-082, ADR-087 §3) — unavoidable, exception as the server side (ADR-082, ADR-087 §3) — unavoidable,
and isolated to one dial method. 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, names the ALPN and its role; the handshake protocol (token/no-token,
the frames, the `PeerEntry` creation, the session credential return) the frames, the `PeerEntry` creation, the session credential return)
is a separate ADR tied to OQ-58. A worker cannot register natively 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 `dial_iroh`) is one-way — changing the signatures after consumers exist
is a rewrite. The internal implementation (how `ConnectionCredentials` is a rewrite. The internal implementation (how `ConnectionCredentials`
feeds `TlsClientConfig::new`, how the iroh dial maps the 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 one-way (wire compatibility); its wire protocol is two-way until the
dedicated ADR lands. dedicated ADR lands.
@@ -551,7 +551,7 @@ dedicated ADR lands.
(the take-over `AlknetClient` feeds) (the take-over `AlknetClient` feeds)
- [ADR-022](022-call-protocol-client-and-adapter-contract.md) — - [ADR-022](022-call-protocol-client-and-adapter-contract.md) —
`CallClient::spawn_dispatch` (the take-over `AlknetClient` feeds) `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) is the native analogue)
- `docs/architecture/crates/channels/channel-client.md` §"Relationship - `docs/architecture/crates/channels/channel-client.md` §"Relationship
to `AlknetClient`" — the deferral this ADR resolves 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. registered handler (Layer 0) is not on this path.
- **"Marked ops invoked outside a channels session" (ADR-047 §2):** - **"Marked ops invoked outside a channels session" (ADR-047 §2):**
unchanged. A `channels/<alpn>/sub` op registered only on a channels 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 connection (the overlay isn't attached there) — the dispatch path
returns `NOT_FOUND`, which is the correct behavior for "no channels returns `NOT_FOUND`, which is the correct behavior for "no channels
session" (the `channel:no_channels_session` error code from the 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 - **Gap F** (`channel_open` marker wire format) — the marker is a boolean
field `"channel_open": true` on the `services/schema` payload. The ALPN field `"channel_open": true` on the `services/schema` payload. The ALPN
is derivable from the op name (`channels/<alpn>/sub` → 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. the ALPN string. `spec_to_json` emits it; `rebuild_spec_for` parses it.
- **Gap G** (`resource_id_path` ACL vs handler ownership) — complementary, - **Gap G** (`resource_id_path` ACL vs handler ownership) — complementary,
not redundant. The ACL check (via `resource_id_path` + not redundant. The ACL check (via `resource_id_path` +
@@ -215,7 +215,7 @@ pub struct OperationSpec {
} }
pub struct ChannelOpenSpec { 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 **Marked ops invoked outside a channels session.** `channels/tty/sub` is
registered on the call registry — which means it's also visible/invocable 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 `ChannelManager`. The open-op wrapper resolves `channel_manager()` at
invocation time via the extension trait (Gap E); if it returns `None`, invocation time via the extension trait (Gap E); if it returns `None`,
the wrapper returns `channel:no_channels_session`. 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, defaults `None`). Every spec-constructing site adds the field,
defaulting to `None`. This is a mechanical, additive change — the defaulting to `None`. This is a mechanical, additive change — the
`Option`/`None`-default keeps it non-breaking. `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`; 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 non-`alk/*` ALPNs need a rule. The rule: the path segment is the
ALPN with the `alknet/` prefix stripped; ALPNs without that prefix ALPN with the `alk/` prefix stripped; ALPNs without that prefix
use their full ALPN string as the path segment (rare case, two-way- use their full ALPN string as the path segment (rare case, two-way-
door). door).
- The `from_call` relay wrapper (Gap C) is a consumer concern, not in - 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-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-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-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-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-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). | | 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-25 | BiStream type definition | resolved | high | ADR-005 — trait, Connection parameter |
| OQ-26 | AuthContext resolution timing | resolved | high | ADR-003 — hybrid resolution | | 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-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-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 | | 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 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 { pub enum OperationType {
@@ -940,7 +940,7 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe
## Constraints ## 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. - 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. - `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. - The call protocol does not depend on any database. Operation specs are in-memory, populated at startup.
+501
View File
@@ -0,0 +1,501 @@
# Review 003 — ALPN Prefix Rename (`alknet/` → `alk/`)
## Status
Verified, open for remediation.
## Scope
This review covers the ALPN prefix rename from `alknet/<name>` to
`alk/<name>` across the entire alkcall crate. The rename is driven by
the alktty crate (`/workspace/@alkdev/alktty`), the first downstream
consumer, which uses `alk/tty` as its ALPN. The shorter prefix is
cleaner and more readable; doing it now (before any published consumers
exist) avoids a breaking change later.
The review identifies every location that must change, every location
that must **not** change (filesystem paths, historical references), and
the special-cased logic in `derive_alpn_from_op_name` that needs
updating.
Every finding below was verified directly in the source during this
pass (2026-08-14). Each finding carries the affected file(s), the
nature of the change, and the remediation unit it belongs to.
## Baseline verification (this pass)
```
cargo test → 483 passed, 0 failed
cargo clippy --all-targets -- -D warnings → clean
cargo fmt --check → clean
cargo doc --no-deps → clean
```
The suite is green. This is a mechanical rename — no logic changes, no
new features, no protocol changes beyond the ALPN byte strings
themselves. The risk is in missing a location or accidentally changing a
non-ALPN reference (filesystem paths, historical commit references).
## Verdict
The rename is straightforward but high-volume (~400+ locations across
~50 files). The work splits into three categories:
1. **Wire-format ALPNs** (byte strings `b"alknet/..."` and string
literals `"alknet/..."` in `src/`) — these are the actual protocol
identifiers. Changing them is a one-way door: old peers using
`alknet/` will not interoperate with new peers using `alk/`. Since
no published consumers exist yet, this is the right time.
2. **Doc comments, ADRs, architecture docs, AGENTS.md, README.md** —
these are documentation. They must be updated to match the new
ALPNs so the docs don't mislead future readers.
3. **`derive_alpn_from_op_name` special-casing** — this function has
explicit logic for the `alknet/` prefix that must be updated to
`alk/`. This is the only non-mechanical change.
The `alknet/` prefix in **filesystem paths** (e.g.
`/workspace/@alkdev/alknet/`, `alknet/docs/architecture/`) and
**historical commit references** must **not** be changed — those are
not ALPNs.
## Severity legend
- **[critical]** — a wire-format ALPN is missed, causing silent
interop failure with downstream crates.
- **[major]** — a doc or ADR reference is missed, causing confusion for
future readers; or the `derive_alpn_from_op_name` logic is wrong.
- **[minor]** — a test-only ALPN is missed (no interop impact, but
inconsistent).
---
# Part A — Wire-format ALPNs (src/ byte strings and string literals)
## A-01 [critical] — `CHANNELS_ALPN` constant
**Location:** `src/channels/adapter.rs:40`
**Current:** `pub const CHANNELS_ALPN: &[u8] = b"alknet/channels";`
**Required:** `pub const CHANNELS_ALPN: &[u8] = b"alk/channels";`
This is the only named ALPN constant. It is referenced from:
- `src/channels/adapter.rs:243` (channel 0 construction)
- `src/channels/client.rs:96,349,391,474,587,845,1160` (channel 0 construction in tests and `ChannelClient`)
- `src/channels/manager.rs:359,503,518,546,565,606,635,706,730,754,775` (channel 0 ALPN in tests)
All references use the constant, so changing the constant definition
propagates automatically. However, the string literal `"alknet/call"`
appears alongside `CHANNELS_ALPN` in many of these locations (as the
channel-0 call ALPN) — those must be changed separately (see A-02).
## A-02 [critical] — Hardcoded `b"alknet/call"` in `CallAdapter::alpn()`
**Location:** `src/protocol/adapter.rs:151`
**Current:** `b"alknet/call"`
**Required:** `b"alk/call"`
This is the call protocol's ALPN. It is not a named constant — it is
hardcoded in the `ProtocolHandler` impl. Consider extracting it as
`pub const CALL_ALPN: &[u8] = b"alk/call";` to match the pattern of
`CHANNELS_ALPN`. This would reduce the number of hardcoded string
literals that need individual updates.
## A-03 [critical] — Hardcoded `"alknet/call"` string literals in src/
These are the channel-0 call ALPN used as a `String` (not a byte
string) in channel 0 construction and test assertions. They appear in:
| File | Lines | Context |
|------|-------|---------|
| `src/channels/adapter.rs` | 267, 279, 291, 297, 310, 333, 346, 395, 404 | Channel 0 construction, test assertions |
| `src/channels/client.rs` | 351, 357, 393, 399, 476, 482, 589, 595, 847, 850, 1162, 1165 | Channel 0 construction, test assertions |
| `src/channels/manager.rs` | 507, 532, 550, 639, 710, 757, 779 | Test channel 0 ALPN assertions |
| `src/channels/operations.rs` | 805, 911, 941, 1038, 1044, 1068, 1074, 1098, 1104, 1130, 1142, 1173, 1185, 1215, 1222, 1252, 1264, 1275 | Test auth contexts, channel opens |
| `src/protocol/adapter.rs` | 297, 840, 858, 874 | Test assertions |
| `src/protocol/connection.rs` | 1057 | Test connection construction |
| `src/protocol/test_support.rs` | 64, 124, 128 | Test support connections |
| `src/client/call_client.rs` | 187 | Test connection construction |
**Required:** all `"alknet/call"` → `"alk/call"`.
If A-02's recommendation to extract `CALL_ALPN` is adopted, many of
these can use the constant instead of a hardcoded string.
## A-04 [critical] — Data-plane ALPNs in src/ (string literals)
These are channel ALPNs used in tests and the `derive_alpn_from_op_name`
logic:
| ALPN | Files | Approx. count |
|------|-------|---------------|
| `"alknet/tty"` | `channels/adapter.rs`, `channels/client.rs`, `channels/manager.rs`, `channels/operations.rs`, `client/from_call.rs`, `registry/discovery.rs`, `registry/spec.rs` | ~40 |
| `"alknet/tunnel"` | `channels/manager.rs`, `channels/operations.rs`, `client/from_call.rs` | ~10 |
| `"alknet/a"`, `"alknet/b"`, `"alknet/c"` | `channels/adapter.rs`, `channels/client.rs` | ~8 |
| `"alknet/test"` | `core/auth.rs`, `core/types.rs` | ~6 |
**Required:** `"alknet/tty"` → `"alk/tty"`, `"alknet/tunnel"` →
`"alk/tunnel"`, `"alknet/a"` → `"alk/a"`, etc.
## A-05 [critical] — `derive_alpn_from_op_name` special-casing
**Location:** `src/client/from_call.rs:294-307`
```rust
fn derive_alpn_from_op_name(op_name: &str) -> Option<String> {
let rest = op_name.strip_prefix("channels/")?;
let segment = rest
.strip_suffix("/sub")
.or_else(|| rest.strip_suffix("/pub"))?;
if segment.is_empty() {
return None;
}
if segment.starts_with("alknet/") || segment == "alknet" || segment.contains('/') {
Some(segment.to_string())
} else {
Some(format!("alknet/{segment}"))
}
}
```
This function derives the data-plane ALPN from an open-op name
(`channels/<alpn>/sub` → `alknet/<alpn>`). The logic has two branches:
1. If the segment already starts with `alknet/` or equals `alknet` or
contains `/`, return it as-is (it's already a full ALPN).
2. Otherwise, prepend `alknet/`.
**Required changes:**
- Line 302: `starts_with("alknet/")` → `starts_with("alk/")`
- Line 302: `segment == "alknet"` → `segment == "alk"`
- Line 305: `format!("alknet/{segment}")` → `format!("alk/{segment}")`
The doc comment on lines 284-293 also references `alknet/` and must be
updated.
The corresponding tests in `from_call.rs` (lines ~600-740) use
`"alknet/tty"` and `"alknet/tunnel"` as expected outputs — these must
be updated to `"alk/tty"` and `"alk/tunnel"`.
---
# Part B — Doc comments in src/ (/// and //!)
## B-01 [major] — Module-level and item-level doc comments
Every doc comment that references an ALPN must be updated. Key files:
| File | Approx. count | Example |
|------|---------------|---------|
| `src/lib.rs` | 4 | `alknet/call`, `alknet/channels`, `alknet/alknode` |
| `src/channels/mod.rs` | 2 | `alknet/call`, `alknet/channels` |
| `src/channels/adapter.rs` | 5 | `alknet/channels`, `alknet/call` |
| `src/channels/client.rs` | 5 | `alknet/channels`, `alknet/call`, `alknet/tty` |
| `src/channels/manager.rs` | 1 | `alknet/call` |
| `src/channels/wire.rs` | 1 | `alknet/call` |
| `src/channels/operations.rs` | ~20 | Various ALPN references |
| `src/protocol/mod.rs` | 1 | `alknet/call` |
| `src/protocol/connection.rs` | 3 | `alknet/call` |
| `src/protocol/dispatch.rs` | 1 | `alknet/call` |
| `src/protocol/adapter.rs` | 1 | `alknet/call` |
| `src/client/call_client.rs` | 1 | `alknet/call` |
| `src/client/from_call.rs` | 5 | `alknet/<alpn>` |
| `src/registry/spec.rs` | 3 | `alknet/tty`, `alknet/<alpn>` |
**Required:** mechanical `alknet/` → `alk/` in all doc comments.
---
# Part C — Architecture docs and ADRs
## C-01 [major] — Architecture overview docs
| File | Approx. count |
|------|---------------|
| `docs/architecture/README.md` | ~15 |
| `docs/architecture/call-README.md` | ~5 |
| `docs/architecture/call-protocol.md` | ~10 |
| `docs/architecture/channels-README.md` | ~8 |
| `docs/architecture/channels-overview.md` | ~20 |
| `docs/architecture/channels-adapter.md` | ~3 |
| `docs/architecture/channels-wire.md` | ~8 |
| `docs/architecture/channels-connection.md` | ~3 |
| `docs/architecture/channel-client.md` | ~4 |
| `docs/architecture/channel-operations.md` | ~12 |
| `docs/architecture/client-and-adapters.md` | ~6 |
| `docs/architecture/operation-registry.md` | ~3 |
| `docs/architecture/open-questions.md` | ~2 |
**Required:** mechanical `alknet/` → `alk/` in all architecture docs.
**Exception:** filesystem paths like `/workspace/@alkdev/alknet/` must
**not** be changed.
## C-02 [major] — ADR decision documents
| ADR | Approx. count |
|-----|---------------|
| `decisions/001-alpn-protocol-dispatch.md` | ~2 |
| `decisions/002-protocol-handler-trait.md` | ~1 |
| `decisions/004-alpn-convention-and-connection-model.md` | ~12 |
| `decisions/007-connection-from-stream-generic-single-stream.md` | ~1 |
| `decisions/009-bistream-as-the-handler-leaf.md` | ~1 |
| `decisions/012-connectioncredentials-decouple-dial-from-call.md` | ~3 |
| `decisions/013-irpc-as-call-protocol-foundation.md` | ~1 |
| `decisions/015-call-protocol-stream-model.md` | ~3 |
| `decisions/019-operation-registry-layering.md` | ~2 |
| `decisions/022-call-protocol-client-and-adapter-contract.md` | ~1 |
| `decisions/023-callclient-peer-scoped-registry-filtering.md` | ~1 |
| `decisions/031-crate-decomposition.md` | ~1 |
| `decisions/034-channels-wire-format.md` | ~7 |
| `decisions/035-channels-pure-channel-multiplexing.md` | ~10 |
| `decisions/036-channel-0-pre-negotiated-call.md` | ~15 |
| `decisions/037-channel-lifecycle-operations.md` | ~6 |
| `decisions/038-channelconnection-bidistreamsource.md` | ~2 |
| `decisions/039-channelsadapter-and-channelmanager.md` | ~4 |
| `decisions/041-per-identity-channel-cap.md` | ~1 |
| `decisions/042-hub-relay-translate-not-forward.md` | ~5 |
| `decisions/043-channelclient.md` | ~3 |
| `decisions/044-channels-subcrate-decomposition.md` | ~6 |
| `decisions/045-alknetclient-native-dial-seam.md` | ~10 |
| `decisions/046-publish-operation-type-and-handler-kind-sink.md` | ~2 |
| `decisions/047-openable-alpns-are-operations.md` | ~8 |
**Required:** mechanical `alknet/` → `alk/` in all ADRs.
**Exception:** filesystem paths and historical references to the
`alknet` mono-repo must **not** be changed.
### ADR-004 special consideration
ADR-004 (`004-alpn-convention-and-connection-model.md`) defines the
ALPN string convention. It currently specifies the `alknet/` prefix.
This ADR must be amended to reflect the new `alk/` prefix. The
amendment should note:
- The original decision used `alknet/` as the prefix.
- The prefix was shortened to `alk/` before the first published
release (v0.1.1) for brevity and readability.
- The convention otherwise remains: one ALPN per connection, the
prefix identifies the alk protocol family.
## C-03 [major] — AGENTS.md
**Location:** `AGENTS.md`
**Lines:** 53, 207-209, 243, 263
Key changes:
- Line 53: `alknet/call` → `alk/call`
- Line 207: `/workspace/@alkdev/alknet/docs/architecture/` — **do not
change** (filesystem path)
- Line 208-209: "The ALPN strings (`alknet/call`, `alknet/channels`)
are wire-stable and unchanged." — **must be updated** to reflect the
new ALPNs. The strings are changing; the statement that they are
"unchanged" is now false. Replace with the new ALPNs and note that
they are wire-stable going forward.
- Line 243: `alknet/call` → `alk/call`
- Line 263: `alknet/` prefix → `alk/` prefix
## C-04 [major] — README.md
**Location:** `README.md`
**Lines:** 60, 77, 116
- Line 60: `b"alknet/call"` → `b"alk/call"`
- Line 77: `b"alknet/channels"` → `b"alk/channels"`
- Line 116: `alknet/channels` → `alk/channels`
## C-05 [minor] — Existing review documents
**Locations:**
- `docs/reviews/001-pub-and-channels-integration-review.md` (~15 occurrences)
- `docs/reviews/002-pre-publish-coverage-and-cleanup-review.md` (0 occurrences)
Review 001 references `alknet/` in code snippets and analysis. These
are historical — they document findings against the code as it existed
at the time. **Do not change** historical review documents. They are
snapshots of a past state.
---
# Part D — What must NOT change
## D-01 [critical] — Filesystem paths
These are **not** ALPNs and must **not** be changed:
| Pattern | Example | Files |
|---------|---------|-------|
| `/workspace/@alkdev/alknet/` | `/workspace/@alkdev/alknet/docs/architecture/` | `AGENTS.md:207`, `docs/architecture/README.md:15,291`, several ADRs |
| `alknet/docs/` | `alknet/docs/architecture/decisions/` | ADR-046, ADR-047 |
| `alknet-call` | The old crate name | `AGENTS.md:205`, `docs/architecture/README.md:14` |
| `alknet-channels` | The old crate name | `AGENTS.md:205`, `docs/architecture/README.md:14` |
| `alknet-client` | The old crate name | ADR-045 |
| `alknet/alknode` | The composition layer name | `src/lib.rs:68`, `docs/architecture/README.md:228` |
The crate names `alknet-call` and `alknet-channels` are historical
references to the pre-unification crates in the alknet mono-repo.
These are not ALPNs and should not be changed.
The composition layer `alknet/alknode` in `src/lib.rs:68` is a
reference to a future crate, not an ALPN. It should not be changed.
## D-02 [minor] — Historical review documents
`docs/reviews/001-pub-and-channels-integration-review.md` and
`docs/reviews/002-pre-publish-coverage-and-cleanup-review.md` are
historical snapshots. Do not modify them.
---
# Cross-cutting: what is solid (verified)
These were checked and are correct — the rename does not affect them:
- **Wire format shapes:** `EventEnvelope` and the channels 8-byte chunk
header are unchanged. Only the ALPN byte strings carried in the TLS
handshake change. ✓
- **No logic changes:** the rename is purely mechanical. No function
signatures, trait bounds, or control flow change. ✓
- **No new dependencies, no feature flag changes.** ✓
- **`alktype` dependency unaffected.** ✓
- **483 tests pass before the rename; they should all pass after.** ✓
- **The `alknet/` prefix in `derive_alpn_from_op_name` is the only
non-mechanical change** — the function's logic stays the same, only
the prefix string changes. ✓
---
# Remediation plan
The plan is split into **four units of work**, ordered by risk. Each
unit is an independently shippable commit.
## Unit 1 — Wire-format ALPNs + `derive_alpn_from_op_name` (A-01 through A-05)
**Goal:** all ALPN byte strings and string literals in `src/` use
`alk/` prefix; `derive_alpn_from_op_name` uses `alk/`; all tests pass.
**Files:** `src/channels/adapter.rs`, `src/channels/client.rs`,
`src/channels/manager.rs`, `src/channels/operations.rs`,
`src/protocol/adapter.rs`, `src/protocol/connection.rs`,
`src/protocol/test_support.rs`, `src/client/call_client.rs`,
`src/client/from_call.rs`, `src/core/auth.rs`, `src/core/types.rs`,
`src/registry/discovery.rs`, `src/registry/spec.rs`.
**Scope:**
- **A-01:** change `CHANNELS_ALPN` from `b"alknet/channels"` to
`b"alk/channels"`.
- **A-02:** change `b"alknet/call"` in `CallAdapter::alpn()` to
`b"alk/call"`. Optionally extract as `pub const CALL_ALPN: &[u8] =
b"alk/call";` and use it in all channel-0 construction sites.
- **A-03:** change all `"alknet/call"` string literals to `"alk/call"`.
- **A-04:** change all data-plane ALPN string literals (`"alknet/tty"`,
`"alknet/tunnel"`, `"alknet/a"`, `"alknet/b"`, `"alknet/c"`,
`"alknet/test"`) to their `alk/` equivalents.
- **A-05:** update `derive_alpn_from_op_name`:
- `starts_with("alknet/")` → `starts_with("alk/")`
- `segment == "alknet"` → `segment == "alk"`
- `format!("alknet/{segment}")` → `format!("alk/{segment}")`
- Update the doc comment.
- Update the corresponding tests.
**Acceptance gate:** `cargo test` green; `cargo clippy
--all-targets -- -D warnings` clean; `cargo fmt --check` clean; `cargo
doc --no-deps` clean. No remaining `b"alknet/` or `"alknet/` in `src/`
(verified by grep).
## Unit 2 — Doc comments in src/ (B-01)
**Goal:** all doc comments in `src/` use `alk/` prefix.
**Files:** all files listed in B-01.
**Scope:** mechanical `s/alknet\//alk\//g` in doc comments (`///` and
`//!` lines) across all `src/` files. Verify no code lines are
affected (Unit 1 already handled those).
**Acceptance gate:** `cargo doc --no-deps` clean (0 warnings); grep for
`alknet/` in `src/` returns no matches in doc comments.
## Unit 3 — Architecture docs, ADRs, AGENTS.md, README.md (C-01 through C-05)
**Goal:** all documentation uses `alk/` prefix; ADR-004 is amended.
**Files:** all files listed in C-01 through C-04.
**Scope:**
- **C-01:** mechanical `s/alknet\//alk\//g` in all architecture docs,
with careful exclusion of filesystem paths.
- **C-02:** mechanical `s/alknet\//alk\//g` in all ADRs, with careful
exclusion of filesystem paths and historical crate names.
- **ADR-004 amendment:** add an amendment section noting the prefix
change from `alknet/` to `alk/`, the rationale (brevity, readability,
done before first published release), and the effective date.
- **C-03:** update AGENTS.md — change ALPN references, update the
"wire-stable and unchanged" statement to reflect the new ALPNs.
- **C-04:** update README.md — change ALPN references in code examples
and prose.
**Acceptance gate:** grep for `alknet/call` and `alknet/channels` in
`docs/` returns no matches (except in historical review documents and
filesystem paths). ADR-004 has an amendment section.
## Unit 4 — Final verification + version bump
**Goal:** confirm nothing was missed; bump version to 0.1.1.
**Files:** `Cargo.toml`.
**Scope:**
- Run `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check`, `cargo doc --no-deps`.
- Grep the entire repo for `alknet/` and manually verify every
remaining match is either a filesystem path, a historical crate name
(`alknet-call`, `alknet-channels`, `alknet-client`), or in a
historical review document.
- Bump `version` in `Cargo.toml` from `0.1.0` to `0.1.1`.
- Run `cargo publish --dry-run --allow-dirty` (may still fail on
missing README.md — that's Review 002's R-01, not this review's
concern).
**Acceptance gate:** all verification commands pass; version is 0.1.1;
no unexpected `alknet/` matches remain.
## Suggested sequencing
```
Unit 1 (wire-format ALPNs + derive_alpn_from_op_name) → no deps; do first
Unit 2 (doc comments in src/) → depends on Unit 1 (to avoid conflicts)
Unit 3 (architecture docs, ADRs, AGENTS.md, README.md) → no deps; independent
Unit 4 (final verification + version bump) → depends on Units 1-3
```
Units 1 and 3 are independent and can proceed in parallel. Unit 2
should follow Unit 1 to avoid merge conflicts on the same files. Unit 4
is the final pass.
---
## Verification log (this pass)
Findings verified directly in source on 2026-08-14 against tree
`7cd8a57` (post-Review-002 baseline).
- **A-01:** verified `CHANNELS_ALPN` at `adapter.rs:40` and all
references to it.
- **A-02:** verified hardcoded `b"alknet/call"` at `adapter.rs:151`.
- **A-03:** verified all `"alknet/call"` string literals in src/ (56
locations across 8 files).
- **A-04:** verified all data-plane ALPN string literals (~60
locations).
- **A-05:** verified `derive_alpn_from_op_name` at `from_call.rs:294-307`
and its tests at lines ~600-740.
- **B-01:** verified all doc comment references (~40 locations across
12 files).
- **C-01 through C-04:** verified all documentation references (~200+
locations across ~35 files).
- **D-01:** verified filesystem path references that must not change
(~15 locations).
- **D-02:** verified historical review documents (2 files).
Total estimated `alknet/` occurrences requiring change: ~350.
Total estimated `alknet/` occurrences that must NOT change: ~50
(filesystem paths, historical crate names, review documents).
+19 -19
View File
@@ -1,5 +1,5 @@
//! `ChannelsAdapter` — implements `ProtocolHandler` for //! `ChannelsAdapter` — implements `ProtocolHandler` for
//! `alknet/channels` (ADR-039). The accept path: receive one //! `alk/channels` (ADR-039). The accept path: receive one
//! `Connection`, install channel 0, then run the demux loop — read //! `Connection`, install channel 0, then run the demux loop — read
//! 8-byte chunk headers off the bidi stream and route each chunk's //! 8-byte chunk headers off the bidi stream and route each chunk's
//! payload to the matching `channel_id`'s reassembly buffer. //! payload to the matching `channel_id`'s reassembly buffer.
@@ -12,7 +12,7 @@
//! The wire format and demux loop are correct for both substrates; //! The wire format and demux loop are correct for both substrates;
//! only the outer `accept_bi()` loop is missing. //! only the outer `accept_bi()` loop is missing.
//! //!
//! Channel 0 is pre-negotiated as `alknet/call` (ADR-036). The //! Channel 0 is pre-negotiated as `alk/call` (ADR-036). The
//! `install_channel_zero` hook (ADR-036 amendment — single-stream call //! `install_channel_zero` hook (ADR-036 amendment — single-stream call
//! mode) receives channel 0's `Connection` (already constructed by the //! mode) receives channel 0's `Connection` (already constructed by the
//! adapter from the reassembled read half + the mux write half) and //! adapter from the reassembled read half + the mux write half) and
@@ -37,7 +37,7 @@ use super::policy::ChannelLifecyclePolicy;
use super::wire::CHUNK_HEADER_LEN; use super::wire::CHUNK_HEADER_LEN;
/// The ALPN the `ChannelsAdapter` registers on. /// The ALPN the `ChannelsAdapter` registers on.
pub const CHANNELS_ALPN: &[u8] = b"alknet/channels"; pub const CHANNELS_ALPN: &[u8] = b"alk/channels";
/// The hook for `channels-call` to install the `CallAdapter` on /// The hook for `channels-call` to install the `CallAdapter` on
/// channel 0 (ADR-036 amendment — single-stream call mode). The /// channel 0 (ADR-036 amendment — single-stream call mode). The
@@ -46,7 +46,7 @@ pub const CHANNELS_ALPN: &[u8] = b"alknet/channels";
/// runs the call dispatch loop on it. /// runs the call dispatch loop on it.
/// ///
/// The callback receives channel 0's `Connection` (carrying the /// The callback receives channel 0's `Connection` (carrying the
/// `alknet/call` ALPN) and the `AuthContext` (so the call dispatch /// `alk/call` ALPN) and the `AuthContext` (so the call dispatch
/// loop can resolve the peer's identity). The `ChannelManager` is /// loop can resolve the peer's identity). The `ChannelManager` is
/// accessible via the connection's `BidiStreamSource` (the channel-0 /// accessible via the connection's `BidiStreamSource` (the channel-0
/// source closes over it), so the hook does not need it as a separate /// source closes over it), so the hook does not need it as a separate
@@ -56,7 +56,7 @@ pub type InstallChannelZero = Arc<
dyn Fn(ChannelManager, Connection, AuthContext) -> tokio::task::JoinHandle<()> + Send + Sync, dyn Fn(ChannelManager, Connection, AuthContext) -> tokio::task::JoinHandle<()> + Send + Sync,
>; >;
/// `ChannelsAdapter` — the `ProtocolHandler` for `alknet/channels`. /// `ChannelsAdapter` — the `ProtocolHandler` for `alk/channels`.
/// Its `handle()` receives one `Connection`, installs channel 0 via /// Its `handle()` receives one `Connection`, installs channel 0 via
/// the `install_channel_zero` hook, then runs the demux loop. /// the `install_channel_zero` hook, then runs the demux loop.
pub struct ChannelsAdapter { pub struct ChannelsAdapter {
@@ -231,7 +231,7 @@ impl ProtocolHandler for ChannelsAdapter {
} }
}); });
// Channel 0 is pre-negotiated as `alknet/call` (ADR-036); the // Channel 0 is pre-negotiated as `alk/call` (ADR-036); the
// `install_channel_zero` hook runs the single-stream call // `install_channel_zero` hook runs the single-stream call
// dispatch loop on channel 0's `Connection`. // dispatch loop on channel 0's `Connection`.
let (channel0_send, channel0_recv) = manager let (channel0_send, channel0_recv) = manager
@@ -240,7 +240,7 @@ impl ProtocolHandler for ChannelsAdapter {
.map_err(|e| HandlerError::Internal(format!("channel 0 install failed: {e}").into()))?; .map_err(|e| HandlerError::Internal(format!("channel 0 install failed: {e}").into()))?;
let channel0_source = let channel0_source =
super::source::channel_source(channel0_recv, channel0_send, connection.remote_addr()); super::source::channel_source(channel0_recv, channel0_send, connection.remote_addr());
let channel0_conn = Connection::from_source(channel0_source, b"alknet/call".to_vec()); let channel0_conn = Connection::from_source(channel0_source, b"alk/call".to_vec());
let _handler_task = let _handler_task =
(self.install_channel_zero)(manager.clone(), channel0_conn, auth.clone()); (self.install_channel_zero)(manager.clone(), channel0_conn, auth.clone());
@@ -264,7 +264,7 @@ mod tests {
#[test] #[test]
fn channels_alpn_is_alknet_channels() { fn channels_alpn_is_alknet_channels() {
assert_eq!(CHANNELS_ALPN, b"alknet/channels"); assert_eq!(CHANNELS_ALPN, b"alk/channels");
} }
#[test] #[test]
@@ -276,7 +276,7 @@ mod tests {
}); });
let policy = crate::channels::policy::default_policy(); let policy = crate::channels::policy::default_policy();
let adapter = ChannelsAdapter::new(install, policy); let adapter = ChannelsAdapter::new(install, policy);
assert_eq!(adapter.alpn(), b"alknet/channels"); assert_eq!(adapter.alpn(), b"alk/channels");
} }
#[test] #[test]
@@ -288,13 +288,13 @@ mod tests {
}); });
let policy = crate::channels::policy::default_policy(); let policy = crate::channels::policy::default_policy();
let adapter = ChannelsAdapter::with_limits(install, 128, 32, policy); let adapter = ChannelsAdapter::with_limits(install, 128, 32, policy);
assert_eq!(adapter.alpn(), b"alknet/channels"); assert_eq!(adapter.alpn(), b"alk/channels");
} }
#[tokio::test] #[tokio::test]
async fn handle_installs_channel_zero_and_runs_demux_loop() { async fn handle_installs_channel_zero_and_runs_demux_loop() {
let (client, server) = tokio::io::duplex(64 * 1024); let (client, server) = tokio::io::duplex(64 * 1024);
let conn = Connection::from_bidi(server, b"alknet/channels".to_vec(), None); let conn = Connection::from_bidi(server, b"alk/channels".to_vec(), None);
let installed = Arc::new(AtomicBool::new(false)); let installed = Arc::new(AtomicBool::new(false));
let installed_clone = Arc::clone(&installed); let installed_clone = Arc::clone(&installed);
@@ -307,7 +307,7 @@ mod tests {
let policy = crate::channels::policy::default_policy(); let policy = crate::channels::policy::default_policy();
let adapter = ChannelsAdapter::new(install_channel_zero, policy); let adapter = ChannelsAdapter::new(install_channel_zero, policy);
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let handle_task = tokio::spawn(async move { adapter.handle(conn, &auth).await }); let handle_task = tokio::spawn(async move { adapter.handle(conn, &auth).await });
@@ -330,7 +330,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn handle_demux_loop_processes_frames() { async fn handle_demux_loop_processes_frames() {
let (client, server) = tokio::io::duplex(64 * 1024); let (client, server) = tokio::io::duplex(64 * 1024);
let conn = Connection::from_bidi(server, b"alknet/channels".to_vec(), None); let conn = Connection::from_bidi(server, b"alk/channels".to_vec(), None);
let installed = Arc::new(AtomicBool::new(false)); let installed = Arc::new(AtomicBool::new(false));
let installed_clone = Arc::clone(&installed); let installed_clone = Arc::clone(&installed);
@@ -343,7 +343,7 @@ mod tests {
let policy = crate::channels::policy::default_policy(); let policy = crate::channels::policy::default_policy();
let adapter = ChannelsAdapter::new(install_channel_zero, policy); let adapter = ChannelsAdapter::new(install_channel_zero, policy);
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let handle_task = tokio::spawn(async move { adapter.handle(conn, &auth).await }); let handle_task = tokio::spawn(async move { adapter.handle(conn, &auth).await });
@@ -392,7 +392,7 @@ mod tests {
fn close(&self, _code: u32, _reason: &str) {} fn close(&self, _code: u32, _reason: &str) {}
} }
let conn = Connection::from_source(ClosedSource, b"alknet/channels".to_vec()); let conn = Connection::from_source(ClosedSource, b"alk/channels".to_vec());
let installed = Arc::new(AtomicBool::new(false)); let installed = Arc::new(AtomicBool::new(false));
let install: InstallChannelZero = Arc::new(move |_m, _c, _a| { let install: InstallChannelZero = Arc::new(move |_m, _c, _a| {
@@ -401,7 +401,7 @@ mod tests {
}); });
let policy = crate::channels::policy::default_policy(); let policy = crate::channels::policy::default_policy();
let adapter = ChannelsAdapter::new(install, policy); let adapter = ChannelsAdapter::new(install, policy);
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let result = adapter.handle(conn, &auth).await; let result = adapter.handle(conn, &auth).await;
match result { match result {
@@ -425,7 +425,7 @@ mod tests {
let manager = ChannelManager::with_defaults(mux_handle, None); let manager = ChannelManager::with_defaults(mux_handle, None);
let (id, _send, mut recv) = manager let (id, _send, mut recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
@@ -480,11 +480,11 @@ mod tests {
let manager = ChannelManager::with_defaults(mux_handle, None); let manager = ChannelManager::with_defaults(mux_handle, None);
let (id_a, _send_a, mut recv_a) = manager let (id_a, _send_a, mut recv_a) = manager
.open_channel("alknet/a", "alice", None) .open_channel("alk/a", "alice", None)
.await .await
.expect("open a"); .expect("open a");
let (id_b, _send_b, mut recv_b) = manager let (id_b, _send_b, mut recv_b) = manager
.open_channel("alknet/b", "bob", None) .open_channel("alk/b", "bob", None)
.await .await
.expect("open b"); .expect("open b");
+31 -31
View File
@@ -37,7 +37,7 @@ use super::reassembly::{MpscRecvStream, MpscSendStream};
/// and the `CallConnection` (for calling open ops on channel 0). /// and the `CallConnection` (for calling open ops on channel 0).
/// ///
/// The consumer dials the transport and establishes the /// The consumer dials the transport and establishes the
/// `alknet/channels` ALPN, then hands the `Connection` to /// `alk/channels` ALPN, then hands the `Connection` to
/// `from_connection`. The client runs the demux/mux in spawned tasks /// `from_connection`. The client runs the demux/mux in spawned tasks
/// and exposes `open_channel` to call the per-ALPN open ops /// and exposes `open_channel` to call the per-ALPN open ops
/// (`channels/<alpn>/sub`, `channels/<alpn>/pub`) on channel 0. /// (`channels/<alpn>/sub`, `channels/<alpn>/pub`) on channel 0.
@@ -48,8 +48,8 @@ pub struct ChannelClient {
impl ChannelClient { impl ChannelClient {
/// Construct from an established `Connection` (the consumer dials /// Construct from an established `Connection` (the consumer dials
/// the transport and establishes the `alknet/channels` ALPN). /// the transport and establishes the `alk/channels` ALPN).
/// Installs channel 0 (pre-negotiated as `alknet/call`, /// Installs channel 0 (pre-negotiated as `alk/call`,
/// ADR-036), wraps it as a `CallConnection` in single-stream call /// ADR-036), wraps it as a `CallConnection` in single-stream call
/// mode (ADR-036 amendment), spawns the demux and mux tasks, and /// mode (ADR-036 amendment), spawns the demux and mux tasks, and
/// returns the client. /// returns the client.
@@ -93,7 +93,7 @@ impl ChannelClient {
// (for `call.requested`) and a read pump (for responses). // (for `call.requested`) and a read pump (for responses).
let channel0_source = let channel0_source =
super::source::channel_source(channel0_recv, channel0_send, remote_addr); super::source::channel_source(channel0_recv, channel0_send, remote_addr);
let channel0_conn = Connection::from_source(channel0_source, b"alknet/call".to_vec()); let channel0_conn = Connection::from_source(channel0_source, b"alk/call".to_vec());
let channel0_bidi = channel0_conn.accept_bi().await?; let channel0_bidi = channel0_conn.accept_bi().await?;
let (single_stream_writer, single_stream_reader) = let (single_stream_writer, single_stream_reader) =
crate::protocol::connection::split_single_stream(channel0_bidi); crate::protocol::connection::split_single_stream(channel0_bidi);
@@ -165,7 +165,7 @@ impl ChannelClient {
/// `Connection` from these via `channel_source` and /// `Connection` from these via `channel_source` and
/// `Connection::from_source`. /// `Connection::from_source`.
/// ///
/// `alpn` is the data-plane ALPN (e.g. `alknet/tty`), used for /// `alpn` is the data-plane ALPN (e.g. `alk/tty`), used for
/// observability in the local manager. /// observability in the local manager.
pub async fn open_channel( pub async fn open_channel(
&self, &self,
@@ -346,15 +346,15 @@ mod tests {
let (client_end, server_end) = tokio::io::duplex(64 * 1024); let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn = let client_conn =
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn = let server_conn =
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new( let adapter = ChannelsAdapter::new(
make_install_channel_zero(Arc::clone(&registry)), make_install_channel_zero(Arc::clone(&registry)),
Arc::new(NoCap), Arc::new(NoCap),
); );
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move { let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
}); });
@@ -388,15 +388,15 @@ mod tests {
let (client_end, server_end) = tokio::io::duplex(64 * 1024); let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn = let client_conn =
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn = let server_conn =
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new( let adapter = ChannelsAdapter::new(
make_install_channel_zero(Arc::clone(&registry)), make_install_channel_zero(Arc::clone(&registry)),
Arc::new(NoCap), Arc::new(NoCap),
); );
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move { let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
}); });
@@ -471,15 +471,15 @@ mod tests {
let (client_end, server_end) = tokio::io::duplex(64 * 1024); let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn = let client_conn =
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn = let server_conn =
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new( let adapter = ChannelsAdapter::new(
make_install_channel_zero(Arc::clone(&registry)), make_install_channel_zero(Arc::clone(&registry)),
Arc::new(NoCap), Arc::new(NoCap),
); );
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move { let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
}); });
@@ -584,15 +584,15 @@ mod tests {
let (client_end, server_end) = tokio::io::duplex(64 * 1024); let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn = let client_conn =
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn = let server_conn =
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new( let adapter = ChannelsAdapter::new(
make_install_channel_zero(Arc::clone(&registry)), make_install_channel_zero(Arc::clone(&registry)),
Arc::new(NoCap), Arc::new(NoCap),
); );
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move { let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
}); });
@@ -819,7 +819,7 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(ChannelOpenSpec::new("alknet/tty")); .with_channel_open(ChannelOpenSpec::new("alk/tty"));
core.register_openable( core.register_openable(
spec, spec,
Arc::clone(&open_handler), Arc::clone(&open_handler),
@@ -842,12 +842,12 @@ mod tests {
let (client_end, server_end) = tokio::io::duplex(64 * 1024); let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn = let client_conn =
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn = let server_conn =
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap)); let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap));
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move { let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
}); });
@@ -926,12 +926,12 @@ mod tests {
assert!(dyn_policy.check_open(&alice).is_ok()); assert!(dyn_policy.check_open(&alice).is_ok());
manager manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open alice"); .expect("open alice");
assert!(dyn_policy.check_open(&bob).is_ok()); assert!(dyn_policy.check_open(&bob).is_ok());
manager manager
.open_channel("alknet/tty", "bob", None) .open_channel("alk/tty", "bob", None)
.await .await
.expect("open bob"); .expect("open bob");
@@ -988,9 +988,9 @@ mod tests {
let m3 = Arc::clone(&manager); let m3 = Arc::clone(&manager);
let (r1, r2, r3) = tokio::join!( let (r1, r2, r3) = tokio::join!(
m1.open_channel("alknet/a", "alice", None), m1.open_channel("alk/a", "alice", None),
m2.open_channel("alknet/b", "bob", None), m2.open_channel("alk/b", "bob", None),
m3.open_channel("alknet/c", "carol", None), m3.open_channel("alk/c", "carol", None),
); );
let successes = [r1.is_ok(), r2.is_ok(), r3.is_ok()] let successes = [r1.is_ok(), r2.is_ok(), r3.is_ok()]
@@ -1135,7 +1135,7 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(ChannelOpenSpec::new("alknet/tty")); .with_channel_open(ChannelOpenSpec::new("alk/tty"));
core.register_openable( core.register_openable(
spec, spec,
Arc::clone(&open_handler), Arc::clone(&open_handler),
@@ -1157,12 +1157,12 @@ mod tests {
let (client_end, server_end) = tokio::io::duplex(64 * 1024); let (client_end, server_end) = tokio::io::duplex(64 * 1024);
let client_conn = let client_conn =
Connection::from_bidi(client_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(client_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let server_conn = let server_conn =
Connection::from_bidi(server_end, b"alknet/channels".to_vec(), Some(TEST_ADDR)); Connection::from_bidi(server_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap)); let adapter = ChannelsAdapter::new(install_hook, Arc::new(NoCap));
let auth = AuthContext::anonymous(b"alknet/channels"); let auth = AuthContext::anonymous(b"alk/channels");
let _server_handle = tokio::spawn(async move { let _server_handle = tokio::spawn(async move {
let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await;
}); });
@@ -1175,7 +1175,7 @@ mod tests {
.open_channel( .open_channel(
"channels/tty/sub", "channels/tty/sub",
serde_json::json!({ "container": "abc" }), serde_json::json!({ "container": "abc" }),
"alknet/tty", "alk/tty",
) )
.await .await
.expect("open_channel"); .expect("open_channel");
+20 -20
View File
@@ -335,7 +335,7 @@ impl ChannelManager {
} }
} }
/// Install channel 0 (pre-negotiated as `alknet/call`, ADR-036). /// Install channel 0 (pre-negotiated as `alk/call`, ADR-036).
/// Channel 0 is special only in that it's pre-allocated (by /// Channel 0 is special only in that it's pre-allocated (by
/// `channels-call`); the `ChannelsAdapter` hands the resulting /// `channels-call`); the `ChannelsAdapter` hands the resulting
/// `Connection` to the `CallAdapter`. The `channel_id` is 0. /// `Connection` to the `CallAdapter`. The `channel_id` is 0.
@@ -356,7 +356,7 @@ impl ChannelManager {
let state = ChannelState { let state = ChannelState {
demux_sender, demux_sender,
handler_task, handler_task,
alpn: "alknet/call".to_string(), alpn: "alk/call".to_string(),
}; };
{ {
let mut channels = self.inner.channels.lock(); let mut channels = self.inner.channels.lock();
@@ -500,11 +500,11 @@ mod tests {
async fn open_channel_returns_unique_ids() { async fn open_channel_returns_unique_ids() {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
let (id1, _send1, _recv1) = manager let (id1, _send1, _recv1) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open 1"); .expect("open 1");
let (id2, _send2, _recv2) = manager let (id2, _send2, _recv2) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open 2"); .expect("open 2");
assert_ne!(id1, id2, "channel IDs are unique"); assert_ne!(id1, id2, "channel IDs are unique");
@@ -515,7 +515,7 @@ mod tests {
async fn open_channel_records_opener_in_ledger() { async fn open_channel_records_opener_in_ledger() {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
let (id, _send, _recv) = manager let (id, _send, _recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
assert_eq!( assert_eq!(
@@ -529,7 +529,7 @@ mod tests {
async fn teardown_channel_removes_state() { async fn teardown_channel_removes_state() {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
let (id, _send, _recv) = manager let (id, _send, _recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
assert!(manager.has_channel(id)); assert!(manager.has_channel(id));
@@ -543,11 +543,11 @@ mod tests {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
assert!(manager.channel_ids().is_empty(), "no channels open yet"); assert!(manager.channel_ids().is_empty(), "no channels open yet");
let (id1, _send1, _recv1) = manager let (id1, _send1, _recv1) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open 1"); .expect("open 1");
let (id2, _send2, _recv2) = manager let (id2, _send2, _recv2) = manager
.open_channel("alknet/tunnel", "bob", None) .open_channel("alk/tunnel", "bob", None)
.await .await
.expect("open 2"); .expect("open 2");
let mut ids = manager.channel_ids(); let mut ids = manager.channel_ids();
@@ -562,7 +562,7 @@ mod tests {
async fn route_payload_to_open_channel_succeeds() { async fn route_payload_to_open_channel_succeeds() {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
let (id, _send, mut recv) = manager let (id, _send, mut recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
manager manager
@@ -603,7 +603,7 @@ mod tests {
async fn route_payload_to_known_channel_does_not_increment_dropped_counter() { async fn route_payload_to_known_channel_does_not_increment_dropped_counter() {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
let (id, _send, mut recv) = manager let (id, _send, mut recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
manager manager
@@ -632,11 +632,11 @@ mod tests {
async fn clear_all_returns_opener_ids() { async fn clear_all_returns_opener_ids() {
let manager = make_manager_with_runner().await; let manager = make_manager_with_runner().await;
manager manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
manager manager
.open_channel("alknet/tty", "bob", None) .open_channel("alk/tty", "bob", None)
.await .await
.expect("open"); .expect("open");
let drained = manager.clear_all(); let drained = manager.clear_all();
@@ -703,11 +703,11 @@ mod tests {
let accept = ChannelManager::new(connect_handle, 256, 64, None, ChannelSide::Accept); let accept = ChannelManager::new(connect_handle, 256, 64, None, ChannelSide::Accept);
let (id_c, _, _) = connect let (id_c, _, _) = connect
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("connect open"); .expect("connect open");
let (id_a, _, _) = accept let (id_a, _, _) = accept
.open_channel("alknet/tty", "bob", None) .open_channel("alk/tty", "bob", None)
.await .await
.expect("accept open"); .expect("accept open");
@@ -727,7 +727,7 @@ mod tests {
let manager = ChannelManager::with_defaults(handle, None); let manager = ChannelManager::with_defaults(handle, None);
let (send, mut recv) = manager let (send, mut recv) = manager
.adopt_channel(7, "alknet/tty", None) .adopt_channel(7, "alk/tty", None)
.await .await
.expect("adopt"); .expect("adopt");
@@ -751,10 +751,10 @@ mod tests {
let manager = ChannelManager::with_defaults(handle, None); let manager = ChannelManager::with_defaults(handle, None);
manager manager
.adopt_channel(7, "alknet/tty", None) .adopt_channel(7, "alk/tty", None)
.await .await
.expect("first adopt"); .expect("first adopt");
match manager.adopt_channel(7, "alknet/tty", None).await { match manager.adopt_channel(7, "alk/tty", None).await {
Err(ManagerError::ChannelExists(7)) => {} Err(ManagerError::ChannelExists(7)) => {}
Err(other) => panic!("expected ChannelExists, got {other}"), Err(other) => panic!("expected ChannelExists, got {other}"),
Ok(_) => panic!("expected ChannelExists, got Ok"), Ok(_) => panic!("expected ChannelExists, got Ok"),
@@ -772,14 +772,14 @@ mod tests {
let manager = ChannelManager::new(handle, 2, 64, None, ChannelSide::Accept); let manager = ChannelManager::new(handle, 2, 64, None, ChannelSide::Accept);
manager manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open 1"); .expect("open 1");
manager manager
.open_channel("alknet/tty", "bob", None) .open_channel("alk/tty", "bob", None)
.await .await
.expect("open 2"); .expect("open 2");
match manager.open_channel("alknet/tty", "carol", None).await { match manager.open_channel("alk/tty", "carol", None).await {
Err(ManagerError::TooManyChannels { count, max }) => { Err(ManagerError::TooManyChannels { count, max }) => {
assert_eq!(count, 2); assert_eq!(count, 2);
assert_eq!(max, 2); assert_eq!(max, 2);
+2 -2
View File
@@ -1,7 +1,7 @@
//! Channels protocol: N logical channels over one transport stream //! Channels protocol: N logical channels over one transport stream
//! (ADR-034, amended by ADR-035 — no `stream_type` concept). //! (ADR-034, amended by ADR-035 — no `stream_type` concept).
//! //!
//! Channel 0 is pre-negotiated as `alknet/call` (ADR-036); channels //! Channel 0 is pre-negotiated as `alk/call` (ADR-036); channels
//! 1..N are opened dynamically via per-ALPN open ops on channel 0 //! 1..N are opened dynamically via per-ALPN open ops on channel 0
//! (ADR-047). The channels layer is a re-framing proxy: it converts //! (ADR-047). The channels layer is a re-framing proxy: it converts
//! between "one transport stream carrying N channels" (the wire) and //! between "one transport stream carrying N channels" (the wire) and
@@ -18,7 +18,7 @@
//! - [`source`]: `ChannelBidiStreamSource` — implements //! - [`source`]: `ChannelBidiStreamSource` — implements
//! `BidiStreamSource` for a single channel (yield-once `accept_bi`). //! `BidiStreamSource` for a single channel (yield-once `accept_bi`).
//! - [`adapter`]: `ChannelsAdapter` — `ProtocolHandler` for //! - [`adapter`]: `ChannelsAdapter` — `ProtocolHandler` for
//! `alknet/channels` (the demux loop). //! `alk/channels` (the demux loop).
//! - [`operations`]: `ChannelOperations` — registers `channel/close`, //! - [`operations`]: `ChannelOperations` — registers `channel/close`,
//! `channel/control`, `channel/resources/subscribe` on the call //! `channel/control`, `channel/resources/subscribe` on the call
//! `OperationRegistry`; the per-ALPN open ops are registered by the //! `OperationRegistry`; the per-ALPN open ops are registered by the
+19 -24
View File
@@ -802,7 +802,7 @@ mod tests {
async fn resources_subscribe_handler_returns_not_implemented() { async fn resources_subscribe_handler_returns_not_implemented() {
let manager = make_manager().await; let manager = make_manager().await;
manager manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open tty"); .expect("open tty");
let handler = make_resources_subscribe_handler(manager.clone()); let handler = make_resources_subscribe_handler(manager.clone());
@@ -908,7 +908,7 @@ mod tests {
async fn close_handler_success_path_closes_open_channel() { async fn close_handler_success_path_closes_open_channel() {
let manager = make_manager().await; let manager = make_manager().await;
let (id, _send, _recv) = manager let (id, _send, _recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
assert!(manager.has_channel(id)); assert!(manager.has_channel(id));
@@ -938,7 +938,7 @@ mod tests {
async fn control_handler_success_path_returns_not_implemented() { async fn control_handler_success_path_returns_not_implemented() {
let manager = make_manager().await; let manager = make_manager().await;
let (id, _send, _recv) = manager let (id, _send, _recv) = manager
.open_channel("alknet/tty", "alice", None) .open_channel("alk/tty", "alice", None)
.await .await
.expect("open"); .expect("open");
let handler = make_control_handler(manager); let handler = make_control_handler(manager);
@@ -1035,13 +1035,13 @@ mod tests {
spawned_clone.store(true, Ordering::SeqCst); spawned_clone.store(true, Ordering::SeqCst);
tokio::spawn(async {}) tokio::spawn(async {})
}); });
let auth = AuthContext::anonymous(b"alknet/call"); let auth = AuthContext::anonymous(b"alk/call");
let handler = make_open_handler_once( let handler = make_open_handler_once(
manager.clone(), manager.clone(),
policy, policy,
open_handler, open_handler,
auth, auth,
"alknet/tty".to_string(), "alk/tty".to_string(),
); );
let env = handler(json!({}), test_context("open-once-1")).await; let env = handler(json!({}), test_context("open-once-1")).await;
match env.result { match env.result {
@@ -1065,13 +1065,13 @@ mod tests {
spawned_clone.store(true, Ordering::SeqCst); spawned_clone.store(true, Ordering::SeqCst);
tokio::spawn(async {}) tokio::spawn(async {})
}); });
let auth = AuthContext::anonymous(b"alknet/call"); let auth = AuthContext::anonymous(b"alk/call");
let handler = make_open_handler_stream( let handler = make_open_handler_stream(
manager.clone(), manager.clone(),
policy, policy,
open_handler, open_handler,
auth, auth,
"alknet/tty".to_string(), "alk/tty".to_string(),
); );
let mut stream = handler(json!({}), test_context("open-stream-1")); let mut stream = handler(json!({}), test_context("open-stream-1"));
let env = stream.next().await.expect("one envelope"); let env = stream.next().await.expect("one envelope");
@@ -1095,14 +1095,9 @@ mod tests {
let manager = make_manager().await; let manager = make_manager().await;
let policy = super::super::policy::default_policy(); let policy = super::super::policy::default_policy();
let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {})); let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {}));
let auth = AuthContext::anonymous(b"alknet/call"); let auth = AuthContext::anonymous(b"alk/call");
let handler = make_open_handler_sink( let handler =
manager, make_open_handler_sink(manager, policy, open_handler, auth, "alk/tty".to_string());
policy,
open_handler,
auth,
"alknet/tty".to_string(),
);
let publish_stream: crate::registry::registration::PublishStream = let publish_stream: crate::registry::registration::PublishStream =
Box::pin(futures::stream::empty()); Box::pin(futures::stream::empty());
let env = handler(json!({}), test_context("open-sink-1"), publish_stream).await; let env = handler(json!({}), test_context("open-sink-1"), publish_stream).await;
@@ -1127,7 +1122,7 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(ChannelOpenSpec::new("alknet/tty")); .with_channel_open(ChannelOpenSpec::new("alk/tty"));
let spawned = Arc::new(AtomicBool::new(false)); let spawned = Arc::new(AtomicBool::new(false));
let spawned_clone = Arc::clone(&spawned); let spawned_clone = Arc::clone(&spawned);
let open_handler: OpenHandler = Arc::new(move |_input, _conn, _auth| { let open_handler: OpenHandler = Arc::new(move |_input, _conn, _auth| {
@@ -1139,7 +1134,7 @@ mod tests {
spec, spec,
open_handler, open_handler,
&mut registry, &mut registry,
AuthContext::anonymous(b"alknet/call"), AuthContext::anonymous(b"alk/call"),
) )
.expect("register"); .expect("register");
assert!(registry.registration("channels/tty/sub").is_some()); assert!(registry.registration("channels/tty/sub").is_some());
@@ -1170,7 +1165,7 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(ChannelOpenSpec::new("alknet/tty")); .with_channel_open(ChannelOpenSpec::new("alk/tty"));
let spawned = Arc::new(AtomicBool::new(false)); let spawned = Arc::new(AtomicBool::new(false));
let spawned_clone = Arc::clone(&spawned); let spawned_clone = Arc::clone(&spawned);
let open_handler: OpenHandler = Arc::new(move |_input, _conn, _auth| { let open_handler: OpenHandler = Arc::new(move |_input, _conn, _auth| {
@@ -1182,7 +1177,7 @@ mod tests {
spec, spec,
open_handler, open_handler,
&mut registry, &mut registry,
AuthContext::anonymous(b"alknet/call"), AuthContext::anonymous(b"alk/call"),
) )
.expect("register"); .expect("register");
let mut stream = let mut stream =
@@ -1212,14 +1207,14 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(ChannelOpenSpec::new("alknet/tty")); .with_channel_open(ChannelOpenSpec::new("alk/tty"));
let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {})); let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {}));
let mut registry = OperationRegistry::new(); let mut registry = OperationRegistry::new();
core.register_openable( core.register_openable(
spec, spec,
open_handler, open_handler,
&mut registry, &mut registry,
AuthContext::anonymous(b"alknet/call"), AuthContext::anonymous(b"alk/call"),
) )
.expect("register"); .expect("register");
assert!(registry.registration("channels/tty/pub").is_some()); assert!(registry.registration("channels/tty/pub").is_some());
@@ -1249,7 +1244,7 @@ mod tests {
spec, spec,
open_handler, open_handler,
&mut registry, &mut registry,
AuthContext::anonymous(b"alknet/call"), AuthContext::anonymous(b"alk/call"),
); );
assert!(result.is_err()); assert!(result.is_err());
assert!(result.unwrap_err().contains("channel_open marker")); assert!(result.unwrap_err().contains("channel_open marker"));
@@ -1261,7 +1256,7 @@ mod tests {
let policy: Arc<dyn ChannelLifecyclePolicy> = let policy: Arc<dyn ChannelLifecyclePolicy> =
Arc::new(super::super::policy::PerIdentityChannelPolicy::new(0)); Arc::new(super::super::policy::PerIdentityChannelPolicy::new(0));
let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {})); let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {}));
let auth = AuthContext::anonymous(b"alknet/call"); let auth = AuthContext::anonymous(b"alk/call");
let opener_id = Identity { let opener_id = Identity {
id: "alice".to_string(), id: "alice".to_string(),
scopes: vec![], scopes: vec![],
@@ -1272,7 +1267,7 @@ mod tests {
&policy, &policy,
&open_handler, &open_handler,
&auth, &auth,
"alknet/tty", "alk/tty",
json!({}), json!({}),
opener_id.id.clone(), opener_id.id.clone(),
opener_id, opener_id,
+1 -1
View File
@@ -29,7 +29,7 @@ pub const CHUNK_HEADER_LEN: usize = 8;
/// so the demux can always resync by reading the next 8-byte header. /// so the demux can always resync by reading the next 8-byte header.
pub const MAX_CHUNK_LEN: u32 = 16 * 1024 * 1024; pub const MAX_CHUNK_LEN: u32 = 16 * 1024 * 1024;
/// A chunk channel ID of 0 is pre-negotiated as `alknet/call` (ADR-036). /// A chunk channel ID of 0 is pre-negotiated as `alk/call` (ADR-036).
/// Both sides know `channel_id = 0` is routed to the `CallAdapter` /// Both sides know `channel_id = 0` is routed to the `CallAdapter`
/// without an explicit open op exchange. /// without an explicit open op exchange.
pub const CHANNEL_ID_ZERO: u32 = 0; pub const CHANNEL_ID_ZERO: u32 = 0;
+2 -2
View File
@@ -26,7 +26,7 @@ use crate::protocol::connection::CallConnection;
use crate::protocol::dispatch::Dispatcher; use crate::protocol::dispatch::Dispatcher;
use crate::registry::registration::OperationRegistry; use crate::registry::registration::OperationRegistry;
/// Outbound `alknet/call` connection opener (the #1 gap, ADR-017 §1). /// Outbound `alk/call` connection opener (the #1 gap, ADR-017 §1).
/// ///
/// Peer authorization flows through the existing `AccessControl::check` gate /// Peer authorization flows through the existing `AccessControl::check` gate
/// in `OperationRegistry::invoke` (ADR-029 §3) — no parallel `remote_safe`/ /// in `OperationRegistry::invoke` (ADR-029 §3) — no parallel `remote_safe`/
@@ -184,7 +184,7 @@ mod tests {
conn.connection() conn.connection()
.expect("quic connection present") .expect("quic connection present")
.remote_alpn(), .remote_alpn(),
b"alknet/call" b"alk/call"
); );
std::mem::drop(conn); std::mem::drop(conn);
} }
+13 -13
View File
@@ -257,7 +257,7 @@ fn rebuild_spec_for(
// ADR-047 §2: the `channel_open` marker survives discovery // ADR-047 §2: the `channel_open` marker survives discovery
// serialization as a boolean. The ALPN is derived from the op name // serialization as a boolean. The ALPN is derived from the op name
// (`channels/<alpn>/sub` → `alknet/<alpn>`). The consumer (e.g. the // (`channels/<alpn>/sub` → `alk/<alpn>`). The consumer (e.g. the
// hub) branches on the marker to wrap marked ops with relay // hub) branches on the marker to wrap marked ops with relay
// machinery (ADR-047 §1, Gap C) instead of the plain forwarding // machinery (ADR-047 §1, Gap C) instead of the plain forwarding
// stub. The marker is on the spec so the consumer can see it // stub. The marker is on the spec so the consumer can see it
@@ -282,12 +282,12 @@ fn rebuild_spec_for(
} }
/// Derive the data-plane ALPN from an open-op name /// Derive the data-plane ALPN from an open-op name
/// (`channels/<alpn>/sub` → `alknet/<alpn>`). Returns `None` for op /// (`channels/<alpn>/sub` → `alk/<alpn>`). Returns `None` for op
/// names that don't match the `channels/<segment>/(sub|pub)` shape — /// names that don't match the `channels/<segment>/(sub|pub)` shape —
/// the op is not a channel-open op, and the marker (if present) is /// the op is not a channel-open op, and the marker (if present) is
/// ignored. ADR-047 §"Negative": the path segment is the ALPN with the /// ignored. ADR-047 §"Negative": the path segment is the ALPN with the
/// `alknet/` prefix stripped; ALPNs without that prefix use their full /// `alk/` prefix stripped; ALPNs without that prefix use their full
/// ALPN string as the path segment (rare case). Multi-segment non-`alknet/*` /// ALPN string as the path segment (rare case). Multi-segment non-`alk/*`
/// ALPNs (e.g. `custom/proto`) survive because the `/sub` or `/pub` /// ALPNs (e.g. `custom/proto`) survive because the `/sub` or `/pub`
/// suffix is stripped from `rest` rather than taking only the first /// suffix is stripped from `rest` rather than taking only the first
/// path segment. /// path segment.
@@ -299,10 +299,10 @@ fn derive_alpn_from_op_name(op_name: &str) -> Option<String> {
if segment.is_empty() { if segment.is_empty() {
return None; return None;
} }
if segment.starts_with("alknet/") || segment == "alknet" || segment.contains('/') { if segment.starts_with("alk/") || segment == "alk" || segment.contains('/') {
Some(segment.to_string()) Some(segment.to_string())
} else { } else {
Some(format!("alknet/{segment}")) Some(format!("alk/{segment}"))
} }
} }
@@ -597,7 +597,7 @@ mod tests {
schema["channel_open"] = json!(true); schema["channel_open"] = json!(true);
let spec = rebuild_spec_for(&schema, "channels/tty/sub", &None).expect("rebuild"); let spec = rebuild_spec_for(&schema, "channels/tty/sub", &None).expect("rebuild");
let marker = spec.channel_open.expect("channel_open marker parsed"); let marker = spec.channel_open.expect("channel_open marker parsed");
assert_eq!(marker.alpn, "alknet/tty"); assert_eq!(marker.alpn, "alk/tty");
} }
#[test] #[test]
@@ -697,11 +697,11 @@ mod tests {
fn derive_alpn_from_op_name_strips_channels_prefix() { fn derive_alpn_from_op_name_strips_channels_prefix() {
assert_eq!( assert_eq!(
derive_alpn_from_op_name("channels/tty/sub"), derive_alpn_from_op_name("channels/tty/sub"),
Some("alknet/tty".to_string()) Some("alk/tty".to_string())
); );
assert_eq!( assert_eq!(
derive_alpn_from_op_name("channels/tunnel/pub"), derive_alpn_from_op_name("channels/tunnel/pub"),
Some("alknet/tunnel".to_string()) Some("alk/tunnel".to_string())
); );
} }
@@ -717,7 +717,7 @@ mod tests {
assert_eq!( assert_eq!(
derive_alpn_from_op_name("channels/custom/proto/sub"), derive_alpn_from_op_name("channels/custom/proto/sub"),
Some("custom/proto".to_string()), Some("custom/proto".to_string()),
"multi-segment non-alknet/* ALPN uses full ALPN as the path segment" "multi-segment non-alk/* ALPN uses full ALPN as the path segment"
); );
assert_eq!( assert_eq!(
derive_alpn_from_op_name("channels/vendor/service/run/pub"), derive_alpn_from_op_name("channels/vendor/service/run/pub"),
@@ -728,8 +728,8 @@ mod tests {
#[test] #[test]
fn derive_alpn_from_op_name_explicit_alknet_prefix_returned_as_is() { fn derive_alpn_from_op_name_explicit_alknet_prefix_returned_as_is() {
assert_eq!( assert_eq!(
derive_alpn_from_op_name("channels/alknet/tty/sub"), derive_alpn_from_op_name("channels/alk/tty/sub"),
Some("alknet/tty".to_string()) Some("alk/tty".to_string())
); );
} }
@@ -737,7 +737,7 @@ mod tests {
fn derive_alpn_from_op_name_strips_pub_suffix() { fn derive_alpn_from_op_name_strips_pub_suffix() {
assert_eq!( assert_eq!(
derive_alpn_from_op_name("channels/tty/pub"), derive_alpn_from_op_name("channels/tty/pub"),
Some("alknet/tty".to_string()) Some("alk/tty".to_string())
); );
} }
+4 -4
View File
@@ -81,19 +81,19 @@ mod tests {
fn auth_context_is_clone() { fn auth_context_is_clone() {
let ctx = AuthContext { let ctx = AuthContext {
identity: None, identity: None,
alpn: b"alknet/test".to_vec(), alpn: b"alk/test".to_vec(),
remote_addr: None, remote_addr: None,
tls_client_fingerprint: None, tls_client_fingerprint: None,
}; };
let cloned = ctx.clone(); let cloned = ctx.clone();
assert_eq!(cloned.alpn, b"alknet/test"); assert_eq!(cloned.alpn, b"alk/test");
assert!(cloned.identity.is_none()); assert!(cloned.identity.is_none());
} }
#[test] #[test]
fn auth_context_anonymous_sets_alpn_only() { fn auth_context_anonymous_sets_alpn_only() {
let ctx = AuthContext::anonymous(b"alknet/test"); let ctx = AuthContext::anonymous(b"alk/test");
assert_eq!(ctx.alpn, b"alknet/test"); assert_eq!(ctx.alpn, b"alk/test");
assert!(ctx.identity.is_none()); assert!(ctx.identity.is_none());
assert!(ctx.remote_addr.is_none()); assert!(ctx.remote_addr.is_none());
assert!(ctx.tls_client_fingerprint.is_none()); assert!(ctx.tls_client_fingerprint.is_none());
+4 -4
View File
@@ -603,10 +603,10 @@ mod from_source_tests {
addr, addr,
closed: Arc::clone(&recorded), closed: Arc::clone(&recorded),
}, },
b"alknet/test".to_vec(), b"alk/test".to_vec(),
); );
assert_eq!(conn.remote_alpn(), b"alknet/test"); assert_eq!(conn.remote_alpn(), b"alk/test");
assert_eq!(conn.remote_addr(), addr); assert_eq!(conn.remote_addr(), addr);
let mut stream = conn.accept_bi().await.expect("first accept_bi yields"); let mut stream = conn.accept_bi().await.expect("first accept_bi yields");
@@ -692,7 +692,7 @@ mod tests {
fn test_connection() -> Connection { fn test_connection() -> Connection {
Connection::from_bidi( Connection::from_bidi(
SinkEmpty, SinkEmpty,
b"alknet/test".to_vec(), b"alk/test".to_vec(),
Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234)), Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234)),
) )
} }
@@ -789,7 +789,7 @@ mod tests {
#[test] #[test]
fn connection_remote_alpn_and_addr_from_bidi() { fn connection_remote_alpn_and_addr_from_bidi() {
let conn = test_connection(); let conn = test_connection();
assert_eq!(conn.remote_alpn(), b"alknet/test"); assert_eq!(conn.remote_alpn(), b"alk/test");
assert_eq!( assert_eq!(
conn.remote_addr(), conn.remote_addr(),
Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234)) Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234))
+4 -4
View File
@@ -4,7 +4,7 @@
//! This crate is the unification of the call protocol (structured JSON RPC: //! This crate is the unification of the call protocol (structured JSON RPC:
//! operations, streaming subscriptions, service discovery) and the channels //! operations, streaming subscriptions, service discovery) and the channels
//! protocol (multiplexing proxy: N logical channels over one transport //! protocol (multiplexing proxy: N logical channels over one transport
//! stream, channel 0 pre-negotiated as `alknet/call`). Both halves share //! stream, channel 0 pre-negotiated as `alk/call`). Both halves share
//! the vendored core types and the call protocol's `OperationRegistry` — //! the vendored core types and the call protocol's `OperationRegistry` —
//! channel lifecycle is orchestrated by call operations on channel 0 //! channel lifecycle is orchestrated by call operations on channel 0
//! (ADR-047: openable ALPNs are operations). //! (ADR-047: openable ALPNs are operations).
@@ -25,7 +25,7 @@
//! wire format, demux/mux, `ChannelManager`, `ChannelsAdapter`, //! wire format, demux/mux, `ChannelManager`, `ChannelsAdapter`,
//! `ChannelBidiStreamSource`, `ChannelOperations`, //! `ChannelBidiStreamSource`, `ChannelOperations`,
//! `ChannelLifecyclePolicy`, `ChannelClient`. Channel 0 is //! `ChannelLifecyclePolicy`, `ChannelClient`. Channel 0 is
//! pre-negotiated as `alknet/call`; channels 1..N are opened via //! pre-negotiated as `alk/call`; channels 1..N are opened via
//! per-ALPN open ops (`channels/<alpn>/sub`, `channels/<alpn>/pub`) //! per-ALPN open ops (`channels/<alpn>/sub`, `channels/<alpn>/pub`)
//! on channel 0 (ADR-047). //! on channel 0 (ADR-047).
//! //!
@@ -47,7 +47,7 @@
//! //!
//! A single process can be a producer of some ops, a consumer of others, //! 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 //! a channel opener for TTY, and a channel acceptor for tunnels — all on
//! the same `alknet/channels` connection. //! the same `alk/channels` connection.
//! //!
//! ### Pattern for protocol crates //! ### Pattern for protocol crates
//! //!
@@ -65,7 +65,7 @@
//! //!
//! The protocol crate doesn't know whether it's running on a hub, a //! The protocol crate doesn't know whether it's running on a hub, a
//! spoke, or a standalone process. The networking + composition layer //! spoke, or a standalone process. The networking + composition layer
//! (alknet/alknode) wires protocol crates' producers into registries //! (alk/alknode) wires protocol crates' producers into registries
//! and consumers into clients. //! and consumers into clients.
pub mod channels; pub mod channels;
+6 -6
View File
@@ -1,4 +1,4 @@
//! `CallAdapter`: implements `ProtocolHandler` for ALPN `alknet/call`. //! `CallAdapter`: implements `ProtocolHandler` for ALPN `alk/call`.
//! //!
//! Accepts bidirectional streams, reads `EventEnvelope` frames, and //! Accepts bidirectional streams, reads `EventEnvelope` frames, and
//! dispatches `call.requested` events to the operation registry. See //! dispatches `call.requested` events to the operation registry. See
@@ -148,7 +148,7 @@ impl CallAdapter {
#[async_trait] #[async_trait]
impl ProtocolHandler for CallAdapter { impl ProtocolHandler for CallAdapter {
fn alpn(&self) -> &'static [u8] { fn alpn(&self) -> &'static [u8] {
b"alknet/call" b"alk/call"
} }
async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError> { async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError> {
@@ -294,7 +294,7 @@ mod tests {
let registry = Arc::new(OperationRegistry::new()); let registry = Arc::new(OperationRegistry::new());
let provider: Arc<dyn IdentityProvider> = Arc::new(StaticIdentityProvider::new()); let provider: Arc<dyn IdentityProvider> = Arc::new(StaticIdentityProvider::new());
let adapter = CallAdapter::new(registry, provider); let adapter = CallAdapter::new(registry, provider);
assert_eq!(adapter.alpn(), b"alknet/call"); assert_eq!(adapter.alpn(), b"alk/call");
} }
#[test] #[test]
@@ -837,7 +837,7 @@ mod tests {
let conn = stub_connection(); let conn = stub_connection();
let auth = AuthContext { let auth = AuthContext {
identity: Some(identity_with_scopes("caller", &["user"])), identity: Some(identity_with_scopes("caller", &["user"])),
alpn: b"alknet/call".to_vec(), alpn: b"alk/call".to_vec(),
remote_addr: None, remote_addr: None,
tls_client_fingerprint: None, tls_client_fingerprint: None,
}; };
@@ -855,7 +855,7 @@ mod tests {
let conn = stub_connection(); let conn = stub_connection();
let auth = AuthContext { let auth = AuthContext {
identity: None, identity: None,
alpn: b"alknet/call".to_vec(), alpn: b"alk/call".to_vec(),
remote_addr: None, remote_addr: None,
tls_client_fingerprint: None, tls_client_fingerprint: None,
}; };
@@ -871,7 +871,7 @@ mod tests {
let conn = stub_connection(); let conn = stub_connection();
let auth = AuthContext { let auth = AuthContext {
identity: None, identity: None,
alpn: b"alknet/call".to_vec(), alpn: b"alk/call".to_vec(),
remote_addr: None, remote_addr: None,
tls_client_fingerprint: None, tls_client_fingerprint: None,
}; };
+4 -4
View File
@@ -1,4 +1,4 @@
//! `CallConnection`: an established `alknet/call` connection (either //! `CallConnection`: an established `alk/call` connection (either
//! direction — accepted or opened). Holds the connection's Layer 2 overlay //! direction — accepted or opened). Holds the connection's Layer 2 overlay
//! (imported ops). //! (imported ops).
//! //!
@@ -190,7 +190,7 @@ impl CallConnection {
/// `forwarded_for` (ADR-032) and `auth_token` (ADR-017 §7) for the hub /// `forwarded_for` (ADR-032) and `auth_token` (ADR-017 §7) for the hub
/// forwarding path used by `from_call`. /// forwarding path used by `from_call`.
/// ///
/// In stream-per-request mode (top-level `alknet/call`), opens a fresh /// In stream-per-request mode (top-level `alk/call`), opens a fresh
/// bidi stream per call and pumps the read half in a spawned task. In /// bidi stream per call and pumps the read half in a spawned task. In
/// single-stream mode (channel 0, ADR-036 amendment), writes /// single-stream mode (channel 0, ADR-036 amendment), writes
/// `call.requested` through the shared frame writer and relies on the /// `call.requested` through the shared frame writer and relies on the
@@ -442,7 +442,7 @@ impl CallConnection {
/// sink forwarding handler. Mirrors /// sink forwarding handler. Mirrors
/// [`subscribe_with_payload`](Self::subscribe_with_payload). /// [`subscribe_with_payload`](Self::subscribe_with_payload).
/// ///
/// In stream-per-request mode (top-level `alknet/call`), opens a /// In stream-per-request mode (top-level `alk/call`), opens a
/// fresh bidi stream, writes `call.requested` + chunks + /// fresh bidi stream, writes `call.requested` + chunks +
/// `call.completed` on the same write half, and pumps the read half /// `call.completed` on the same write half, and pumps the read half
/// concurrently for the single `call.responded` / `call.error` /// concurrently for the single `call.responded` / `call.error`
@@ -1054,7 +1054,7 @@ mod tests {
conn.connection() conn.connection()
.expect("quic connection present") .expect("quic connection present")
.remote_alpn(), .remote_alpn(),
b"alknet/call" b"alk/call"
); );
} }
+1 -1
View File
@@ -1,4 +1,4 @@
//! Shared dispatch loop for `alknet/call` connections. //! Shared dispatch loop for `alk/call` connections.
//! //!
//! Both [`super::adapter::CallAdapter`]'s accept path and //! Both [`super::adapter::CallAdapter`]'s accept path and
//! [`crate::client::CallClient`]'s connect path produce a [`CallConnection`] //! [`crate::client::CallClient`]'s connect path produce a [`CallConnection`]
+1 -1
View File
@@ -1,6 +1,6 @@
//! Call protocol: wire format, streams, and the call adapter. //! Call protocol: wire format, streams, and the call adapter.
//! //!
//! Implements `ProtocolHandler` for ALPN `alknet/call` on top of the //! Implements `ProtocolHandler` for ALPN `alk/call` on top of the
//! operation registry. See `docs/architecture/` for the full specification. //! operation registry. See `docs/architecture/` for the full specification.
pub mod abort; pub mod abort;
+3 -3
View File
@@ -61,7 +61,7 @@ impl AsyncWrite for SinkEmpty {
pub(crate) fn sink_empty_connection() -> Connection { pub(crate) fn sink_empty_connection() -> Connection {
Connection::from_bidi( Connection::from_bidi(
SinkEmpty, SinkEmpty,
b"alknet/call".to_vec(), b"alk/call".to_vec(),
Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321)), Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321)),
) )
} }
@@ -121,11 +121,11 @@ pub(crate) fn duplex_connection_pair(buffer: usize) -> (Connection, Connection)
let addr = Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321)); let addr = Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321));
let client = Connection::from_source( let client = Connection::from_source(
SingleStreamSource::new(BiStream::from_bidi(client_end), addr), SingleStreamSource::new(BiStream::from_bidi(client_end), addr),
b"alknet/call".to_vec(), b"alk/call".to_vec(),
); );
let server = Connection::from_source( let server = Connection::from_source(
SingleStreamSource::new(BiStream::from_bidi(server_end), addr), SingleStreamSource::new(BiStream::from_bidi(server_end), addr),
b"alknet/call".to_vec(), b"alk/call".to_vec(),
); );
(client, server) (client, server)
} }
+1 -1
View File
@@ -821,7 +821,7 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(super::super::spec::ChannelOpenSpec::new("alknet/tty")); .with_channel_open(super::super::spec::ChannelOpenSpec::new("alk/tty"));
let json_val = spec_to_json(&spec); let json_val = spec_to_json(&spec);
assert_eq!(json_val.get("channel_open"), Some(&json!(true))); assert_eq!(json_val.get("channel_open"), Some(&json!(true)));
} }
+4 -4
View File
@@ -31,8 +31,8 @@ pub enum Visibility {
/// dispatch hint. /// dispatch hint.
/// ///
/// `alpn` is the data-plane ALPN the channel will carry (e.g. /// `alpn` is the data-plane ALPN the channel will carry (e.g.
/// `"alknet/tty"`). It is derivable from the op name /// `"alk/tty"`). It is derivable from the op name
/// (`channels/<alpn>/sub` → `alknet/<alpn>`), but carried here so the /// (`channels/<alpn>/sub` → `alk/<alpn>`), but carried here so the
/// channels layer doesn't have to parse the op name. On the wire /// channels layer doesn't have to parse the op name. On the wire
/// (`services/schema`), the marker is a boolean `"channel_open": true`; /// (`services/schema`), the marker is a boolean `"channel_open": true`;
/// the ALPN is not serialized (it's derivable). /// the ALPN is not serialized (it's derivable).
@@ -371,9 +371,9 @@ mod tests {
AccessControl::default(), AccessControl::default(),
None, None,
) )
.with_channel_open(ChannelOpenSpec::new("alknet/tty")); .with_channel_open(ChannelOpenSpec::new("alk/tty"));
let marker = spec.channel_open.expect("channel_open set"); let marker = spec.channel_open.expect("channel_open set");
assert_eq!(marker.alpn, "alknet/tty"); assert_eq!(marker.alpn, "alk/tty");
} }
#[test] #[test]