From 08e7df2aa0ab2590a917e3dac0d51b8240b90f6f Mon Sep 17 00:00:00 2001 From: deepseek-v4-pro Date: Fri, 14 Aug 2026 13:55:28 +0000 Subject: [PATCH] feat: rename ALPN prefix from alknet/ to alk/ (v0.1.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- AGENTS.md | 11 +- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 +- docs/architecture/README.md | 18 +- docs/architecture/call-README.md | 8 +- docs/architecture/call-protocol.md | 14 +- docs/architecture/channel-client.md | 6 +- docs/architecture/channel-operations.md | 18 +- docs/architecture/channels-README.md | 12 +- docs/architecture/channels-adapter.md | 4 +- docs/architecture/channels-connection.md | 4 +- docs/architecture/channels-overview.md | 32 +- docs/architecture/channels-wire.md | 10 +- docs/architecture/client-and-adapters.md | 10 +- .../decisions/001-alpn-protocol-dispatch.md | 4 +- .../decisions/002-protocol-handler-trait.md | 2 +- ...04-alpn-convention-and-connection-model.md | 48 +- ...ction-from-stream-generic-single-stream.md | 2 +- .../009-bistream-as-the-handler-leaf.md | 2 +- ...tioncredentials-decouple-dial-from-call.md | 6 +- .../013-irpc-as-call-protocol-foundation.md | 2 +- .../015-call-protocol-stream-model.md | 6 +- .../019-operation-registry-layering.md | 4 +- ...ll-protocol-client-and-adapter-contract.md | 2 +- ...llclient-peer-scoped-registry-filtering.md | 2 +- .../decisions/031-crate-decomposition.md | 2 +- .../decisions/034-channels-wire-format.md | 12 +- .../035-channels-pure-channel-multiplexing.md | 18 +- .../036-channel-0-pre-negotiated-call.md | 20 +- .../037-channel-lifecycle-operations.md | 10 +- .../038-channelconnection-bidistreamsource.md | 4 +- .../039-channelsadapter-and-channelmanager.md | 8 +- .../decisions/041-per-identity-channel-cap.md | 2 +- .../042-hub-relay-translate-not-forward.md | 8 +- .../decisions/043-channelclient.md | 6 +- .../044-channels-subcrate-decomposition.md | 12 +- .../045-alknetclient-native-dial-seam.md | 20 +- .../047-openable-alpns-are-operations.md | 14 +- docs/architecture/open-questions.md | 4 +- docs/architecture/operation-registry.md | 4 +- docs/reviews/003-alpn-prefix-rename.md | 501 ++++++++++++++++++ src/channels/adapter.rs | 38 +- src/channels/client.rs | 62 +-- src/channels/manager.rs | 40 +- src/channels/mod.rs | 4 +- src/channels/operations.rs | 43 +- src/channels/wire.rs | 2 +- src/client/call_client.rs | 4 +- src/client/from_call.rs | 26 +- src/core/auth.rs | 8 +- src/core/types.rs | 8 +- src/lib.rs | 8 +- src/protocol/adapter.rs | 12 +- src/protocol/connection.rs | 8 +- src/protocol/dispatch.rs | 2 +- src/protocol/mod.rs | 2 +- src/protocol/test_support.rs | 6 +- src/registry/discovery.rs | 2 +- src/registry/spec.rs | 8 +- 60 files changed, 837 insertions(+), 328 deletions(-) create mode 100644 docs/reviews/003-alpn-prefix-rename.md diff --git a/AGENTS.md b/AGENTS.md index 092f2f4..b33fb41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ This is the call + channels RPC crate — the unification of `alknet-call` (structured JSON RPC: operations, streaming subscriptions, service discovery) and `alknet-channels` (multiplexing proxy: N logical 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` §Project Conventions and are repeated here so they apply to every 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 architecture docs were ported from `/workspace/@alkdev/alknet/docs/architecture/` and renumbered as - alkcall ADRs (001..047). The ALPN strings (`alknet/call`, - `alknet/channels`) are wire-stable and unchanged. + alkcall ADRs (001..047). The ALPN strings (`alk/call`, + `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: **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-035 — channels pure channel multiplexing (no `stream_type`, `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`/ `control`/`resources/subscribe` on channel 0's call registry) - 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-003 — auth as shared core (`IdentityProvider` in core, handlers extract credentials) - - ADR-004 — ALPN string convention (`alknet/` prefix, one ALPN per + - ADR-004 — ALPN string convention (`alk/` prefix, one ALPN per connection) - ADR-005 — `BiStream` type definition (handlers receive `Connection`, not `BiStream`) diff --git a/Cargo.lock b/Cargo.lock index fd1d788..8e1b86f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,7 +27,7 @@ dependencies = [ [[package]] name = "alkcall" -version = "0.1.0" +version = "0.1.1" dependencies = [ "alktype", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index be66ffb..d2b96c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alkcall" -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.85" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index e6d45d2..c7173c8 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ use alkcall::protocol::connection::CallConnection; let connection = Connection::from_bidi( transport_stream, - b"alknet/call".to_vec(), + b"alk/call".to_vec(), Some(remote_addr), ); let conn = CallConnection::new(connection); @@ -74,7 +74,7 @@ use alkcall::core::Connection; let connection = Connection::from_bidi( transport_stream, - b"alknet/channels".to_vec(), + b"alk/channels".to_vec(), Some(remote_addr), ); 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 channel opener for TTY, and a channel acceptor for tunnels — all on the -same `alknet/channels` connection. +same `alk/channels` connection. ## Documentation diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 23fbed6..688de18 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -7,13 +7,13 @@ last_updated: 2026-08-12 The call + channels RPC crate. Structured JSON RPC (operations, streaming subscriptions, service discovery) and N-channel multiplexing over one -transport stream (channel 0 pre-negotiated as `alknet/call`). +transport stream (channel 0 pre-negotiated as `alk/call`). This crate unifies `alknet-call` and `alknet-channels` from the alknet mono-repo, plus the vendored core types formerly in `alknet-core`. The source architecture docs were ported from `/workspace/@alkdev/alknet/docs/architecture/` and renumbered as alkcall -ADRs (ADR-001..045). The ALPN strings (`alknet/call`, `alknet/channels`) +ADRs (ADR-001..045). The ALPN strings (`alk/call`, `alk/channels`) are wire-stable and unchanged — see ADR-004. ## Documents @@ -41,7 +41,7 @@ are wire-stable and unchanged — see ADR-004. | [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | HandlerRegistry, ALPN routing | | [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | The trait every handler implements | | [003](decisions/003-auth-as-shared-core.md) | Auth as Shared Core | IdentityProvider, Identity, AuthToken | -| [004](decisions/004-alpn-convention-and-connection-model.md) | ALPN String Convention | `alknet/` prefix, one ALPN per connection | +| [004](decisions/004-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/` prefix, one ALPN per connection | | [005](decisions/005-bistream-type-definition.md) | BiStream Type Definition | BiStream, handlers receive Connection | | [006](decisions/006-authcontext-structure.md) | AuthContext Structure | AuthContext fields, hybrid resolution | | [007](decisions/007-connection-from-stream-generic-single-stream.md) | Connection::from_stream | Generic single-stream connections | @@ -88,7 +88,7 @@ are wire-stable and unchanged — see ADR-004. |-----|-------|-----------| | [034](decisions/034-channels-wire-format.md) | Channels Wire Format | 8-byte chunk header; one-way door | | [035](decisions/035-channels-pure-channel-multiplexing.md) | Pure Channel Multiplexing | No stream_type; BiStream-only; handler owns sub-mux | -| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alknet/call | +| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alk/call | | [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations | channel/open, close, control, resources/subscribe | | [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel BidiStreamSource; yield-once accept_bi | | [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Demux/mux; ALPN-blind, auth-blind | @@ -119,7 +119,7 @@ questions affecting this crate: ## Key Design Principles -1. **One connection, full access**: An `alknet/call` connection gives +1. **One connection, full access**: An `alk/call` connection gives access to the entire operation registry. 2. **Protocol is symmetric**: Both sides can initiate calls. Producer/ consumer, not server/client. @@ -135,7 +135,7 @@ questions affecting this crate: See ADR-024. 8. **Streams are streams**: Every channel is a BiStream. The handler owns its sub-stream multiplexing. See ADR-035. -9. **Channel 0 is alknet/call**: Channel lifecycle is call operations on +9. **Channel 0 is alk/call**: Channel lifecycle is call operations on channel 0. See ADR-036, ADR-037. 10. **Wire formats are stable**: EventEnvelope shape and the 8-byte chunk header are one-way doors. See ADR-014, ADR-034. @@ -157,7 +157,7 @@ Downstream crates compose on top of it in a layered dependency chain. A single process can be a producer of some ops, a consumer of others, a channel opener for TTY, and a channel acceptor for tunnels — all on the -same `alknet/channels` connection. The types don't encode the system +same `alk/channels` connection. The types don't encode the system role; they just don't prevent any combination. Within a single connection, direction is independent of role: @@ -225,7 +225,7 @@ alkcall. They provide two halves: (e.g. `trader_client.status().await` instead of `call_connection.call("trader/status", ...).await`). -**alknet/alknode** depends on alkcall + whichever protocol crates are +**alk/alknode** depends on alkcall + whichever protocol crates are needed. It's the composition layer — the only place that knows about network topology, peer routing, and which protocol crates are wired in. Protocol crates never import a QUIC or TLS dependency. @@ -250,7 +250,7 @@ A protocol crate that uses channels (e.g. alktty) follows this pattern: │ │ ┌────────┴────────┐ ┌───────┴──────────────┐ │ Direct ALPN │ │ Through channels │ -│ (alknet/tty) │ │ (alknet/channels) │ +│ (alk/tty) │ │ (alk/channels) │ │ │ │ │ │ TtyAdapter │ │ OpenHandler │ │ impl Protocol │ │ (registered via │ diff --git a/docs/architecture/call-README.md b/docs/architecture/call-README.md index 3e27e8a..3853f17 100644 --- a/docs/architecture/call-README.md +++ b/docs/architecture/call-README.md @@ -6,7 +6,7 @@ review: call/review-call passed 2026-06-23 — registry, protocol, ADR (005/012/ # alknet-call -Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alknet/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-007), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps (the dial is in `alknet-client`, the TLS config is in `alknet-tls`). +Structured RPC: operations, request/response, streaming subscriptions, and service discovery. Implements `ProtocolHandler` on ALPN `alk/call`. Runs over QUIC (quinn/iroh) and, via `Connection::from_stream` (ADR-007), over any `AsyncRead + AsyncWrite` transport. A pure protocol crate — no TLS or transport deps (the dial is in `alknet-client`, the TLS config is in `alknet-tls`). ## Documents @@ -20,7 +20,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi | ADR | Title | Relevance | |-----|-------|-----------| -| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alknet/call` | +| [001](decisions/001-alpn-protocol-dispatch.md) | ALPN-Based Protocol Dispatch | CallAdapter registers on ALPN `alk/call` | | [002](decisions/002-protocol-handler-trait.md) | ProtocolHandler Trait | CallAdapter implements ProtocolHandler | | [003](decisions/003-crate-decomposition.md) | Crate Decomposition | alknet-call depends on alknet-core (no irpc — ADR-014) | | [013](decisions/013-rust-canonical-implementation.md) | Rust as Canonical Implementation Language | Adapter traits defined in Rust; TS is reference/browser adaptation | @@ -28,7 +28,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi | [005](decisions/005-irpc-as-call-protocol-foundation.md) | ~~irpc as Call Protocol Foundation~~ | ~~Accepted~~ → **Superseded** by [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) (irpc was never integrated; framing is hand-rolled) | | [064](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-Rolled EventEnvelope Framing | Wire format, registry, dispatch are hand-rolled in alknet-call; supersedes ADR-013 | | [065](decisions/065-connection-from-stream-generic-single-stream.md) | `Connection::from_stream` | Generic single-stream connections; unblocks TCP+TLS/SSH/WT/wasm dispatch | -| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alknet/call` ALPN, one ALPN per connection | +| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention | `alk/call` ALPN, one ALPN per connection | | [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | CallAdapter receives Connection, not BiStream | | [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Vault accessed at assembly layer, not on the wire | | [010](decisions/010-alpn-router-and-endpoint.md) | ALPN Router and Endpoint | Static handler registration | @@ -73,7 +73,7 @@ Structured RPC: operations, request/response, streaming subscriptions, and servi ## Key Design Principles -1. **One connection, full access**: An `alknet/call` connection gives access to the entire operation registry — calls, subscriptions, batch, schema. +1. **One connection, full access**: An `alk/call` connection gives access to the entire operation registry — calls, subscriptions, batch, schema. 2. **Protocol is symmetric**: Both sides can initiate calls. The server calling a client uses the same EventEnvelope format and correlation. 3. **Stream-agnostic correlation**: PendingRequestMap correlates by request ID, not by stream. The protocol works with any stream arrangement. 4. **Operation registry is layered**: The curated layer (`Local` provenance) is static — registered at startup by the CLI binary, immutable for the process lifetime. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic overlays at their respective scopes (per-session, per-connection). The registry supports JSON Schema discovery. See ADR-019. diff --git a/docs/architecture/call-protocol.md b/docs/architecture/call-protocol.md index 26ab99d..72af52d 100644 --- a/docs/architecture/call-protocol.md +++ b/docs/architecture/call-protocol.md @@ -5,13 +5,13 @@ last_updated: 2026-07-09 # Call Protocol -The wire protocol, stream model, framing, and adapter that alknet-call implements on ALPN `alknet/call`. +The wire protocol, stream model, framing, and adapter that alknet-call implements on ALPN `alk/call`. ## What -The call protocol is a bidirectional, transport-agnostic RPC protocol that runs over any ordered, reliable bidirectional stream within a single `alknet/call` connection. It supports request/response calls, streaming subscriptions, batch operations, and service discovery — all using the same EventEnvelope wire format. +The call protocol is a bidirectional, transport-agnostic RPC protocol that runs over any ordered, reliable bidirectional stream within a single `alk/call` connection. It supports request/response calls, streaming subscriptions, batch operations, and service discovery — all using the same EventEnvelope wire format. -The `CallAdapter` implements `ProtocolHandler` for ALPN `alknet/call`. It receives a `Connection` from the endpoint (QUIC-native, or TCP+TLS/WebTransport/SSH via `Connection::from_stream` — ADR-007), accepts bidirectional streams, and dispatches incoming `EventEnvelope` messages to the operation registry. +The `CallAdapter` implements `ProtocolHandler` for ALPN `alk/call`. It receives a `Connection` from the endpoint (QUIC-native, or TCP+TLS/WebTransport/SSH via `Connection::from_stream` — ADR-007), accepts bidirectional streams, and dispatches incoming `EventEnvelope` messages to the operation registry. ## Why @@ -104,13 +104,13 @@ below. ### CallConnection -A `CallConnection` represents an established `alknet/call` connection, +A `CallConnection` represents an established `alk/call` connection, regardless of which side opened it (ADR-022). It holds the connection's imported-ops overlay (Layer 2, ADR-019) — the set of `from_call`-imported operations discovered when the connection was established. ```rust -/// An established alknet/call connection (either direction — accepted or +/// An established alk/call connection (either direction — accepted or /// opened). Holds the connection's Layer 2 overlay (imported ops). pub struct CallConnection { /// The underlying transport Connection (from endpoint.accept, @@ -197,7 +197,7 @@ The call protocol uses bidirectional streams with EventEnvelope framing (transpo - **Either side can open streams**: The client opens a stream to call a server operation. The server opens a stream to call a client operation. Both use `open_bi()` and `accept_bi()`. - **Correlation by request ID**: The `id` field in `EventEnvelope` correlates requests with responses. A response arriving on stream N can fulfill a request sent on stream M. The `PendingRequestMap` is keyed by ID, not by stream. - **Stream usage is the client's choice**: A client may open one stream per operation, one stream for all operations, or any mix. The server processes EventEnvelopes regardless of stream origin. -- **One connection, full access**: A single `alknet/call` connection provides access to all operations (call, subscribe, batch, schema). No need for multiple connections or multiple ALPNs. +- **One connection, full access**: A single `alk/call` connection provides access to all operations (call, subscribe, batch, schema). No need for multiple connections or multiple ALPNs. ### Wire Format: EventEnvelope @@ -569,7 +569,7 @@ Handlers clean up resources when their call is cancelled (in Rust, the future is |----------|-----|---------| | Hand-rolled EventEnvelope framing (irpc never integrated) | [ADR-014](decisions/064-irpc-never-integrated-hand-rolled-framing.md) | Hand-rolled length-prefixed JSON framing, operation registry, dispatch; supersedes ADR-013 (irpc was never imported) | | Call protocol stream model | [ADR-015](decisions/012-call-protocol-stream-model.md) | Bidirectional streams, EventEnvelope, ID-based correlation | -| ALPN per connection | [ADR-004](decisions/006-alpn-convention-and-connection-model.md) | `alknet/call` is a distinct ALPN, one connection per ALPN | +| ALPN per connection | [ADR-004](decisions/006-alpn-convention-and-connection-model.md) | `alk/call` is a distinct ALPN, one connection per ALPN | | ProtocolHandler receives Connection | [ADR-005](decisions/007-bistream-type-definition.md) | CallAdapter gets Connection, can accept/open multiple streams | | Vault integration point | [ADR-008](decisions/008-secret-service-integration.md) | Vault is a capability source, accessed at assembly time | | Secret material flow | [ADR-010](decisions/014-secret-material-flow-and-capability-injection.md) | Call protocol carries no secret material; capabilities injected at assembly layer | diff --git a/docs/architecture/channel-client.md b/docs/architecture/channel-client.md index fac7bf7..4356a2d 100644 --- a/docs/architecture/channel-client.md +++ b/docs/architecture/channel-client.md @@ -30,13 +30,13 @@ pub struct ChannelClient { impl ChannelClient { /// Construct a `ChannelClient` over a pre-established transport - /// `Connection` on ALPN `alknet/channels`. This is the + /// `Connection` on ALPN `alk/channels`. This is the /// transport-agnostic primary constructor: the caller (or a /// transport-specific dial helper) produces the `Connection` — /// via `Connection::from_bidi` (TCP+TLS, WebTransport, SSH /// `direct-tcpip`), a quinn connection, or any other `AsyncRead + /// AsyncWrite` source — and this method takes over: installs - /// channel 0 (`alknet/call`), spawns the demux/mux, and returns + /// channel 0 (`alk/call`), spawns the demux/mux, and returns /// the client. Mirrors the server side's transport-agnostic /// `ChannelsAdapter::handle(Connection)` and /// `CallClient::spawn_dispatch(Connection)`. @@ -111,7 +111,7 @@ the one-way-door API surface. It takes a pre-established `Connection` and takes over channels establishment. The transport is the caller's concern: `Connection::from_bidi(tls_stream, ...)` for TCP+TLS, a quinn `Connection`, a WebTransport `BiStream`, an SSH `direct-tcpip` channel wrapped via -`from_bidi`, a WebSocket carrying `alknet/channels` (the browser path per +`from_bidi`, a WebSocket carrying `alk/channels` (the browser path per ADR-044) — all produce a `Connection` that `from_connection` accepts unchanged. This mirrors the server side's `ChannelsAdapter::handle(Connection)`, which is substrate-agnostic by the same mechanism. diff --git a/docs/architecture/channel-operations.md b/docs/architecture/channel-operations.md index e71c3c5..596c140 100644 --- a/docs/architecture/channel-operations.md +++ b/docs/architecture/channel-operations.md @@ -25,7 +25,7 @@ and/or `channels//pub`: | `channels//sub` | `Sub` | consumer (subscribes) | producer (streams) | server → client | | `channels//pub` | `Pub` | producer (publishes) | consumer (receives) | client → server | -The `channels//...` path segment is the ALPN with the `alknet/` +The `channels//...` path segment is the ALPN with the `alk/` prefix stripped (ADR-047 §"Negative"). An ALPN may register one or both ops; separate op types → separate ACLs. The op spec carries the `channel_open: Option` marker (ADR-047 §2) — the @@ -44,8 +44,8 @@ Request (`call.requested` on channel 0): ``` The `input` is ALPN-specific params (the former `params` field, now the -op's `input_schema`). For `alknet/tty` this is the `NegotiateRequest`; -for `alknet/tunnel` this is the target resource. The channels layer does +op's `input_schema`). For `alk/tty` this is the `NegotiateRequest`; +for `alk/tunnel` this is the target resource. The channels layer does not interpret `input`. Response (`call.responded`): @@ -174,11 +174,11 @@ The responder registers a `StreamingHandler` that emits a "output": { "resources": [ { - "alpn": "alknet/tty", + "alpn": "alk/tty", "backends": ["docker", "local"] }, { - "alpn": "alknet/tunnel", + "alpn": "alk/tunnel", "targets": ["container:*", "service:postgres"] } ] @@ -394,7 +394,7 @@ This is not a flaw — it is the same shape as any per-peer ACL. ### Recursive channels do not bypass the cap -A recursive `alknet/channels`-inside-`alknet/channels` channel runs a +A recursive `alk/channels`-inside-`alk/channels` channel runs a new `ChannelsAdapter` with a new `ChannelManager`. If the same `ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`, the inner channels are counted against the same identity. Recursion @@ -422,8 +422,8 @@ JSON payload; the hub's `CallAdapter` translates these too (rewrites `channel/control` — it's a call operation, translated, not byte-forwarded. -The hub never runs a handler for `alknet/tty`, `alknet/ssh`, or -`alknet/tunnel`. It runs `alknet/channels` (the relay) and `alknet/call` +The hub never runs a handler for `alk/tty`, `alk/ssh`, or +`alk/tunnel`. It runs `alk/channels` (the relay) and `alk/call` (for its own hub-level operations + translation). The `channel_open` marker (ADR-047 §2) is how the hub recognizes a channel-open op during `from_call` discovery (ADR-047 §1, Gap C) — the `from_call` relay @@ -437,7 +437,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). |-----|----------|---------| | [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations | The generic ops; `direction` pinned (amended by ADR-047 — `channel/open` dissolves; `direction` removed) | | [047](decisions/047-openable-alpns-are-operations.md) | Openable ALPNs Are Operations | Per-ALPN open ops; `channel_open` marker; `ChannelCore` wrapper; opener ledger | -| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alknet/call | +| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = alk/call | | [042](decisions/042-hub-relay-translate-not-forward.md) | Hub Relay | Translate channel 0, byte-forward data channels | | [041](decisions/041-per-identity-channel-cap.md) | Per-Identity Channel Cap | 256 per PeerId, enforced via ChannelLifecyclePolicy in channels-call (amended by ADR-047 §7 — opener ledger, every teardown path) | | [035](decisions/035-channels-pure-channel-multiplexing.md) | Pure Channel Multiplexing | No stream_types; handler owns sub-mux | diff --git a/docs/architecture/channels-README.md b/docs/architecture/channels-README.md index fd55fba..66313d4 100644 --- a/docs/architecture/channels-README.md +++ b/docs/architecture/channels-README.md @@ -5,9 +5,9 @@ last_updated: 2026-07-18 # alknet-channels -A multiplexing proxy: a `ProtocolHandler` on `alknet/channels` that +A multiplexing proxy: a `ProtocolHandler` on `alk/channels` that decomposes a single bidirectional transport stream into N logical channels, -each carrying a different ALPN. Channel 0 is pre-negotiated as `alknet/call` +each carrying a different ALPN. Channel 0 is pre-negotiated as `alk/call` (ADR-036); every other channel is opened dynamically via call operations on channel 0 and routed through the same `HandlerRegistry` as top-level connections. The channels layer is a re-framing proxy — it converts between @@ -23,7 +23,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. | [overview.md](overview.md) | draft | Crate purpose, the multiplexing collapse, dependencies, ALPN, transport agnosticism, WASM, relationship to existing crates | | [channels-wire.md](channels-wire.md) | draft | The 8-byte chunk format (`[channel_id:u32 be][length:u32 be][payload]`), the add/strip composition, sentinels, framing disambiguation, wire-level invariants (REQ-CH-01..05) | | [channels-connection.md](channels-connection.md) | draft | `ChannelBidiStreamSource` (implements `BidiStreamSource` — ADR-008/074, as amended by ADR-035), `accept_bi` yields one `BiStream` per channel, recursive composition | -| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alknet/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) | +| [channels-adapter.md](channels-adapter.md) | draft | `ChannelsAdapter` (`ProtocolHandler` on `alk/channels`), `ChannelManager`, demux/mux contracts (REQ-CH-01..04), the two-pump pattern (ADR-078) | | [channel-operations.md](channel-operations.md) | draft | `channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe` — call-protocol operations on channel 0, ACL flow, `direction` semantics, the hub relay contract (ADR-042) | | [channel-client.md](channel-client.md) | draft | `ChannelClient` — the client side of a channels connection; transport-agnostic `from_connection` primary; dial lives in `AlknetClient` (ADR-045); bidirectionality preserved | @@ -33,7 +33,7 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. |-----|-------|-----------| | [071](decisions/071-channels-wire-format.md) | channels Wire Format — 8-Byte Chunk Header | The chunk format; channels layer has no `stream_type` concept (amended by ADR-035); substrate-agnostic; one-way door | | [093](decisions/093-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte | -| [072](decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alknet/call` | Channel 0 = call protocol; no special control plane | +| [072](decisions/072-channel-0-pre-negotiated-call.md) | Channel 0 Is Pre-Negotiated `alk/call` | Channel 0 = call protocol; no special control plane | | [073](decisions/073-channel-lifecycle-operations.md) | Channel Lifecycle Operations on the Call Protocol | `channel/open`/`close`/`control`/`resources/subscribe`; `direction` semantics; subscribe not poll | | [074](decisions/074-channelconnection-bidistreamsource.md) | ChannelConnection — BidiStreamSource over Chunk Reassembly | Per-channel `BidiStreamSource` impl; `accept_bi` yields `BiStream` (amended by ADR-035 — `into_sub_streams` removed) | | [075](decisions/075-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 contracts | @@ -72,9 +72,9 @@ handler owns its sub-stream multiplexing on the `BiStream` it receives. `HandlerRegistry`. See [overview.md](overview.md) and ADR-034 (as amended by ADR-035). -2. **Channel 0 is `alknet/call` pre-negotiated, not a special control +2. **Channel 0 is `alk/call` pre-negotiated, not a special control plane.** The call protocol runs on channel 0 exactly as on a top-level - `alknet/call` connection. Channel lifecycle operations + `alk/call` connection. Channel lifecycle operations (`channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe`) are call operations on channel 0's `OperationRegistry`, gated by the existing `AccessControl::check`. No diff --git a/docs/architecture/channels-adapter.md b/docs/architecture/channels-adapter.md index 0beef46..640ec05 100644 --- a/docs/architecture/channels-adapter.md +++ b/docs/architecture/channels-adapter.md @@ -16,7 +16,7 @@ per channel (not per `(channel_id, stream_type)`). | Component | Role | What it knows | |-----------|------|---------------| -| `ChannelsAdapter` | `ProtocolHandler` on `alknet/channels`; reads 8-byte chunk headers off every bidi stream the transport yields and routes to `ChannelManager`. Substrate-agnostic (ADR-034 §substrate modes, as amended by ADR-035). | The transport stream(s); the `ChannelManager` handle. ALPN-blind. | +| `ChannelsAdapter` | `ProtocolHandler` on `alk/channels`; reads 8-byte chunk headers off every bidi stream the transport yields and routes to `ChannelManager`. Substrate-agnostic (ADR-034 §substrate modes, as amended by ADR-035). | The transport stream(s); the `ChannelManager` handle. ALPN-blind. | | `ChannelManager` | Shared state; holds `channel_id → ChannelState`, `HandlerRegistry`. Constructs `ChannelBidiStreamSource` per channel. What `channel/open` closes over (in `channels-call`). | The channel map; the handler registry for ALPN lookup. ALPN-blind (looks up ALPNs, doesn't parse their protocols). | The split mirrors the TTY crate's `ChunkReader`/`ChunkWriter` + adapter @@ -28,7 +28,7 @@ channel 0 is special only in that it's pre-allocated (by `channels-call`). ```rust #[async_trait] impl ProtocolHandler for ChannelsAdapter { - fn alpn(&self) -> &'static [u8] { b"alknet/channels" } + fn alpn(&self) -> &'static [u8] { b"alk/channels" } async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError> diff --git a/docs/architecture/channels-connection.md b/docs/architecture/channels-connection.md index 47d0777..73d1ce0 100644 --- a/docs/architecture/channels-connection.md +++ b/docs/architecture/channels-connection.md @@ -130,8 +130,8 @@ validated shape and the `StreamBidiStreamSource` yield-once contract A `ChannelBidiStreamSource` is a `BidiStreamSource`, and `Connection::from_source` wraps it. A handler that is itself -`alknet/channels` can open a sub-channels connection on a data channel — -`alknet/channels` inside `alknet/channels`. The outer layer strips its +`alk/channels` can open a sub-channels connection on a data channel — +`alk/channels` inside `alk/channels`. The outer layer strips its 8-byte header; the inner layer parses its own 8-byte header from the payload. Each level is the same shape: `BiStream → accept_bi → N BiStreams`. The recursion is unbounded and uniform at every level. diff --git a/docs/architecture/channels-overview.md b/docs/architecture/channels-overview.md index 0919d50..db1d258 100644 --- a/docs/architecture/channels-overview.md +++ b/docs/architecture/channels-overview.md @@ -8,14 +8,14 @@ last_updated: 2026-07-18 ## What `alknet-channels` is a multiplexing proxy crate. It implements -`ProtocolHandler` for the `alknet/channels` ALPN: it receives one +`ProtocolHandler` for the `alk/channels` ALPN: it receives one bidirectional transport stream, reads 8-byte chunk headers, and routes each chunk's payload to the right logical channel. Each channel is reassembled into a `BiStream` (a concrete `AsyncRead + AsyncWrite` newtype, per ADR-009) and presented to its handler as a `Connection` — the handler doesn't know it's inside a channels connection. -Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Every other channel +Channel 0 is pre-negotiated as `alk/call` (ADR-036). Every other channel is opened dynamically via `channel/open` on channel 0 (ADR-037) and routed through the same `HandlerRegistry` as top-level connections. The channels layer does no protocol work itself — it is a re-framing proxy that converts @@ -45,11 +45,11 @@ and per-ALPN connection management. ### The collapse: one multiplexing model, one connection per leg -With `alknet/channels`, one connection carries everything: +With `alk/channels`, one connection carries everything: ``` Browser ──WebTransport──► Hub ──QUIC──► Spoke - alknet/channels alknet/channels + alk/channels alk/channels ┌─────────────┐ ┌─────────────┐ │ ch0: call │ │ ch0: call │ │ ch1: tty │ relay │ ch1: tty │ @@ -69,7 +69,7 @@ The collapse is at three levels: 2. **One multiplexing model, not three.** Connection-level, stream-level, and sub-stream-level all become channels chunks. 3. **The call protocol orchestrates from inside.** Channel 0 is - `alknet/call` on both legs. The call protocol's `OperationRegistry`, + `alk/call` on both legs. The call protocol's `OperationRegistry`, `AccessControl`, and `forwarded_for` machinery govern channel lifecycle with no new auth. @@ -92,7 +92,7 @@ on the `BiStream` the channels layer gives them (ADR-035). `STREAM_CTRL_OUT` — ADR-052 amended by Phase 7). The channels layer doesn't carry control. - **Recursive composition is literal.** A channel with ALPN - `alknet/channels` runs another channels demux on its `BiStream`. The + `alk/channels` runs another channels demux on its `BiStream`. The outer layer strips its 8-byte header; the inner layer parses its own 8-byte header from the payload. @@ -101,7 +101,7 @@ on the `BiStream` the channels layer gives them (ADR-035). The crate has two internal components (ADR-039): - **`ChannelsAdapter`** — implements `ProtocolHandler` for - `alknet/channels`. Its `handle()` receives one `Connection`, reads 8-byte + `alk/channels`. Its `handle()` receives one `Connection`, reads 8-byte chunk headers, and routes chunks to the `ChannelManager`. The read/demux half. - **`ChannelManager`** — the shared state. Holds `channel_id → @@ -128,7 +128,7 @@ The channels subsystem is split into two modules: call-protocol-blind, transport-blind. This is where the "streams are streams" insight lives. - **`channels` (call coupling)** — channel 0 pre-negotiation as - `alknet/call` (ADR-036), the lifecycle operations (ADR-037) registered + `alk/call` (ADR-036), the lifecycle operations (ADR-037) registered on the call protocol's `OperationRegistry`, `ChannelCore` (ADR-047 §3), and `ChannelClient` (ADR-043). This is where the call-protocol coupling lives, isolated from the pure multiplexer. @@ -155,7 +155,7 @@ A worker is any crate that uses `ChannelClient` to dial. There are no ## ALPN -`alknet/channels` — the ALPN the `ChannelsAdapter` registers on. One ALPN +`alk/channels` — the ALPN the `ChannelsAdapter` registers on. One ALPN per channels connection; the connection carries N logical channels, each with its own ALPN (negotiated via `channel/open`). @@ -166,11 +166,11 @@ stream: | Transport | How | |-----------|-----| -| QUIC bidi stream | `alknet/channels` ALPN on a QUIC connection; one bidi stream carries all channels | -| TCP+TLS | `alknet/channels` ALPN on a TLS connection; the TCP stream carries all channels | -| WebTransport | `alknet/channels` session (deferred per ADR-044; the browser path uses WebSocket carrying `alknet/channels`) | +| QUIC bidi stream | `alk/channels` ALPN on a QUIC connection; one bidi stream carries all channels | +| TCP+TLS | `alk/channels` ALPN on a TLS connection; the TCP stream carries all channels | +| WebTransport | `alk/channels` session (deferred per ADR-044; the browser path uses WebSocket carrying `alk/channels`) | | SSH channel | channels connection riding inside an SSH `direct-tcpip` channel (channels-over-SSH) | -| Another channels connection | recursive composition (channel type `alknet/channels` inside `alknet/channels`) | +| Another channels connection | recursive composition (channel type `alk/channels` inside `alk/channels`) | The same wire format, the same chunk reassembly, the same `Connection` abstraction. The transport is a parameter, not a design constraint. @@ -219,7 +219,7 @@ A protocol crate can support two paths for its wire format: | Path | How it connects | Wire format | |------|----------------|-------------| -| Direct ALPN | `ProtocolHandler` on its own ALPN (e.g. `alknet/tty`), gets a `Connection`, loops `accept_bi` | Own wire format (e.g. TTY's 5-byte header) | +| Direct ALPN | `ProtocolHandler` on its own ALPN (e.g. `alk/tty`), gets a `Connection`, loops `accept_bi` | Own wire format (e.g. TTY's 5-byte header) | | Through channels | Registered via `ChannelCore::register_openable`, gets a `BiStream` per session | Own wire format rides inside channels 8-byte payload | The protocol crate doesn't know which path it's on — it takes a @@ -230,7 +230,7 @@ feature flags) or in the downstream alknet crate. ### alknet-call The call protocol runs on channel 0 exactly as on a top-level -`alknet/call` connection. The `Dispatcher` receives a `Connection` +`alk/call` connection. The `Dispatcher` receives a `Connection` backed by channel-0 chunk reassembly and dispatches operations — it doesn't know it's inside channels. The call protocol's `EventEnvelope` framing (ADR-014) is the channels payload; the channels layer carries @@ -254,7 +254,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). |-----|----------|---------| | [034](decisions/034-channels-wire-format.md) | channels Wire Format | 8-byte chunk header (amended by ADR-035); channels layer has no `stream_type` concept; one-way door | | [035](decisions/035-channels-pure-channel-multiplexing.md) | channels Pure Channel Multiplexing | The umbrella decision: 8-byte header, no `stream_type`, `into_sub_streams` removed, `BiStream`-only, TTY always 5-byte | -| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = `alknet/call` | +| [036](decisions/036-channel-0-pre-negotiated-call.md) | Channel 0 Pre-Negotiated | Channel 0 = `alk/call` | | [037](decisions/037-channel-lifecycle-operations.md) | Channel Lifecycle Operations | `channel/open`/`close`/`control`/`resources/subscribe`; subscribe not poll; `direction` pinned | | [038](decisions/038-channelconnection-bidistreamsource.md) | ChannelConnection | Per-channel `BidiStreamSource`; yield-once `accept_bi` (amended by ADR-035 — `into_sub_streams` removed) | | [039](decisions/039-channelsadapter-and-channelmanager.md) | ChannelsAdapter and ChannelManager | Substrate-agnostic demux loop; REQ-CH-01..04 | diff --git a/docs/architecture/channels-wire.md b/docs/architecture/channels-wire.md index 5665ff0..45f9e03 100644 --- a/docs/architecture/channels-wire.md +++ b/docs/architecture/channels-wire.md @@ -5,7 +5,7 @@ last_updated: 2026-07-18 # channels-wire.md — The 8-Byte Chunk Format -The wire format for `alknet/channels`: an 8-byte chunk header that +The wire format for `alk/channels`: an 8-byte chunk header that multiplexes N logical channels over a single ordered, reliable bidirectional transport stream. ADR-034 (amended by ADR-035) is the decision; this doc specifies the format and the wire-level invariants. @@ -23,7 +23,7 @@ multiplexing on the `BiStream` the channels layer gives it. | field | offset | width | meaning | |-------|--------|-------|---------| -| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). | +| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). | | `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN`. | The payload is opaque to the channels layer. The handler parses its own @@ -70,10 +70,10 @@ stream — the demux drops the chunk and continues. The header is always exactly 8 bytes, so the demux can always resync by reading the next 8-byte header. -## Channel 0 — pre-negotiated `alknet/call` +## Channel 0 — pre-negotiated `alk/call` Channel 0 is not a special "control plane" with its own framing. It is -`alknet/call` pre-negotiated (ADR-036): both sides know `channel_id = 0` +`alk/call` pre-negotiated (ADR-036): both sides know `channel_id = 0` is routed to the `CallAdapter` without an explicit `channel/open` exchange. @@ -230,7 +230,7 @@ a `channel_id` was stripped before it saw the bytes. The composition is uniform — the same shape at every level. This is SSH's model (layered headers, each layer strips its own at its boundary), -applied to channels. A `alknet/channels`-inside-`alknet/channels` +applied to channels. A `alk/channels`-inside-`alk/channels` recursive composition is the outer layer stripping its 8-byte header, the inner layer parsing its own 8-byte header from the payload — same code, same shape, each level. diff --git a/docs/architecture/client-and-adapters.md b/docs/architecture/client-and-adapters.md index 03f6d39..229e097 100644 --- a/docs/architecture/client-and-adapters.md +++ b/docs/architecture/client-and-adapters.md @@ -21,7 +21,7 @@ client-side connection-establishment half and the adapter surface. This document specifies three components, all in `alknet-call`: 1. **`CallClient`** — takes over an established transport `Connection` - on ALPN `alknet/call`, spawns the shared dispatch loop, and produces + on ALPN `alk/call`, spawns the shared dispatch loop, and produces a `CallConnection`. Transport-agnostic (`spawn_dispatch` primary; dial lives in `AlknetClient` per ADR-045); the dispatch loop is shared with the server-side `CallAdapter` @@ -88,7 +88,7 @@ cross-peer dissolved / same-peer stays, DC-4→OQ-26). ### CallClient `CallClient` takes over an established transport `Connection` on ALPN -`alknet/call`, spawns the shared dispatch loop, and produces a +`alk/call`, spawns the shared dispatch loop, and produces a `CallConnection`. The `CallConnection` type is already implemented (`call-protocol.md` §"CallConnection") — it wraps an established `Connection` and holds the Layer 2 imported-ops overlay. `CallClient` @@ -115,7 +115,7 @@ impl CallClient { pub fn new(registry: Arc, idp: Arc) -> Self; /// Transport-agnostic primary constructor. Takes a pre-established - /// `Connection` on ALPN `alknet/call` (any transport — QUIC via + /// `Connection` on ALPN `alk/call` (any transport — QUIC via /// `from_quinn`, TCP+TLS via `from_bidi`, WebTransport, SSH /// `direct-tcpip`, a WebSocket), spawns the shared dispatch loop, /// and returns a live `CallConnection`. Mirrors the server-side @@ -609,7 +609,7 @@ Container service (runs on a vast.ai/docker instance): Connects to hub as a CallClient (outbound connection — runner pattern) Hub (central server): - Runs CallAdapter (server) on alknet/call (already implemented) + Runs CallAdapter (server) on alk/call (already implemented) When the container service connects: hub runs from_call → discovers /container/* via services/list + services/schema registers them as FromCall provenance (leaf, forwarding handlers) in the @@ -634,7 +634,7 @@ Bilateral: the container service ALSO runs from_call against the hub, **Why the container service doesn't need alknet-ssh**: under the call protocol, the container service is a `CallClient` that dials the hub's -`alknet/call` ALPN (over QUIC, TCP+TLS, or any transport) — no SSH in +`alk/call` ALPN (over QUIC, TCP+TLS, or any transport) — no SSH in the loop. SSH port forwarding becomes the *transitional* mechanism for targets that can't run a call-protocol client (the `alknet-ssh` phase-0 findings document this diff --git a/docs/architecture/decisions/001-alpn-protocol-dispatch.md b/docs/architecture/decisions/001-alpn-protocol-dispatch.md index 1b69e12..74caf01 100644 --- a/docs/architecture/decisions/001-alpn-protocol-dispatch.md +++ b/docs/architecture/decisions/001-alpn-protocol-dispatch.md @@ -32,9 +32,9 @@ The endpoint advertises the union of all registered handlers' ALPN strings. When - WASM story is clean: handlers receive byte streams, protocol parsers that operate on bytes compile to WASM **Negative:** -- ALPN is negotiated per-connection, not per-stream — a client that wants to use multiple ALPNs (e.g., SSH and call protocol) opens separate QUIC connections for each. QUIC connections are cheap (multiplexed over the same UDP flow), so this is acceptable, but it means `alknet/call` cannot serve as a multiplexer for other ALPNs within a single connection unless explicitly designed to do so (see ADR-004). +- ALPN is negotiated per-connection, not per-stream — a client that wants to use multiple ALPNs (e.g., SSH and call protocol) opens separate QUIC connections for each. QUIC connections are cheap (multiplexed over the same UDP flow), so this is acceptable, but it means `alk/call` cannot serve as a multiplexer for other ALPNs within a single connection unless explicitly designed to do so (see ADR-004). - All protocols must be registered at endpoint creation time (or use hot-reload via ArcSwap for dynamic addition) -- Custom protocols require reserving ALPN strings — we own the `alknet/` namespace +- Custom protocols require reserving ALPN strings — we own the `alk/` namespace - Debugging requires knowing which ALPN was negotiated (mitigated by logging at the endpoint level) ## References diff --git a/docs/architecture/decisions/002-protocol-handler-trait.md b/docs/architecture/decisions/002-protocol-handler-trait.md index fc15675..e7e11e8 100644 --- a/docs/architecture/decisions/002-protocol-handler-trait.md +++ b/docs/architecture/decisions/002-protocol-handler-trait.md @@ -24,7 +24,7 @@ A single `ProtocolHandler` trait replaces both `StreamInterface` and `MessageInt ```rust #[async_trait] pub trait ProtocolHandler: Send + Sync + 'static { - /// The ALPN string this handler claims (e.g. b"alknet/ssh") + /// The ALPN string this handler claims (e.g. b"alk/ssh") fn alpn(&self) -> &'static [u8]; /// Handle an incoming connection (revised by ADR-005 to receive diff --git a/docs/architecture/decisions/004-alpn-convention-and-connection-model.md b/docs/architecture/decisions/004-alpn-convention-and-connection-model.md index c6c73ab..29f855f 100644 --- a/docs/architecture/decisions/004-alpn-convention-and-connection-model.md +++ b/docs/architecture/decisions/004-alpn-convention-and-connection-model.md @@ -18,25 +18,25 @@ The iroh reference project uses the same model: each `ProtocolHandler` claims an ### ALPN String Convention -Custom ALPN strings use the `alknet/` prefix: +Custom ALPN strings use the `alk/` prefix: | ALPN | Handler | Type | |------|---------|------| -| `alknet/ssh` | SshAdapter | Custom | -| `alknet/call` | CallAdapter | Custom | -| `alknet/git` | GitAdapter | Custom | -| `alknet/sftp` | SftpAdapter | Custom | -| `alknet/msg` | MessageAdapter | Custom | -| `alknet/http` | HttpAdapter | Custom | -| `alknet/dns` | DnsAdapter | Custom | -| `h3` | WebTransport → alknet/http | Standard (IANA) | -| `h2` | HTTP/2 → alknet/http | Standard (IANA) | -| `http/1.1` | HTTP/1.1 → alknet/http | Standard (IANA) | +| `alk/ssh` | SshAdapter | Custom | +| `alk/call` | CallAdapter | Custom | +| `alk/git` | GitAdapter | Custom | +| `alk/sftp` | SftpAdapter | Custom | +| `alk/msg` | MessageAdapter | Custom | +| `alk/http` | HttpAdapter | Custom | +| `alk/dns` | DnsAdapter | Custom | +| `h3` | WebTransport → alk/http | Standard (IANA) | +| `h2` | HTTP/2 → alk/http | Standard (IANA) | +| `http/1.1` | HTTP/1.1 → alk/http | Standard (IANA) | Rules: -- Custom ALPNs use the format `alknet/` — lowercase, no version number +- Custom ALPNs use the format `alk/` — lowercase, no version number - Standard ALPNs (`h2`, `http/1.1`, `h3`) use their IANA-registered strings and are handled by the HTTP adapter -- No version numbers in ALPN strings initially. If protocol compatibility breaks, a new ALPN string is registered (e.g., `alknet/call/v2`). This is simpler than version negotiation and follows the QUIC convention that ALPN mismatch means connection failure +- No version numbers in ALPN strings initially. If protocol compatibility breaks, a new ALPN string is registered (e.g., `alk/call/v2`). This is simpler than version negotiation and follows the QUIC convention that ALPN mismatch means connection failure - ALPN strings are compile-time constants in each handler's `alpn()` method — no runtime registration of new ALPN strings ### Connection Model @@ -44,23 +44,23 @@ Rules: **One ALPN per connection.** A client that wants to use multiple ALPNs opens one QUIC connection per ALPN. All connections from the same client are multiplexed over the same UDP flow (QUIC's natural connection multiplexing), so the overhead is minimal. This means: -- `alknet/call` is a distinct ALPN with its own connection — not a multiplexer for other ALPNs +- `alk/call` is a distinct ALPN with its own connection — not a multiplexer for other ALPNs - A client interacting with both SSH and call protocol has two QUIC connections -- Within an `alknet/call` connection, multiple QUIC streams can carry independent operations (see ADR-013) +- Within an `alk/call` connection, multiple QUIC streams can carry independent operations (see ADR-013) - The endpoint logs the negotiated ALPN for each connection for observability ## Consequences **Positive:** - Simple model: one connection, one protocol — no multiplexing layer needed inside a connection -- ALPN strings are predictable and discoverable — `alknet/` is a clear namespace +- ALPN strings are predictable and discoverable — `alk/` is a clear namespace - No version negotiation complexity — incompatible versions get new ALPN strings - QUIC connection multiplexing means multiple ALPN connections share the same UDP flow **Negative:** - Multiple ALPNs require multiple connections — a full-featured client might have 3-5 QUIC connections open simultaneously - No version negotiation — an incompatible change requires a new ALPN string, which means old and new clients can coexist only if the server registers both ALPNs -- The `alknet/` namespace is owned by this project — third-party extensions need their own prefix +- The `alk/` namespace is owned by this project — third-party extensions need their own prefix ## References @@ -68,4 +68,16 @@ This means: - ADR-002: ProtocolHandler trait - OQ-03: ALPN string naming convention (resolved by this ADR) - OQ-06: Server-side ALPN vs client-side ALPN (resolved by this ADR) -- iroh reference: `docs/research/references/iroh/` \ No newline at end of file +- 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. \ No newline at end of file diff --git a/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md b/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md index 745f1b7..4c0c084 100644 --- a/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md +++ b/docs/architecture/decisions/007-connection-from-stream-generic-single-stream.md @@ -186,7 +186,7 @@ WebTransport stream). - `alknet-ssh` is unblocked: the SSH handler wraps each russh channel via `from_stream` and dispatches by channel-type (treated as the ALPN string) through `HandlerRegistry`. One SSH connection carries heterogeneous - channels (`alknet/tty`, `alknet/call`, `h2`, ...) — a multiplexing power + channels (`alk/tty`, `alk/call`, `h2`, ...) — a multiplexing power QUIC's per-connection ALPN doesn't give natively. - WebTransport stream dispatch is unblocked: the WT handler wraps each WT stream via `from_stream` and dispatches through `HandlerRegistry` (the diff --git a/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md b/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md index ae3d582..638a7a1 100644 --- a/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md +++ b/docs/architecture/decisions/009-bistream-as-the-handler-leaf.md @@ -345,7 +345,7 @@ pump halves, the same idiom it would use over `TcpStream`. ### `BiStream` over WebSocket enables "VPN-like without being a VPN" in v1 The `webtransport.md` spec describes the "VPN-like without being a VPN" -path: a browser opens a WebTransport session to `/alknet/ssh`, the h3 +path: a browser opens a WebTransport session to `/alk/ssh`, the h3 handler hands each bidi stream to `SshAdapter::handle` as a `Connection`, the browser's WASM SSH parser speaks SSH over the stream. WebTransport is deferred per ADR-044. diff --git a/docs/architecture/decisions/012-connectioncredentials-decouple-dial-from-call.md b/docs/architecture/decisions/012-connectioncredentials-decouple-dial-from-call.md index 2827bbb..6d7bdeb 100644 --- a/docs/architecture/decisions/012-connectioncredentials-decouple-dial-from-call.md +++ b/docs/architecture/decisions/012-connectioncredentials-decouple-dial-from-call.md @@ -52,7 +52,7 @@ identity is unavailable: - **Browsers** — no raw-key support, no client cert the hub can fingerprint; the browser authenticates via a bearer token over HTTP/WebSocket, and the hub's `IdentityProvider` resolves it. -- **`alknet/register`** — a native worker that hasn't been enrolled dials +- **`alk/register`** — a native worker that hasn't been enrolled dials in with no prior peer relationship; a registration token (or open registration) establishes identity, not a TLS fingerprint. @@ -106,7 +106,7 @@ dimensions every dial consumes: /// (fingerprint, driving verifier selection per ADR-034). /// /// This is NOT the call-protocol credential bundle. The call-protocol -/// `auth_token` (hub-correlated bearer for browsers / `alknet/register`) +/// `auth_token` (hub-correlated bearer for browsers / `alk/register`) /// is a per-request field on `call.requested` payloads, not a /// transport credential. It stays in the call-protocol layer. pub struct ConnectionCredentials { @@ -235,7 +235,7 @@ credential bundle is needed):** bearer token to an `Identity` via `IdentityProvider::resolve_from_token` at the HTTP boundary. The call protocol receives the `Identity`, not the token. -2. **Registration** (`alknet/register` native ALPN, `/register` HTTP +2. **Registration** (`alk/register` native ALPN, `/register` HTTP endpoint) — a client not yet associated with a hub presents a one-time registration token; the hub creates a `PeerEntry` (a new identity based on the fingerprint). Outbound, the vault manages the diff --git a/docs/architecture/decisions/013-irpc-as-call-protocol-foundation.md b/docs/architecture/decisions/013-irpc-as-call-protocol-foundation.md index 3536b7e..95515f1 100644 --- a/docs/architecture/decisions/013-irpc-as-call-protocol-foundation.md +++ b/docs/architecture/decisions/013-irpc-as-call-protocol-foundation.md @@ -33,7 +33,7 @@ The call protocol is derived from a TypeScript implementation (`@alkdev/operatio ## Decision -alknet-call uses irpc as its foundation. The `CallAdapter` implements `ProtocolHandler` on ALPN `alknet/call` and delegates to irpc's operation registry, framing, and dispatch. +alknet-call uses irpc as its foundation. The `CallAdapter` implements `ProtocolHandler` on ALPN `alk/call` and delegates to irpc's operation registry, framing, and dispatch. irpc is not replaced or wrapped in an abstraction layer — it IS the call protocol's core. The relationship is: - irpc provides: operation registry, schema discovery, frame encoding/decoding, request/response routing, streaming diff --git a/docs/architecture/decisions/015-call-protocol-stream-model.md b/docs/architecture/decisions/015-call-protocol-stream-model.md index b0b47e6..b7b4003 100644 --- a/docs/architecture/decisions/015-call-protocol-stream-model.md +++ b/docs/architecture/decisions/015-call-protocol-stream-model.md @@ -6,7 +6,7 @@ Accepted ## Context -The call protocol (alknet-call) operates on a QUIC connection with ALPN `alknet/call`. Within that connection, QUIC provides bidirectional streams. The question is how the call protocol uses those streams and how it correlates requests with responses — especially when both sides can initiate calls. +The call protocol (alknet-call) operates on a QUIC connection with ALPN `alk/call`. Within that connection, QUIC provides bidirectional streams. The question is how the call protocol uses those streams and how it correlates requests with responses — especially when both sides can initiate calls. The reference implementation used `EventEnvelope` framing with a `PendingRequestMap` that correlates `call.requested` events to `call.responded` events by request ID, regardless of which stream carries them. This works well but the relationship between streams and operations was underspecified. @@ -16,7 +16,7 @@ OQ-07 asked: "What is the scope of the call protocol within a connection? Should The call protocol uses **bidirectional QUIC streams with EventEnvelope framing and ID-based correlation**. The protocol does not prescribe a stream usage pattern — it works with any arrangement: -1. **EventEnvelope on every stream** — every bidirectional stream opened on the `alknet/call` connection carries length-prefixed JSON `EventEnvelope` messages. The five event types (`call.requested`, `call.responded`, `call.completed`, `call.aborted`, `call.error`) are the protocol primitives. +1. **EventEnvelope on every stream** — every bidirectional stream opened on the `alk/call` connection carries length-prefixed JSON `EventEnvelope` messages. The five event types (`call.requested`, `call.responded`, `call.completed`, `call.aborted`, `call.error`) are the protocol primitives. 2. **PendingRequestMap correlates by ID, not by stream** — the `id` field in `EventEnvelope` correlates requests with responses. A response on stream 5 can fulfill a request sent on stream 3. The PendingRequestMap is keyed by request ID. @@ -30,7 +30,7 @@ The call protocol uses **bidirectional QUIC streams with EventEnvelope framing a 5. **Stream usage is the client's choice** — a client may open one stream per operation, one stream for all operations, or any mix. The protocol is stream-agnostic. The server accepts streams and processes EventEnvelopes regardless of which stream they arrive on. -This resolves OQ-07: the call protocol's scope within a connection is the full operation registry. One `alknet/call` connection gives access to all operations (call, subscribe, batch, schema). QUIC's built-in stream multiplexing handles concurrency — the protocol doesn't need to impose additional multiplexing. +This resolves OQ-07: the call protocol's scope within a connection is the full operation registry. One `alk/call` connection gives access to all operations (call, subscribe, batch, schema). QUIC's built-in stream multiplexing handles concurrency — the protocol doesn't need to impose additional multiplexing. ## Consequences diff --git a/docs/architecture/decisions/019-operation-registry-layering.md b/docs/architecture/decisions/019-operation-registry-layering.md index 1462980..13a2525 100644 --- a/docs/architecture/decisions/019-operation-registry-layering.md +++ b/docs/architecture/decisions/019-operation-registry-layering.md @@ -17,9 +17,9 @@ as sharing one immutability argument: 2. **The call protocol's `OperationRegistry`** (operation name → `HandlerRegistration`). This lives *inside* the `CallAdapter`, which is one - `ProtocolHandler` behind the single ALPN `alknet/call`. Adding an operation + `ProtocolHandler` behind the single ALPN `alk/call`. Adding an operation to the `OperationRegistry` does **not** touch the TLS `ServerConfig` — the - ALPN is already `alknet/call`, registered once at startup. + ALPN is already `alk/call`, registered once at startup. `operation-registry.md` stated the operation registry "is immutable after construction… consistent with OQ-04 and ADR-010." That inheritance was by diff --git a/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md b/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md index a1c27bb..886cbc8 100644 --- a/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md +++ b/docs/architecture/decisions/022-call-protocol-client-and-adapter-contract.md @@ -45,7 +45,7 @@ architecture. ### 1. `CallClient` opens connections and shares the dispatch loop -`CallClient` opens a QUIC connection to a remote node with ALPN `alknet/call`. +`CallClient` opens a QUIC connection to a remote node with ALPN `alk/call`. Once connected, the connection is symmetric — both sides can send and receive `call.requested`. The `CallClient` is not just a caller; it is also a callee. It has its own operation registry to dispatch incoming calls from the remote diff --git a/docs/architecture/decisions/023-callclient-peer-scoped-registry-filtering.md b/docs/architecture/decisions/023-callclient-peer-scoped-registry-filtering.md index 9bf35b6..ecfb4cf 100644 --- a/docs/architecture/decisions/023-callclient-peer-scoped-registry-filtering.md +++ b/docs/architecture/decisions/023-callclient-peer-scoped-registry-filtering.md @@ -20,7 +20,7 @@ identified the gap. ## Context ADR-022 §1 established that a `CallClient` — which opens an outbound -`alknet/call` connection — "has its own operation registry to dispatch incoming +`alk/call` connection — "has its own operation registry to dispatch incoming calls from the remote side." The ADR left the *registry scope* as an explicit two-way door in its Consequences: diff --git a/docs/architecture/decisions/031-crate-decomposition.md b/docs/architecture/decisions/031-crate-decomposition.md index 9a7c8f1..af79170 100644 --- a/docs/architecture/decisions/031-crate-decomposition.md +++ b/docs/architecture/decisions/031-crate-decomposition.md @@ -86,7 +86,7 @@ crate depends on another handler crate" rule were written before `HandlerRegistration`, and `OperationAdapter` trait) was specced. **Clarification:** `alknet-call` is both a handler crate (it implements -`ProtocolHandler` on ALPN `alknet/call`) *and* the protocol-foundation +`ProtocolHandler` on ALPN `alk/call`) *and* the protocol-foundation crate that `alknet-agent`, `alknet-napi`, and `alknet-http` consume for the operation registry, adapter contract, and call client. The "no handler crate depends on another handler crate" rule applies to peer diff --git a/docs/architecture/decisions/034-channels-wire-format.md b/docs/architecture/decisions/034-channels-wire-format.md index 9eb0a45..7da4abb 100644 --- a/docs/architecture/decisions/034-channels-wire-format.md +++ b/docs/architecture/decisions/034-channels-wire-format.md @@ -38,7 +38,7 @@ rationale and the cross-ADR impacts. ## Context `alknet-channels` is a multiplexing proxy: a `ProtocolHandler` on -`alknet/channels` that carries N logical channels, each with a different +`alk/channels` that carries N logical channels, each with a different ALPN, over transport stream(s). The wire format is the substrate that makes this work. @@ -105,7 +105,7 @@ preserving the framing-disambiguation soundness property. | field | width | meaning | |-------|-------|---------| -| `channel_id` | u32 BE | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open`. | +| `channel_id` | u32 BE | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open`. | | `stream_type` | u8 | The unidirectional sub-stream within the channel. See "Stream types" below. | | `length` | u32 BE | The payload length in bytes. 0 = EOF sentinel (same convention as TTY — ADR-052 §Sentinels). | @@ -163,10 +163,10 @@ stream_types: the channels layer routes bytes, the handler interprets them. | ALPN | Active stream_types | Why | |------|---------------------|-----| -| `alknet/call` (channel 0) | [0, 1] | call frames bidirectional via 0=in, 1=out | -| `alknet/tty` | [0, 1, 2, 3, 4] | data in/out/err + control in/out | -| `alknet/tunnel` | [0, 1] | data in/out only (no channels-layer control needed) | -| `alknet/ssh` | [0, 1] | SSH multiplexes internally, including its own control | +| `alk/call` (channel 0) | [0, 1] | call frames bidirectional via 0=in, 1=out | +| `alk/tty` | [0, 1, 2, 3, 4] | data in/out/err + control in/out | +| `alk/tunnel` | [0, 1] | data in/out only (no channels-layer control needed) | +| `alk/ssh` | [0, 1] | SSH multiplexes internally, including its own control | The active set is declared at `channel/open` time (ADR-037 `stream_types` field) and fixed for the channel's lifetime. A tunnel that wants keepalive diff --git a/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md b/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md index d4bc875..68b87c2 100644 --- a/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md +++ b/docs/architecture/decisions/035-channels-pure-channel-multiplexing.md @@ -80,7 +80,7 @@ on the `BiStream` the channels layer gives them. doesn't carry control. The "control isn't actually bidirectional" flaw is fixed at the TTY layer, not the channels layer. - **Recursive composition is literal.** A channel with ALPN - `alknet/channels` runs another channels demux on its `BiStream`. The + `alk/channels` runs another channels demux on its `BiStream`. The outer layer strips its 8-byte header; the inner layer parses its own 8-byte header from the payload. Each level is the same shape — `BiStream → accept_bi → N BiStreams`. @@ -122,7 +122,7 @@ a `channel_id` was stripped before it saw the bytes. The composition is uniform — the same shape at every level. This is SSH's model (layered headers, each layer strips its own at its boundary), -applied to channels. A `alknet/channels`-inside-`alknet/channels` +applied to channels. A `alk/channels`-inside-`alk/channels` recursive composition is the outer layer stripping its 8-byte header, the inner layer parsing its own 8-byte header from the payload — same code, same shape, each level. @@ -193,7 +193,7 @@ handler's framing, carried transparently. | field | offset | width | meaning | |-------|--------|-------|---------| -| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alknet/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). | +| `channel_id` | 0 | 4 (BE) | The logical channel this chunk belongs to. Channel 0 is pre-negotiated as `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). | | `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN` (16 MiB, matching TTY's cap — ADR-052 §5). | The `stream_type` byte is **removed** from the channels header. The @@ -231,8 +231,8 @@ ADR-077's two-mode TTY design (direct vs inside-channels) is reversed. TTY's 5-byte format (`[stream_type:u8][length:u32][payload]`, ADR-052) is TTY's internal format, used in *both* direct mode and inside-channels mode. The two modes differ only in *where the `BiStream` comes from* -(a top-level `alknet/tty` connection vs a `channel/open` with ALPN -`alknet/tty`), not in *how TTY parses it*. The same `wire.rs` code runs +(a top-level `alk/tty` connection vs a `channel/open` with ALPN +`alk/tty`), not in *how TTY parses it*. The same `wire.rs` code runs in both modes. When TTY is inside channels, the channels layer strips its 8-byte header @@ -272,7 +272,7 @@ TTY inside channels: ``` The composition is uniform — the same shape at every level. A -`alknet/channels`-inside-`alknet/channels` recursive composition is the +`alk/channels`-inside-`alk/channels` recursive composition is the outer layer stripping its 8-byte header, the inner layer parsing its own 8-byte header from the payload — same code, same shape, each level. @@ -284,7 +284,7 @@ outer layer stripping its 8-byte header, the inner layer parsing its own transport leaf; this ADR settles the multiplexing layer above it). - **`ProtocolHandler` trait shape** (ADR-002) — unchanged. Handlers receive a `Connection` and call `accept_bi()`. -- **Channel 0 pre-negotiated as `alknet/call`** (ADR-036) — unchanged. +- **Channel 0 pre-negotiated as `alk/call`** (ADR-036) — unchanged. Channel 0's chunks have `channel_id = 0` in the 8-byte header. The call protocol's `EventEnvelope` framing is the payload; the channels layer carries it transparently. @@ -345,7 +345,7 @@ outer layer stripping its 8-byte header, the inner layer parsing its own byte it doesn't use; no handler needs a second accessor (`into_sub_streams`) to reach its sub-streams. The channels layer's API surface is `accept_bi -> BiStream`, period. -- **Recursive composition is literal.** A `alknet/channels` channel runs +- **Recursive composition is literal.** A `alk/channels` channel runs another channels demux on its `BiStream`. The outer layer strips its 8-byte header; the inner layer parses its own 8-byte header from the payload. Same code, same shape, each level. This is a property, not a @@ -452,7 +452,7 @@ breaking the wire format or the handler contract. public stream constructor) - ADR-008: `BidiStreamSource` trait (the extension point `ChannelBidiStreamSource` implements; `accept_bi` yields `BiStream`) -- ADR-036: channel 0 pre-negotiated `alknet/call` (unchanged — channel 0's +- ADR-036: channel 0 pre-negotiated `alk/call` (unchanged — channel 0's chunks have `channel_id = 0` in the 8-byte header; the call protocol's framing is the payload) - ADR-037: channel lifecycle operations (amended — `stream_types` field diff --git a/docs/architecture/decisions/036-channel-0-pre-negotiated-call.md b/docs/architecture/decisions/036-channel-0-pre-negotiated-call.md index 614630d..5abe7da 100644 --- a/docs/architecture/decisions/036-channel-0-pre-negotiated-call.md +++ b/docs/architecture/decisions/036-channel-0-pre-negotiated-call.md @@ -1,4 +1,4 @@ -# ADR-036: Channel 0 Is Pre-Negotiated `alknet/call` +# ADR-036: Channel 0 Is Pre-Negotiated `alk/call` ## Status @@ -20,7 +20,7 @@ not a channels-layer concern; the channels layer routes by `channel_id` only and yields a `BiStream` to the `CallAdapter`. The `CallAdapter`'s `accept_bi()` returns one `BiStream` (per ADR-009); the call protocol reads/writes `EventEnvelope` frames on it, exactly as on a top-level -`alknet/call` connection. +`alk/call` connection. The body below describes the **original** (with `stream_types`) shape; the amendment above is the operative decision. See ADR-035 for the @@ -81,7 +81,7 @@ accept side (the dispatch loop has no stream to accept). accept-side black hole: channel 0's `Connection` is actually driven by a call dispatch loop. -5. **Top-level `alknet/call` connections are unchanged.** +5. **Top-level `alk/call` connections are unchanged.** `CallConnection::new` and `Dispatcher::run_loop` keep the stream-per-request model. Single-stream mode is only for channel 0 (and any future single-stream substrate that multiplexes call @@ -115,13 +115,13 @@ A channels connection carries N logical channels. One of them must carry the call protocol — the JSON-RPC layer that orchestrates channel lifecycle (`channel/open`, `channel/close`, `channel/control`, `channel/resources`). The question is how channel 0 relates to the call protocol: is it a special -"control plane" with its own framing, or is it just `alknet/call` pre- +"control plane" with its own framing, or is it just `alk/call` pre- negotiated? The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md` -§DP-2) recommends channel 0 is `alknet/call` pre-negotiated — no special +§DP-2) recommends channel 0 is `alk/call` pre-negotiated — no special framing, no separate control-plane wire format. The call protocol runs on -channel 0 exactly as it runs on a top-level `alknet/call` QUIC connection. +channel 0 exactly as it runs on a top-level `alk/call` QUIC connection. This matters because the alternative (a special control plane) would mean the channels layer has its own JSON protocol for channel lifecycle, parallel @@ -133,11 +133,11 @@ collapse. ## Decision -**Channel 0 is `alknet/call`, pre-negotiated.** Both sides of a channels +**Channel 0 is `alk/call`, pre-negotiated.** Both sides of a channels connection know that `channel_id = 0` is routed to the `CallAdapter` without an explicit `channel/open` exchange. The `CallAdapter` receives a `Connection` backed by channel-0 chunk reassembly and dispatches operations -exactly as it does on a top-level `alknet/call` connection. +exactly as it does on a top-level `alk/call` connection. ### What this means concretely @@ -163,7 +163,7 @@ exactly as it does on a top-level `alknet/call` connection. `ChannelsAdapter` constructs channel 0's reassembly buffers, wraps them as a `Connection` (via `Connection::from_source` with a `ChannelBidiStreamSource` — ADR-008/074), and hands that `Connection` to - the `CallAdapter` — exactly as if `alknet/call` had been the top-level + the `CallAdapter` — exactly as if `alk/call` had been the top-level ALPN. The `CallAdapter` is looked up in the same `HandlerRegistry` as every other ALPN. @@ -217,7 +217,7 @@ sub-streams. ## Door type -**One-way.** Channel 0's role as `alknet/call` pre-negotiated is a wire- +**One-way.** Channel 0's role as `alk/call` pre-negotiated is a wire- format and protocol-structure commitment. Changing it after deployments exist (e.g., to a special control plane) requires a version migration and re-architecting the channel lifecycle operations. The reservation of diff --git a/docs/architecture/decisions/037-channel-lifecycle-operations.md b/docs/architecture/decisions/037-channel-lifecycle-operations.md index 82b17b8..116a544 100644 --- a/docs/architecture/decisions/037-channel-lifecycle-operations.md +++ b/docs/architecture/decisions/037-channel-lifecycle-operations.md @@ -107,7 +107,7 @@ Request (`call.requested` on channel 0): { "operation": "channel/open", "input": { - "alpn": "alknet/tty", + "alpn": "alk/tty", "stream_types": [0, 1, 2, 3, 4], "params": { "backend": "docker", "cmd": ["bash"], "container": "abc123" }, "direction": "initiator-to-responder" @@ -119,7 +119,7 @@ Request (`call.requested` on channel 0): |-------|------|---------| | `alpn` | string | The ALPN the channel will carry. The responder looks this up in its `HandlerRegistry`. | | `stream_types` | `[u8]` | Which sub-stream types this channel will use. E.g. `[0,1,2,3,4]` for TTY (data in/out/err + control in/out), `[0,1]` for a tunnel, `[0,1]` for channel 0 (call frames). See ADR-034 §stream_type decomposition. | -| `params` | object | ALPN-specific parameters. For `alknet/tty` this is the `NegotiateRequest`. For `alknet/tunnel` this is the target resource. The channels layer does not interpret `params`. | +| `params` | object | ALPN-specific parameters. For `alk/tty` this is the `NegotiateRequest`. For `alk/tunnel` this is the target resource. The channels layer does not interpret `params`. | | `direction` | string | `initiator-to-responder` or `responder-to-initiator`. See "Direction semantics" below. | Response (`call.responded`): @@ -223,12 +223,12 @@ The responder registers a `StreamingHandler` that emits a "output": { "resources": [ { - "alpn": "alknet/tty", + "alpn": "alk/tty", "backends": ["docker", "local"], "access": { "required_scopes": ["tty:open"] } }, { - "alpn": "alknet/tunnel", + "alpn": "alk/tunnel", "targets": ["container:*", "service:postgres"], "access": { "required_scopes_any": ["tunnel:open", "admin"] } } @@ -355,7 +355,7 @@ the underlying one-way commitment. - ADR-035: channels pure channel multiplexing (amends this ADR — `stream_types` field removed from `channel/open`; `stream_type` field removed from `channel/control`; handler owns sub-stream multiplexing) -- ADR-036: channel 0 is pre-negotiated `alknet/call` +- ADR-036: channel 0 is pre-negotiated `alk/call` - ADR-021: StreamingHandler for subscriptions (the machinery `channel/resources/subscribe` uses — implemented and tested) - ADR-020: abort cascade (subscription cancellation) diff --git a/docs/architecture/decisions/038-channelconnection-bidistreamsource.md b/docs/architecture/decisions/038-channelconnection-bidistreamsource.md index ca9ed28..88ca5b7 100644 --- a/docs/architecture/decisions/038-channelconnection-bidistreamsource.md +++ b/docs/architecture/decisions/038-channelconnection-bidistreamsource.md @@ -172,9 +172,9 @@ that both paths are available and the handler crate chooses. ### Recursive composition A `ChannelBidiStreamSource` is a `BidiStreamSource`, and `Connection:: -from_source` wraps it. A handler that is itself `alknet/channels` can open a +from_source` wraps it. A handler that is itself `alk/channels` can open a sub-channels connection on a data channel. This is recursive composition: -`alknet/channels` inside `alknet/channels`. It is allowed (the `Connection` +`alk/channels` inside `alk/channels`. It is allowed (the `Connection` abstraction permits it) but not a feature designed for — the primary use case is one level of multiplexing. Recursive composition is a natural consequence of the abstraction, not a goal. diff --git a/docs/architecture/decisions/039-channelsadapter-and-channelmanager.md b/docs/architecture/decisions/039-channelsadapter-and-channelmanager.md index 87831cd..6078cd1 100644 --- a/docs/architecture/decisions/039-channelsadapter-and-channelmanager.md +++ b/docs/architecture/decisions/039-channelsadapter-and-channelmanager.md @@ -30,7 +30,7 @@ The channels crate has two internal components, split by responsibility Connection Internals): 1. **`ChannelsAdapter`** — implements `ProtocolHandler` for - `alknet/channels`. Its `handle()` receives one `Connection` (the + `alk/channels`. Its `handle()` receives one `Connection` (the transport), reads 9-byte chunk headers, and routes each chunk. It is the read/demux half. @@ -53,12 +53,12 @@ contracts. ```rust #[async_trait] impl ProtocolHandler for ChannelsAdapter { - fn alpn(&self) -> &'static [u8] { b"alknet/channels" } + fn alpn(&self) -> &'static [u8] { b"alk/channels" } async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError> { - // 1. Channel 0 is pre-negotiated as alknet/call (ADR-036). + // 1. Channel 0 is pre-negotiated as alk/call (ADR-036). // The first bidi stream the transport yields is channel 0. let (send, recv) = connection.accept_bi().await?; self.manager.preinstall_channel_0(send, recv, auth).await?; @@ -79,7 +79,7 @@ The `preinstall_channel_0` step constructs the reassembly buffers for `channel_id = 0` using stream_types [0, 1] (ADR-036), wraps them as a `Connection` (via `Connection::from_source` with a `ChannelBidiStreamSource` — ADR-038), and hands that `Connection` to the `CallAdapter` — exactly as if -`alknet/call` had been the top-level ALPN. The `CallAdapter` is looked up in +`alk/call` had been the top-level ALPN. The `CallAdapter` is looked up in the same `HandlerRegistry` as every other ALPN. `run_demux_loop` continues accepting bidi streams from the transport. For diff --git a/docs/architecture/decisions/041-per-identity-channel-cap.md b/docs/architecture/decisions/041-per-identity-channel-cap.md index 5fa8ddf..0160577 100644 --- a/docs/architecture/decisions/041-per-identity-channel-cap.md +++ b/docs/architecture/decisions/041-per-identity-channel-cap.md @@ -212,7 +212,7 @@ per-peer policy). ### 6. Recursive channels do not bypass the cap -A recursive `alknet/channels`-inside-`alknet/channels` channel runs a +A recursive `alk/channels`-inside-`alk/channels` channel runs a new `ChannelsAdapter` with a new `ChannelManager`. If the same `ChannelLifecyclePolicy` is wired into the inner `ChannelOperations`, the inner channels are counted against the same identity. If a diff --git a/docs/architecture/decisions/042-hub-relay-translate-not-forward.md b/docs/architecture/decisions/042-hub-relay-translate-not-forward.md index 561e5f4..59d3574 100644 --- a/docs/architecture/decisions/042-hub-relay-translate-not-forward.md +++ b/docs/architecture/decisions/042-hub-relay-translate-not-forward.md @@ -91,8 +91,8 @@ spoke leg with `spoke_id` in the payload. The relay does not touch | Spoke leg | `ChannelsAdapter` + `CallAdapter` (same) | | Relay | Per-channel byte-forward tasks with `channel_id` rewrite | -The hub never runs a handler for `alknet/tty`, `alknet/ssh`, or -`alknet/tunnel`. It runs `alknet/channels` (the relay) and `alknet/call` +The hub never runs a handler for `alk/tty`, `alk/ssh`, or +`alk/tunnel`. It runs `alk/channels` (the relay) and `alk/call` (for its own hub-level operations + translation). The endpoints at each end do the protocol work. @@ -102,7 +102,7 @@ do the protocol work. registry / ownership store (ADR-011), queried via call operations on channel 0. Channels doesn't touch this. - **ACL at the hub:** does this browser's identity have `channel:open` scope - for `alknet/ssh` to `spoke-X`? `AccessControl::check` on `channel/open`, + for `alk/ssh` to `spoke-X`? `AccessControl::check` on `channel/open`, run by the hub's `CallAdapter` before it forwards. Channels doesn't touch this. - **Relay lifecycle:** when a browser disconnects, the hub tears down the @@ -168,7 +168,7 @@ auth path. The `channel_id` mapping strategy (`HashMap` per pair) is two-way - ADR-034: outgoing-only X.509 and the three peer roles (browser identity resolution) - ADR-011: dynamic resource ownership (the ownership store the hub queries) -- ADR-036: channel 0 is pre-negotiated `alknet/call` (what the hub +- ADR-036: channel 0 is pre-negotiated `alk/call` (what the hub terminates on each leg) - ADR-037: channel lifecycle operations (what the hub translates) - ADR-039: ChannelsAdapter and ChannelManager (the interface the relay uses) diff --git a/docs/architecture/decisions/043-channelclient.md b/docs/architecture/decisions/043-channelclient.md index 55d3685..4b8a319 100644 --- a/docs/architecture/decisions/043-channelclient.md +++ b/docs/architecture/decisions/043-channelclient.md @@ -62,7 +62,7 @@ surface: primary constructor and the one-way-door API. Takes a pre-established `Connection` (produced by any transport — TCP+TLS via `from_bidi`, WebTransport `BiStream`, SSH `direct-tcpip`, a quinn connection, a - WebSocket carrying `alknet/channels` per ADR-044), installs channel 0, + WebSocket carrying `alk/channels` per ADR-044), installs channel 0, spawns the demux/mux, returns the client. Mirrors the server-side `ChannelsAdapter::handle(Connection)` (substrate-agnostic) and the existing `CallClient::spawn_dispatch(Connection)` pattern. @@ -130,7 +130,7 @@ impl ChannelClient { /// Transport-agnostic primary constructor. Takes a pre-established /// `Connection` (any transport — TCP+TLS via `from_bidi`, /// WebTransport BiStream, SSH direct-tcpip, a quinn connection, a - /// WebSocket per ADR-044), installs channel 0 (alknet/call), spawns + /// WebSocket per ADR-044), installs channel 0 (alk/call), spawns /// the demux/mux, and returns the client. Mirrors the server-side /// `ChannelsAdapter::handle(Connection)`. This is the one-way-door /// API surface — it must not be coupled to a transport (ADR-034, @@ -139,7 +139,7 @@ impl ChannelClient { -> Result; /// QUIC convenience constructor. Dials a QUIC connection to `addr` - /// on ALPN `alknet/channels` (credentials → TLS handshake, + /// on ALPN `alk/channels` (credentials → TLS handshake, /// ADR-034 verifier selection), then calls `from_connection`. /// Additive and two-way-door — `connect_tcp_tls`, /// `connect_webtransport`, etc. join it as transports are added. diff --git a/docs/architecture/decisions/044-channels-subcrate-decomposition.md b/docs/architecture/decisions/044-channels-subcrate-decomposition.md index 31f9134..4a58220 100644 --- a/docs/architecture/decisions/044-channels-subcrate-decomposition.md +++ b/docs/architecture/decisions/044-channels-subcrate-decomposition.md @@ -29,7 +29,7 @@ single crate depending on both `alknet-core` and `alknet-call`. The dependency on `alknet-call` arises because channel lifecycle operations (`channel/open`, `channel/close`, `channel/control`, `channel/resources/subscribe`) register on the call protocol's -`OperationRegistry`, and channel 0 is pre-negotiated as `alknet/call` +`OperationRegistry`, and channel 0 is pre-negotiated as `alk/call` (ADR-036). This creates two issues: @@ -41,7 +41,7 @@ This creates two issues: op registrations are the call-protocol coupling. Baking both into one crate means any consumer that wants the multiplexer also pulls in the call-protocol coupling, even if they don't need channel 0 to be - `alknet/call`. + `alk/call`. 2. **The "no special-casing for downstream crates" principle is violated.** The user's constraint: "we don't want to be doing anything @@ -91,24 +91,24 @@ The pure multiplexer. Contains: - `ChannelManager` (the shared state — channel_id → ChannelState, but **without** the `call_ops: Arc` field; the manager is ALPN-blind and call-protocol-blind) -- `ChannelsAdapter` (the `ProtocolHandler` on `alknet/channels` — the +- `ChannelsAdapter` (the `ProtocolHandler` on `alk/channels` — the read/demux loop, substrate-agnostic per ADR-034 revised) Depends on `alknet-core` only. No `alknet-call` dependency. No opinion about what channel 0 carries — that's the consumer's concern. The `ChannelsAdapter::handle` in `channels-core` does NOT preinstall channel -0 as `alknet/call`. It runs the demux loop and routes chunks by +0 as `alk/call`. It runs the demux loop and routes chunks by `channel_id`. Channel 0 is just another channel; what ALPN it carries is determined by the consumer (the `channels-call` crate pre-negotiates it as -`alknet/call`; a hypothetical other consumer could pre-negotiate it +`alk/call`; a hypothetical other consumer could pre-negotiate it differently). ### `alknet-channels-call` The call-protocol coupling. Contains: -- Channel 0 pre-negotiation as `alknet/call` (ADR-036) — the +- Channel 0 pre-negotiation as `alk/call` (ADR-036) — the `preinstall_channel_0` logic that constructs channel 0's reassembly buffers with stream_types [0, 1] and hands the `Connection` to the `CallAdapter`. diff --git a/docs/architecture/decisions/045-alknetclient-native-dial-seam.md b/docs/architecture/decisions/045-alknetclient-native-dial-seam.md index 58ece39..9240692 100644 --- a/docs/architecture/decisions/045-alknetclient-native-dial-seam.md +++ b/docs/architecture/decisions/045-alknetclient-native-dial-seam.md @@ -222,7 +222,7 @@ in the type. > `ConnectionCredentials`, not `CallCredentials`. `CallCredentials` > couples the dial to the call protocol (its `auth_token` field is a > call-protocol / hub-layer concept — bearer-token identity correlation -> for browsers and `alknet/register`, not a transport credential). The +> for browsers and `alk/register`, not a transport credential). The > dial uses only the transport-identity dimensions (`local_identity` + > `remote_identity`); those move to `ConnectionCredentials` in > `alknet-core`. All three dial signatures unify on @@ -350,9 +350,9 @@ removed rather than left as a vestigial enum. If `spawn_dispatch` ever gains a failure path, a fresh error type is cleaner than retrofitting this one. -### 6. `alknet/register` is a dialable ALPN (entry point, wire protocol deferred) +### 6. `alk/register` is a dialable ALPN (entry point, wire protocol deferred) -`AlknetClient::dial_quic` / `dial_tcp_tls` can dial the `alknet/register` +`AlknetClient::dial_quic` / `dial_tcp_tls` can dial the `alk/register` ALPN — the native registration entry point, parallel to HTTP registration (OQ-58) but without the HTTP layer. The connection is an **entry point** (ADR-086 §2): accepted without an established peer @@ -364,7 +364,7 @@ Two registration cases, both hub concerns and both optional: - **Token registration** — a freshly-provisioned worker (docker, vast.ai, runpod) generates its local identity, dials the hub on - `alknet/register`, presents the one-time registration token, and + `alk/register`, presents the one-time registration token, and enrolls its key. The hub creates a `PeerEntry` and returns a session credential. - **No-token (open) registration** — a hub that hosts public services @@ -372,13 +372,13 @@ Two registration cases, both hub concerns and both optional: token. The enrollment creates a `PeerEntry` with no token requirement. -The `alknet/register` **wire protocol** (the handshake on the +The `alk/register` **wire protocol** (the handshake on the `Connection` after the dial — what frames the client sends, what the hub returns) ties into the call crate's ACL and the OQ-58 enrollment model. It is **deferred** to a dedicated ADR — this ADR names the ALPN and its entry-point role; it does not specify the wire protocol. The HTTP registration endpoint (OQ-58) remains the first implementation; -`alknet/register` is the native analogue that removes the HTTP +`alk/register` is the native analogue that removes the HTTP dependency for workers that have no HTTP client. ### 7. OQ-55 is resolved for the native dial @@ -467,7 +467,7 @@ several possible native clients sharing the same wire protocols. on connection failure. `AlknetClient` provides both dials; the fallback policy is a caller concern (or a future `dial_with_fallback` helper — two-way-door). -- **`alknet/register` is named.** The native registration entry point +- **`alk/register` is named.** The native registration entry point has a home in the ALPN registry, parallel to HTTP registration. The wire protocol is deferred, but the ALPN and its entry-point role are decided — a worker that has no HTTP client can register natively. @@ -496,7 +496,7 @@ several possible native clients sharing the same wire protocols. consistency is in the rule, not in the type. This is the same exception as the server side (ADR-082, ADR-087 §3) — unavoidable, and isolated to one dial method. -- **The `alknet/register` wire protocol is still deferred.** This ADR +- **The `alk/register` wire protocol is still deferred.** This ADR names the ALPN and its role; the handshake protocol (token/no-token, the frames, the `PeerEntry` creation, the session credential return) is a separate ADR tied to OQ-58. A worker cannot register natively @@ -520,7 +520,7 @@ boilerplate. The three-dial API (`dial_quic` / `dial_tcp_tls` / `dial_iroh`) is one-way — changing the signatures after consumers exist is a rewrite. The internal implementation (how `ConnectionCredentials` feeds `TlsClientConfig::new`, how the iroh dial maps the -`Ed25519SecretKey`) is two-way. The `alknet/register` ALPN name is +`Ed25519SecretKey`) is two-way. The `alk/register` ALPN name is one-way (wire compatibility); its wire protocol is two-way until the dedicated ADR lands. @@ -551,7 +551,7 @@ dedicated ADR lands. (the take-over `AlknetClient` feeds) - [ADR-022](022-call-protocol-client-and-adapter-contract.md) — `CallClient::spawn_dispatch` (the take-over `AlknetClient` feeds) -- OQ-58 — worker registration flow (the HTTP path; `alknet/register` +- OQ-58 — worker registration flow (the HTTP path; `alk/register` is the native analogue) - `docs/architecture/crates/channels/channel-client.md` §"Relationship to `AlknetClient`" — the deferral this ADR resolves \ No newline at end of file diff --git a/docs/architecture/decisions/047-openable-alpns-are-operations.md b/docs/architecture/decisions/047-openable-alpns-are-operations.md index 761df69..f0af172 100644 --- a/docs/architecture/decisions/047-openable-alpns-are-operations.md +++ b/docs/architecture/decisions/047-openable-alpns-are-operations.md @@ -48,7 +48,7 @@ This preserves every invariant ADR-047 §4 was written to protect: registered handler (Layer 0) is not on this path. - **"Marked ops invoked outside a channels session" (ADR-047 §2):** unchanged. A `channels//sub` op registered only on a channels - connection's overlay is not reachable on a bare `alknet/call` + connection's overlay is not reachable on a bare `alk/call` connection (the overlay isn't attached there) — the dispatch path returns `NOT_FOUND`, which is the correct behavior for "no channels session" (the `channel:no_channels_session` error code from the @@ -164,7 +164,7 @@ amendment is the normal mode here. - **Gap F** (`channel_open` marker wire format) — the marker is a boolean field `"channel_open": true` on the `services/schema` payload. The ALPN is derivable from the op name (`channels//sub` → ALPN - `alknet/`); the marker is the dispatch hint, not a carrier for + `alk/`); the marker is the dispatch hint, not a carrier for the ALPN string. `spec_to_json` emits it; `rebuild_spec_for` parses it. - **Gap G** (`resource_id_path` ACL vs handler ownership) — complementary, not redundant. The ACL check (via `resource_id_path` + @@ -215,7 +215,7 @@ pub struct OperationSpec { } pub struct ChannelOpenSpec { - pub alpn: Cow<'static, str>, // e.g., "alknet/tty"; Cow so from_call can supply an owned String without Box::leak + pub alpn: Cow<'static, str>, // e.g., "alk/tty"; Cow so from_call can supply an owned String without Box::leak } ``` @@ -234,7 +234,7 @@ machinery (Gap C). **Marked ops invoked outside a channels session.** `channels/tty/sub` is registered on the call registry — which means it's also visible/invocable -on a bare top-level `alknet/call` connection, where there is no +on a bare top-level `alk/call` connection, where there is no `ChannelManager`. The open-op wrapper resolves `channel_manager()` at invocation time via the extension trait (Gap E); if it returns `None`, the wrapper returns `channel:no_channels_session`. @@ -423,10 +423,10 @@ stays distinct (endpoint ALPN wrapping channels). defaults `None`). Every spec-constructing site adds the field, defaulting to `None`. This is a mechanical, additive change — the `Option`/`None`-default keeps it non-breaking. -- The ALPN→path-segment mapping (`alknet/tty` → `tty`) needs pinning. +- The ALPN→path-segment mapping (`alk/tty` → `tty`) needs pinning. The op name `channels/tty/sub` implies the path segment is `tty`; - non-`alknet/*` ALPNs need a rule. The rule: the path segment is the - ALPN with the `alknet/` prefix stripped; ALPNs without that prefix + non-`alk/*` ALPNs need a rule. The rule: the path segment is the + ALPN with the `alk/` prefix stripped; ALPNs without that prefix use their full ALPN string as the path segment (rare case, two-way- door). - The `from_call` relay wrapper (Gap C) is a consumer concern, not in diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index d93ff58..db26c85 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -81,7 +81,7 @@ is the load-bearing piece the broker composes on. | OQ-35 | `OperationEnv::channel_manager()` coupling | resolved | high | ADR-047 §4 — extension trait `ChannelOperationEnv` in `channels-call`; call crate stays free of channels types | | OQ-36 | `channel_id` allocation in Pub case | resolved | medium | ADR-047 §5 — "connection owner allocates" (the side that holds the `ChannelManager`); amends "responder allocates" | | OQ-37 | `from_call` relay wrapper for marked ops | open | medium | ADR-047 §1 names it as a consumer (hub) concern; alkcall's `from_call` reconstructs the marker (Gap F resolved) so the consumer can branch on it | -| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alknet/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) | +| OQ-38 | ALPN→path-segment mapping | resolved | low | ADR-047 §"Negative" — strip the `alk/` prefix; ALPNs without that prefix use the full ALPN string (rare, two-way-door) | | OQ-39 | `channel/control` control-handle surface | open | medium | The `channel/control` handler currently returns `channel:control_not_implemented`. The control-handle surface (per-channel control callbacks registered by ALPN crates, routing `message` to the handler's control handle for `channel_id`) is real design work — each ALPN crate needs a way to register a control callback, and the channels layer needs a control-handle registry keyed by `channel_id`. Deferred until an ALPN crate (TTY, tunnel) needs out-of-band control. | | OQ-40 | `channel/resources/subscribe` live subscription | open | medium | The `channel/resources/subscribe` handler currently returns `channel:resources_not_implemented`. The live subscription aggregated from ALPN-crate resource enumerators (ADR-047 §6) requires each ALPN crate to provide a resource enumerator, and the channels layer to aggregate them into a live `Stream` that emits on any change. The current stub is a one-shot error; the real implementation is deferred until a consumer (hub, dashboard) needs live resource discovery. | | OQ-41 | QUIC-native multi-stream substrate | open | medium | Only the in-line substrate mode is implemented (single bidi stream, header-demuxed N channels). The QUIC-native multi-stream substrate (accept remaining bidi streams, read headers off each — ADR-034 §substrate modes) is deferred to the downstream alknet crate. The wire format and demux loop are correct for both substrates; only the outer `accept_bi()` loop is missing. The alknet crate owns the QUIC dial/accept loop and is the natural place for the multi-stream accept loop. This crate stays transport-agnostic (no QUIC dependency, WASM-compatible). | @@ -92,7 +92,7 @@ is the load-bearing piece the broker composes on. |----|-------|--------|----------|------------| | OQ-25 | BiStream type definition | resolved | high | ADR-005 — trait, Connection parameter | | OQ-26 | AuthContext resolution timing | resolved | high | ADR-003 — hybrid resolution | -| OQ-27 | ALPN string naming convention | resolved | medium | ADR-004 — alknet/ prefix | +| OQ-27 | ALPN string naming convention | resolved | medium | ADR-004 — alk/ prefix | | OQ-28 | Dynamic handler registration | resolved | low | ADR-019 — curated static, overlays dynamic | | OQ-29 | Handler-level auth resolution observability | resolved | medium | set_identity() on Connection for observability | | OQ-30 | Dynamic resource ownership | resolved | high | ADR-011 — OwnershipProvider, resource_id_path | \ No newline at end of file diff --git a/docs/architecture/operation-registry.md b/docs/architecture/operation-registry.md index 14c05c8..3c29d85 100644 --- a/docs/architecture/operation-registry.md +++ b/docs/architecture/operation-registry.md @@ -59,7 +59,7 @@ pub struct OperationSpec { } pub struct ChannelOpenSpec { - pub alpn: &'static str, // e.g., "alknet/tty" — derivable from op name, carried for convenience + pub alpn: &'static str, // e.g., "alk/tty" — derivable from op name, carried for convenience } pub enum OperationType { @@ -940,7 +940,7 @@ The `Capabilities` type holds non-serializable, zeroized secret material. It doe ## Constraints -- The registry is **layered by trust boundary** (ADR-019). The curated layer (`Local` provenance) is immutable after construction — adding a `Local` op requires restarting the process, which re-enters the startup trust boundary. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic at their respective scopes (per-session, per-connection). The pre-ADR-019 blanket immutability claim was inherited by analogy from ADR-010's `HandlerRegistry` (ALPN-level) and did not apply to the operation registry — the TLS-config argument that justifies `HandlerRegistry` immutability does not touch the operation registry, which lives behind the single ALPN `alknet/call`. +- The registry is **layered by trust boundary** (ADR-019). The curated layer (`Local` provenance) is immutable after construction — adding a `Local` op requires restarting the process, which re-enters the startup trust boundary. Session (`Session`) and imported (`FromCall` etc.) ops are dynamic at their respective scopes (per-session, per-connection). The pre-ADR-019 blanket immutability claim was inherited by analogy from ADR-010's `HandlerRegistry` (ALPN-level) and did not apply to the operation registry — the TLS-config argument that justifies `HandlerRegistry` immutability does not touch the operation registry, which lives behind the single ALPN `alk/call`. - Operation specs use JSON Schema. The call protocol's external interface is always JSON. Internal handler dispatch is via `Handler` / `StreamingHandler` trait objects (ADR-021), not a binary RPC framework. - `OperationEnv::invoke()` dispatches through the local registry. Remote dispatch (federation, head/worker routing) would be a separate mechanism at a different layer — not a prefix added to operation paths. - The call protocol does not depend on any database. Operation specs are in-memory, populated at startup. diff --git a/docs/reviews/003-alpn-prefix-rename.md b/docs/reviews/003-alpn-prefix-rename.md new file mode 100644 index 0000000..8f71bd4 --- /dev/null +++ b/docs/reviews/003-alpn-prefix-rename.md @@ -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/` to +`alk/` 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 { + 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//sub` → `alknet/`). 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/` | +| `src/registry/spec.rs` | 3 | `alknet/tty`, `alknet/` | + +**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). diff --git a/src/channels/adapter.rs b/src/channels/adapter.rs index cf1d1fd..d3434d2 100644 --- a/src/channels/adapter.rs +++ b/src/channels/adapter.rs @@ -1,5 +1,5 @@ //! `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 //! 8-byte chunk headers off the bidi stream and route each chunk's //! payload to the matching `channel_id`'s reassembly buffer. @@ -12,7 +12,7 @@ //! The wire format and demux loop are correct for both substrates; //! 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 //! mode) receives channel 0's `Connection` (already constructed by the //! 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; /// 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 /// 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. /// /// 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 /// accessible via the connection's `BidiStreamSource` (the channel-0 /// 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, >; -/// `ChannelsAdapter` — the `ProtocolHandler` for `alknet/channels`. +/// `ChannelsAdapter` — the `ProtocolHandler` for `alk/channels`. /// Its `handle()` receives one `Connection`, installs channel 0 via /// the `install_channel_zero` hook, then runs the demux loop. 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 // dispatch loop on channel 0's `Connection`. 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()))?; let channel0_source = 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 = (self.install_channel_zero)(manager.clone(), channel0_conn, auth.clone()); @@ -264,7 +264,7 @@ mod tests { #[test] fn channels_alpn_is_alknet_channels() { - assert_eq!(CHANNELS_ALPN, b"alknet/channels"); + assert_eq!(CHANNELS_ALPN, b"alk/channels"); } #[test] @@ -276,7 +276,7 @@ mod tests { }); let policy = crate::channels::policy::default_policy(); let adapter = ChannelsAdapter::new(install, policy); - assert_eq!(adapter.alpn(), b"alknet/channels"); + assert_eq!(adapter.alpn(), b"alk/channels"); } #[test] @@ -288,13 +288,13 @@ mod tests { }); let policy = crate::channels::policy::default_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] async fn handle_installs_channel_zero_and_runs_demux_loop() { 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_clone = Arc::clone(&installed); @@ -307,7 +307,7 @@ mod tests { let policy = crate::channels::policy::default_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 }); @@ -330,7 +330,7 @@ mod tests { #[tokio::test] async fn handle_demux_loop_processes_frames() { 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_clone = Arc::clone(&installed); @@ -343,7 +343,7 @@ mod tests { let policy = crate::channels::policy::default_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 }); @@ -392,7 +392,7 @@ mod tests { 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 install: InstallChannelZero = Arc::new(move |_m, _c, _a| { @@ -401,7 +401,7 @@ mod tests { }); let policy = crate::channels::policy::default_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; match result { @@ -425,7 +425,7 @@ mod tests { let manager = ChannelManager::with_defaults(mux_handle, None); let (id, _send, mut recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); @@ -480,11 +480,11 @@ mod tests { let manager = ChannelManager::with_defaults(mux_handle, None); let (id_a, _send_a, mut recv_a) = manager - .open_channel("alknet/a", "alice", None) + .open_channel("alk/a", "alice", None) .await .expect("open a"); let (id_b, _send_b, mut recv_b) = manager - .open_channel("alknet/b", "bob", None) + .open_channel("alk/b", "bob", None) .await .expect("open b"); diff --git a/src/channels/client.rs b/src/channels/client.rs index d904c27..602f22f 100644 --- a/src/channels/client.rs +++ b/src/channels/client.rs @@ -37,7 +37,7 @@ use super::reassembly::{MpscRecvStream, MpscSendStream}; /// and the `CallConnection` (for calling open ops on channel 0). /// /// 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 /// and exposes `open_channel` to call the per-ALPN open ops /// (`channels//sub`, `channels//pub`) on channel 0. @@ -48,8 +48,8 @@ pub struct ChannelClient { impl ChannelClient { /// Construct from an established `Connection` (the consumer dials - /// the transport and establishes the `alknet/channels` ALPN). - /// Installs channel 0 (pre-negotiated as `alknet/call`, + /// the transport and establishes the `alk/channels` ALPN). + /// Installs channel 0 (pre-negotiated as `alk/call`, /// ADR-036), wraps it as a `CallConnection` in single-stream call /// mode (ADR-036 amendment), spawns the demux and mux tasks, and /// returns the client. @@ -93,7 +93,7 @@ impl ChannelClient { // (for `call.requested`) and a read pump (for responses). let channel0_source = 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 (single_stream_writer, single_stream_reader) = crate::protocol::connection::split_single_stream(channel0_bidi); @@ -165,7 +165,7 @@ impl ChannelClient { /// `Connection` from these via `channel_source` and /// `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. pub async fn open_channel( &self, @@ -346,15 +346,15 @@ mod tests { let (client_end, server_end) = tokio::io::duplex(64 * 1024); 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 = - 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( make_install_channel_zero(Arc::clone(®istry)), 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 _ = 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_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 = - 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( make_install_channel_zero(Arc::clone(®istry)), 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 _ = 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_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 = - 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( make_install_channel_zero(Arc::clone(®istry)), 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 _ = 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_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 = - 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( make_install_channel_zero(Arc::clone(®istry)), 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 _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; }); @@ -819,7 +819,7 @@ mod tests { AccessControl::default(), None, ) - .with_channel_open(ChannelOpenSpec::new("alknet/tty")); + .with_channel_open(ChannelOpenSpec::new("alk/tty")); core.register_openable( spec, Arc::clone(&open_handler), @@ -842,12 +842,12 @@ mod tests { let (client_end, server_end) = tokio::io::duplex(64 * 1024); 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 = - 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 auth = AuthContext::anonymous(b"alknet/channels"); + let auth = AuthContext::anonymous(b"alk/channels"); let _server_handle = tokio::spawn(async move { 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()); manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open alice"); assert!(dyn_policy.check_open(&bob).is_ok()); manager - .open_channel("alknet/tty", "bob", None) + .open_channel("alk/tty", "bob", None) .await .expect("open bob"); @@ -988,9 +988,9 @@ mod tests { let m3 = Arc::clone(&manager); let (r1, r2, r3) = tokio::join!( - m1.open_channel("alknet/a", "alice", None), - m2.open_channel("alknet/b", "bob", None), - m3.open_channel("alknet/c", "carol", None), + m1.open_channel("alk/a", "alice", None), + m2.open_channel("alk/b", "bob", None), + m3.open_channel("alk/c", "carol", None), ); let successes = [r1.is_ok(), r2.is_ok(), r3.is_ok()] @@ -1135,7 +1135,7 @@ mod tests { AccessControl::default(), None, ) - .with_channel_open(ChannelOpenSpec::new("alknet/tty")); + .with_channel_open(ChannelOpenSpec::new("alk/tty")); core.register_openable( spec, Arc::clone(&open_handler), @@ -1157,12 +1157,12 @@ mod tests { let (client_end, server_end) = tokio::io::duplex(64 * 1024); 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 = - 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 auth = AuthContext::anonymous(b"alknet/channels"); + let auth = AuthContext::anonymous(b"alk/channels"); let _server_handle = tokio::spawn(async move { let _ = crate::core::types::ProtocolHandler::handle(&adapter, server_conn, &auth).await; }); @@ -1175,7 +1175,7 @@ mod tests { .open_channel( "channels/tty/sub", serde_json::json!({ "container": "abc" }), - "alknet/tty", + "alk/tty", ) .await .expect("open_channel"); diff --git a/src/channels/manager.rs b/src/channels/manager.rs index 109efae..7b3cfd2 100644 --- a/src/channels/manager.rs +++ b/src/channels/manager.rs @@ -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 /// `channels-call`); the `ChannelsAdapter` hands the resulting /// `Connection` to the `CallAdapter`. The `channel_id` is 0. @@ -356,7 +356,7 @@ impl ChannelManager { let state = ChannelState { demux_sender, handler_task, - alpn: "alknet/call".to_string(), + alpn: "alk/call".to_string(), }; { let mut channels = self.inner.channels.lock(); @@ -500,11 +500,11 @@ mod tests { async fn open_channel_returns_unique_ids() { let manager = make_manager_with_runner().await; let (id1, _send1, _recv1) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open 1"); let (id2, _send2, _recv2) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open 2"); assert_ne!(id1, id2, "channel IDs are unique"); @@ -515,7 +515,7 @@ mod tests { async fn open_channel_records_opener_in_ledger() { let manager = make_manager_with_runner().await; let (id, _send, _recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); assert_eq!( @@ -529,7 +529,7 @@ mod tests { async fn teardown_channel_removes_state() { let manager = make_manager_with_runner().await; let (id, _send, _recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); assert!(manager.has_channel(id)); @@ -543,11 +543,11 @@ mod tests { let manager = make_manager_with_runner().await; assert!(manager.channel_ids().is_empty(), "no channels open yet"); let (id1, _send1, _recv1) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open 1"); let (id2, _send2, _recv2) = manager - .open_channel("alknet/tunnel", "bob", None) + .open_channel("alk/tunnel", "bob", None) .await .expect("open 2"); let mut ids = manager.channel_ids(); @@ -562,7 +562,7 @@ mod tests { async fn route_payload_to_open_channel_succeeds() { let manager = make_manager_with_runner().await; let (id, _send, mut recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); manager @@ -603,7 +603,7 @@ mod tests { async fn route_payload_to_known_channel_does_not_increment_dropped_counter() { let manager = make_manager_with_runner().await; let (id, _send, mut recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); manager @@ -632,11 +632,11 @@ mod tests { async fn clear_all_returns_opener_ids() { let manager = make_manager_with_runner().await; manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); manager - .open_channel("alknet/tty", "bob", None) + .open_channel("alk/tty", "bob", None) .await .expect("open"); let drained = manager.clear_all(); @@ -703,11 +703,11 @@ mod tests { let accept = ChannelManager::new(connect_handle, 256, 64, None, ChannelSide::Accept); let (id_c, _, _) = connect - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("connect open"); let (id_a, _, _) = accept - .open_channel("alknet/tty", "bob", None) + .open_channel("alk/tty", "bob", None) .await .expect("accept open"); @@ -727,7 +727,7 @@ mod tests { let manager = ChannelManager::with_defaults(handle, None); let (send, mut recv) = manager - .adopt_channel(7, "alknet/tty", None) + .adopt_channel(7, "alk/tty", None) .await .expect("adopt"); @@ -751,10 +751,10 @@ mod tests { let manager = ChannelManager::with_defaults(handle, None); manager - .adopt_channel(7, "alknet/tty", None) + .adopt_channel(7, "alk/tty", None) .await .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(other) => panic!("expected ChannelExists, got {other}"), Ok(_) => panic!("expected ChannelExists, got Ok"), @@ -772,14 +772,14 @@ mod tests { let manager = ChannelManager::new(handle, 2, 64, None, ChannelSide::Accept); manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open 1"); manager - .open_channel("alknet/tty", "bob", None) + .open_channel("alk/tty", "bob", None) .await .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 }) => { assert_eq!(count, 2); assert_eq!(max, 2); diff --git a/src/channels/mod.rs b/src/channels/mod.rs index 4c81737..5d8ff9e 100644 --- a/src/channels/mod.rs +++ b/src/channels/mod.rs @@ -1,7 +1,7 @@ //! Channels protocol: N logical channels over one transport stream //! (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 //! (ADR-047). The channels layer is a re-framing proxy: it converts //! between "one transport stream carrying N channels" (the wire) and @@ -18,7 +18,7 @@ //! - [`source`]: `ChannelBidiStreamSource` — implements //! `BidiStreamSource` for a single channel (yield-once `accept_bi`). //! - [`adapter`]: `ChannelsAdapter` — `ProtocolHandler` for -//! `alknet/channels` (the demux loop). +//! `alk/channels` (the demux loop). //! - [`operations`]: `ChannelOperations` — registers `channel/close`, //! `channel/control`, `channel/resources/subscribe` on the call //! `OperationRegistry`; the per-ALPN open ops are registered by the diff --git a/src/channels/operations.rs b/src/channels/operations.rs index 1c739db..71ff026 100644 --- a/src/channels/operations.rs +++ b/src/channels/operations.rs @@ -802,7 +802,7 @@ mod tests { async fn resources_subscribe_handler_returns_not_implemented() { let manager = make_manager().await; manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open tty"); let handler = make_resources_subscribe_handler(manager.clone()); @@ -908,7 +908,7 @@ mod tests { async fn close_handler_success_path_closes_open_channel() { let manager = make_manager().await; let (id, _send, _recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); assert!(manager.has_channel(id)); @@ -938,7 +938,7 @@ mod tests { async fn control_handler_success_path_returns_not_implemented() { let manager = make_manager().await; let (id, _send, _recv) = manager - .open_channel("alknet/tty", "alice", None) + .open_channel("alk/tty", "alice", None) .await .expect("open"); let handler = make_control_handler(manager); @@ -1035,13 +1035,13 @@ mod tests { spawned_clone.store(true, Ordering::SeqCst); tokio::spawn(async {}) }); - let auth = AuthContext::anonymous(b"alknet/call"); + let auth = AuthContext::anonymous(b"alk/call"); let handler = make_open_handler_once( manager.clone(), policy, open_handler, auth, - "alknet/tty".to_string(), + "alk/tty".to_string(), ); let env = handler(json!({}), test_context("open-once-1")).await; match env.result { @@ -1065,13 +1065,13 @@ mod tests { spawned_clone.store(true, Ordering::SeqCst); tokio::spawn(async {}) }); - let auth = AuthContext::anonymous(b"alknet/call"); + let auth = AuthContext::anonymous(b"alk/call"); let handler = make_open_handler_stream( manager.clone(), policy, open_handler, auth, - "alknet/tty".to_string(), + "alk/tty".to_string(), ); let mut stream = handler(json!({}), test_context("open-stream-1")); let env = stream.next().await.expect("one envelope"); @@ -1095,14 +1095,9 @@ mod tests { let manager = make_manager().await; let policy = super::super::policy::default_policy(); let open_handler: OpenHandler = Arc::new(|_input, _conn, _auth| tokio::spawn(async {})); - let auth = AuthContext::anonymous(b"alknet/call"); - let handler = make_open_handler_sink( - manager, - policy, - open_handler, - auth, - "alknet/tty".to_string(), - ); + let auth = AuthContext::anonymous(b"alk/call"); + let handler = + make_open_handler_sink(manager, policy, open_handler, auth, "alk/tty".to_string()); let publish_stream: crate::registry::registration::PublishStream = Box::pin(futures::stream::empty()); let env = handler(json!({}), test_context("open-sink-1"), publish_stream).await; @@ -1127,7 +1122,7 @@ mod tests { AccessControl::default(), None, ) - .with_channel_open(ChannelOpenSpec::new("alknet/tty")); + .with_channel_open(ChannelOpenSpec::new("alk/tty")); let spawned = Arc::new(AtomicBool::new(false)); let spawned_clone = Arc::clone(&spawned); let open_handler: OpenHandler = Arc::new(move |_input, _conn, _auth| { @@ -1139,7 +1134,7 @@ mod tests { spec, open_handler, &mut registry, - AuthContext::anonymous(b"alknet/call"), + AuthContext::anonymous(b"alk/call"), ) .expect("register"); assert!(registry.registration("channels/tty/sub").is_some()); @@ -1170,7 +1165,7 @@ mod tests { AccessControl::default(), None, ) - .with_channel_open(ChannelOpenSpec::new("alknet/tty")); + .with_channel_open(ChannelOpenSpec::new("alk/tty")); let spawned = Arc::new(AtomicBool::new(false)); let spawned_clone = Arc::clone(&spawned); let open_handler: OpenHandler = Arc::new(move |_input, _conn, _auth| { @@ -1182,7 +1177,7 @@ mod tests { spec, open_handler, &mut registry, - AuthContext::anonymous(b"alknet/call"), + AuthContext::anonymous(b"alk/call"), ) .expect("register"); let mut stream = @@ -1212,14 +1207,14 @@ mod tests { AccessControl::default(), 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 mut registry = OperationRegistry::new(); core.register_openable( spec, open_handler, &mut registry, - AuthContext::anonymous(b"alknet/call"), + AuthContext::anonymous(b"alk/call"), ) .expect("register"); assert!(registry.registration("channels/tty/pub").is_some()); @@ -1249,7 +1244,7 @@ mod tests { spec, open_handler, &mut registry, - AuthContext::anonymous(b"alknet/call"), + AuthContext::anonymous(b"alk/call"), ); assert!(result.is_err()); assert!(result.unwrap_err().contains("channel_open marker")); @@ -1261,7 +1256,7 @@ mod tests { let policy: Arc = Arc::new(super::super::policy::PerIdentityChannelPolicy::new(0)); 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 { id: "alice".to_string(), scopes: vec![], @@ -1272,7 +1267,7 @@ mod tests { &policy, &open_handler, &auth, - "alknet/tty", + "alk/tty", json!({}), opener_id.id.clone(), opener_id, diff --git a/src/channels/wire.rs b/src/channels/wire.rs index 2f9f974..57ee331 100644 --- a/src/channels/wire.rs +++ b/src/channels/wire.rs @@ -29,7 +29,7 @@ pub const CHUNK_HEADER_LEN: usize = 8; /// so the demux can always resync by reading the next 8-byte header. 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` /// without an explicit open op exchange. pub const CHANNEL_ID_ZERO: u32 = 0; diff --git a/src/client/call_client.rs b/src/client/call_client.rs index 122890d..caa060a 100644 --- a/src/client/call_client.rs +++ b/src/client/call_client.rs @@ -26,7 +26,7 @@ use crate::protocol::connection::CallConnection; use crate::protocol::dispatch::Dispatcher; 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 /// in `OperationRegistry::invoke` (ADR-029 §3) — no parallel `remote_safe`/ @@ -184,7 +184,7 @@ mod tests { conn.connection() .expect("quic connection present") .remote_alpn(), - b"alknet/call" + b"alk/call" ); std::mem::drop(conn); } diff --git a/src/client/from_call.rs b/src/client/from_call.rs index a96a92c..402b819 100644 --- a/src/client/from_call.rs +++ b/src/client/from_call.rs @@ -257,7 +257,7 @@ fn rebuild_spec_for( // ADR-047 §2: the `channel_open` marker survives discovery // serialization as a boolean. The ALPN is derived from the op name - // (`channels//sub` → `alknet/`). The consumer (e.g. the + // (`channels//sub` → `alk/`). The consumer (e.g. the // hub) branches on the marker to wrap marked ops with relay // machinery (ADR-047 §1, Gap C) instead of the plain forwarding // 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 -/// (`channels//sub` → `alknet/`). Returns `None` for op +/// (`channels//sub` → `alk/`). Returns `None` for op /// names that don't match the `channels//(sub|pub)` shape — /// 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 -/// `alknet/` prefix stripped; ALPNs without that prefix use their full -/// ALPN string as the path segment (rare case). Multi-segment non-`alknet/*` +/// `alk/` prefix stripped; ALPNs without that prefix use their full +/// ALPN string as the path segment (rare case). Multi-segment non-`alk/*` /// ALPNs (e.g. `custom/proto`) survive because the `/sub` or `/pub` /// suffix is stripped from `rest` rather than taking only the first /// path segment. @@ -299,10 +299,10 @@ fn derive_alpn_from_op_name(op_name: &str) -> Option { if segment.is_empty() { return None; } - if segment.starts_with("alknet/") || segment == "alknet" || segment.contains('/') { + if segment.starts_with("alk/") || segment == "alk" || segment.contains('/') { Some(segment.to_string()) } else { - Some(format!("alknet/{segment}")) + Some(format!("alk/{segment}")) } } @@ -597,7 +597,7 @@ mod tests { schema["channel_open"] = json!(true); let spec = rebuild_spec_for(&schema, "channels/tty/sub", &None).expect("rebuild"); let marker = spec.channel_open.expect("channel_open marker parsed"); - assert_eq!(marker.alpn, "alknet/tty"); + assert_eq!(marker.alpn, "alk/tty"); } #[test] @@ -697,11 +697,11 @@ mod tests { fn derive_alpn_from_op_name_strips_channels_prefix() { assert_eq!( derive_alpn_from_op_name("channels/tty/sub"), - Some("alknet/tty".to_string()) + Some("alk/tty".to_string()) ); assert_eq!( 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!( derive_alpn_from_op_name("channels/custom/proto/sub"), 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!( derive_alpn_from_op_name("channels/vendor/service/run/pub"), @@ -728,8 +728,8 @@ mod tests { #[test] fn derive_alpn_from_op_name_explicit_alknet_prefix_returned_as_is() { assert_eq!( - derive_alpn_from_op_name("channels/alknet/tty/sub"), - Some("alknet/tty".to_string()) + derive_alpn_from_op_name("channels/alk/tty/sub"), + Some("alk/tty".to_string()) ); } @@ -737,7 +737,7 @@ mod tests { fn derive_alpn_from_op_name_strips_pub_suffix() { assert_eq!( derive_alpn_from_op_name("channels/tty/pub"), - Some("alknet/tty".to_string()) + Some("alk/tty".to_string()) ); } diff --git a/src/core/auth.rs b/src/core/auth.rs index c50a6bf..5092d04 100644 --- a/src/core/auth.rs +++ b/src/core/auth.rs @@ -81,19 +81,19 @@ mod tests { fn auth_context_is_clone() { let ctx = AuthContext { identity: None, - alpn: b"alknet/test".to_vec(), + alpn: b"alk/test".to_vec(), remote_addr: None, tls_client_fingerprint: None, }; let cloned = ctx.clone(); - assert_eq!(cloned.alpn, b"alknet/test"); + assert_eq!(cloned.alpn, b"alk/test"); assert!(cloned.identity.is_none()); } #[test] fn auth_context_anonymous_sets_alpn_only() { - let ctx = AuthContext::anonymous(b"alknet/test"); - assert_eq!(ctx.alpn, b"alknet/test"); + let ctx = AuthContext::anonymous(b"alk/test"); + assert_eq!(ctx.alpn, b"alk/test"); assert!(ctx.identity.is_none()); assert!(ctx.remote_addr.is_none()); assert!(ctx.tls_client_fingerprint.is_none()); diff --git a/src/core/types.rs b/src/core/types.rs index edbe4ff..fc78e24 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -603,10 +603,10 @@ mod from_source_tests { addr, 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); let mut stream = conn.accept_bi().await.expect("first accept_bi yields"); @@ -692,7 +692,7 @@ mod tests { fn test_connection() -> Connection { Connection::from_bidi( SinkEmpty, - b"alknet/test".to_vec(), + b"alk/test".to_vec(), Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234)), ) } @@ -789,7 +789,7 @@ mod tests { #[test] fn connection_remote_alpn_and_addr_from_bidi() { let conn = test_connection(); - assert_eq!(conn.remote_alpn(), b"alknet/test"); + assert_eq!(conn.remote_alpn(), b"alk/test"); assert_eq!( conn.remote_addr(), Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 1234)) diff --git a/src/lib.rs b/src/lib.rs index 8005dee..7a9fd03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ //! This crate is the unification of the call protocol (structured JSON RPC: //! operations, streaming subscriptions, service discovery) and the channels //! 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` — //! channel lifecycle is orchestrated by call operations on channel 0 //! (ADR-047: openable ALPNs are operations). @@ -25,7 +25,7 @@ //! wire format, demux/mux, `ChannelManager`, `ChannelsAdapter`, //! `ChannelBidiStreamSource`, `ChannelOperations`, //! `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//sub`, `channels//pub`) //! on channel 0 (ADR-047). //! @@ -47,7 +47,7 @@ //! //! A single process can be a producer of some ops, a consumer of others, //! a channel opener for TTY, and a channel acceptor for tunnels — all on -//! the same `alknet/channels` connection. +//! the same `alk/channels` connection. //! //! ### Pattern for protocol crates //! @@ -65,7 +65,7 @@ //! //! The protocol crate doesn't know whether it's running on a hub, a //! 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. pub mod channels; diff --git a/src/protocol/adapter.rs b/src/protocol/adapter.rs index 17b63e2..d4d3101 100644 --- a/src/protocol/adapter.rs +++ b/src/protocol/adapter.rs @@ -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 //! dispatches `call.requested` events to the operation registry. See @@ -148,7 +148,7 @@ impl CallAdapter { #[async_trait] impl ProtocolHandler for CallAdapter { fn alpn(&self) -> &'static [u8] { - b"alknet/call" + b"alk/call" } async fn handle(&self, connection: Connection, auth: &AuthContext) -> Result<(), HandlerError> { @@ -294,7 +294,7 @@ mod tests { let registry = Arc::new(OperationRegistry::new()); let provider: Arc = Arc::new(StaticIdentityProvider::new()); let adapter = CallAdapter::new(registry, provider); - assert_eq!(adapter.alpn(), b"alknet/call"); + assert_eq!(adapter.alpn(), b"alk/call"); } #[test] @@ -837,7 +837,7 @@ mod tests { let conn = stub_connection(); let auth = AuthContext { identity: Some(identity_with_scopes("caller", &["user"])), - alpn: b"alknet/call".to_vec(), + alpn: b"alk/call".to_vec(), remote_addr: None, tls_client_fingerprint: None, }; @@ -855,7 +855,7 @@ mod tests { let conn = stub_connection(); let auth = AuthContext { identity: None, - alpn: b"alknet/call".to_vec(), + alpn: b"alk/call".to_vec(), remote_addr: None, tls_client_fingerprint: None, }; @@ -871,7 +871,7 @@ mod tests { let conn = stub_connection(); let auth = AuthContext { identity: None, - alpn: b"alknet/call".to_vec(), + alpn: b"alk/call".to_vec(), remote_addr: None, tls_client_fingerprint: None, }; diff --git a/src/protocol/connection.rs b/src/protocol/connection.rs index 26db193..2eccc13 100644 --- a/src/protocol/connection.rs +++ b/src/protocol/connection.rs @@ -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 //! (imported ops). //! @@ -190,7 +190,7 @@ impl CallConnection { /// `forwarded_for` (ADR-032) and `auth_token` (ADR-017 §7) for the hub /// 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 /// single-stream mode (channel 0, ADR-036 amendment), writes /// `call.requested` through the shared frame writer and relies on the @@ -442,7 +442,7 @@ impl CallConnection { /// sink forwarding handler. Mirrors /// [`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 + /// `call.completed` on the same write half, and pumps the read half /// concurrently for the single `call.responded` / `call.error` @@ -1054,7 +1054,7 @@ mod tests { conn.connection() .expect("quic connection present") .remote_alpn(), - b"alknet/call" + b"alk/call" ); } diff --git a/src/protocol/dispatch.rs b/src/protocol/dispatch.rs index 7500085..8a42403 100644 --- a/src/protocol/dispatch.rs +++ b/src/protocol/dispatch.rs @@ -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 //! [`crate::client::CallClient`]'s connect path produce a [`CallConnection`] diff --git a/src/protocol/mod.rs b/src/protocol/mod.rs index cf1b8d4..24add1d 100644 --- a/src/protocol/mod.rs +++ b/src/protocol/mod.rs @@ -1,6 +1,6 @@ //! 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. pub mod abort; diff --git a/src/protocol/test_support.rs b/src/protocol/test_support.rs index bf8e931..39682be 100644 --- a/src/protocol/test_support.rs +++ b/src/protocol/test_support.rs @@ -61,7 +61,7 @@ impl AsyncWrite for SinkEmpty { pub(crate) fn sink_empty_connection() -> Connection { Connection::from_bidi( SinkEmpty, - b"alknet/call".to_vec(), + b"alk/call".to_vec(), 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 client = Connection::from_source( SingleStreamSource::new(BiStream::from_bidi(client_end), addr), - b"alknet/call".to_vec(), + b"alk/call".to_vec(), ); let server = Connection::from_source( SingleStreamSource::new(BiStream::from_bidi(server_end), addr), - b"alknet/call".to_vec(), + b"alk/call".to_vec(), ); (client, server) } diff --git a/src/registry/discovery.rs b/src/registry/discovery.rs index ff6995a..bd8e431 100644 --- a/src/registry/discovery.rs +++ b/src/registry/discovery.rs @@ -821,7 +821,7 @@ mod tests { AccessControl::default(), 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); assert_eq!(json_val.get("channel_open"), Some(&json!(true))); } diff --git a/src/registry/spec.rs b/src/registry/spec.rs index 518adf7..ff3ec78 100644 --- a/src/registry/spec.rs +++ b/src/registry/spec.rs @@ -31,8 +31,8 @@ pub enum Visibility { /// dispatch hint. /// /// `alpn` is the data-plane ALPN the channel will carry (e.g. -/// `"alknet/tty"`). It is derivable from the op name -/// (`channels//sub` → `alknet/`), but carried here so the +/// `"alk/tty"`). It is derivable from the op name +/// (`channels//sub` → `alk/`), but carried here so the /// channels layer doesn't have to parse the op name. On the wire /// (`services/schema`), the marker is a boolean `"channel_open": true`; /// the ALPN is not serialized (it's derivable). @@ -371,9 +371,9 @@ mod tests { AccessControl::default(), None, ) - .with_channel_open(ChannelOpenSpec::new("alknet/tty")); + .with_channel_open(ChannelOpenSpec::new("alk/tty")); let marker = spec.channel_open.expect("channel_open set"); - assert_eq!(marker.alpn, "alknet/tty"); + assert_eq!(marker.alpn, "alk/tty"); } #[test]