docs: re-key architecture docs off the alknet mono-repo — speak as alkcall
The ported docs still framed alknet siblings (alknet-core/-call/-channels/
-http/-tls/-client) as live load-bearing context of this crate. With the
extraction complete and alknet headed for glue-level replacement, the docs
now speak as alkcall and cite the real sibling crates (alkhttp, alktls,
alktty, alktunnels, alktrader).
- ADR-054 (new): re-own the verifier-selection rule in-crate — three
remote roles (public X.509 endpoint / transport relay / hub),
ServerCertVerifier by PeerEntry presence, RemoteIdentity None
load-bearing. Provenance: alknet ADR-034 (never ported), demoted to
footnote. Resolves OQ-21's cross-repo authority gap.
- OQ-43 (new, deferred(scope)): composition-layer ownership and shape
after the alknet replacement — the named receiver for the
downstream-side deferrals (OQ-33 endpoint layer, OQ-41 QUIC-native
substrate, OQ-42 hub broker, OQ-19 persistence adapters, ADR-045 dial
seam). README layering diagram redrawn with the composition layer as
TBD-ownership instead of "alknet / alknode" in concrete.
- Spec docs reframed: titles, self-references, dial/TLS/from_jsonschema
homes, malformed legacy cite rows removed; dead relative research
paths qualified as source-repo paths.
- ADR amendments (append-only): 027 (alknet-http -> alkhttp), 031 (post-
extraction state), 044 (split landed as modules in alkcall), 045
(name normalization; citation-space note).
- Citation hygiene: unprefixed/colliding source-repo cites prefixed
("alknet ADR-NNN (provenance)") across 009/011/022/034/035/038/040/
043/044/045; stale OQ cites corrected (session registries OQ-04, batch
semantics OQ-03, verifier rule ADR-054); stale anchors fixed.
- README provenance note consolidated; citation conventions documented.
Verification: markdown link check (all local links resolve, all in-crate
ADR cites point at existing decisions/ files); table integrity check
(uniform row widths across open-questions.md and edited tables); residual
alknet-mention sweep (only provenance/historical/intentional framing
remains); architecture review subagent pass with zero remaining criticals.
Docs-only change — no code paths touched.
This commit is contained in:
1 parent
7b8a3e7558
commit
5f019c02f1
26 files changed
+670
-383
No files matched your search
@@ -49,7 +49,7 @@ handler picks a joining strategy per-handler:
|
||||
| 1 | `BiStream` trait (`AsyncRead + AsyncWrite + Send + Unpin`) | `crates/alknet-core/src/types.rs:226` | **Vestigial in code.** Declared per ADR-005, named in ADR-008 as "a client-side / test convenience trait," but grep across the workspace finds **zero** consumers — no `impl BiStream`, no `dyn BiStream`, no `Box<dyn BiStream>`. The trait is the ecosystem convention (`tokio::net::TcpStream`, `TlsStream<TcpStream>`, `russh::Channel::into_stream()` all satisfy it natively) but it was never wired in. |
|
||||
| 2 | `Connection` (yields `(SendStream, RecvStream)` via `accept_bi`) | `crates/alknet-core/src/types.rs:507` | The handler-facing abstraction. Leaf is split. |
|
||||
| 3 | `SendStream` (AsyncWrite-only) + `RecvStream` (AsyncRead-only) | `crates/alknet-core/src/types.rs:228-294` | The actual leaves handlers receive. Each carries a quinn/iroh/generic enum (`SendStreamKind` / `RecvStreamKind`) and dispatches per-call. |
|
||||
| 4 | `WsStream` trait (recv/send `axum::ws::Message`) | `crates/alknet-http/src/websocket/upgrade.rs:44` | Bypasses `Connection` entirely. The WS session runs its own dispatch loop directly over `axum::extract::ws::WebSocket`; `CallConnection::new_overlay_only` is used instead of `Connection::from_bidi`. ADR-044/048 already say "a WS message stream is another `BiStream`-satisfying transport" — the code does not. |
|
||||
| 4 | `WsStream` trait (recv/send `axum::ws::Message`) | `crates/alknet-http/src/websocket/upgrade.rs:44` | Bypasses `Connection` entirely. The WS session runs its own dispatch loop directly over `axum::extract::ws::WebSocket`; `CallConnection::new_overlay_only` is used instead of `Connection::from_bidi`. alknet ADR-044/048 (alknet space, provenance) already say "a WS message stream is another `BiStream`-satisfying transport" — the code does not. |
|
||||
| 5 | `MpscSendStream` / `MpscRecvStream` (channels POC) | `/workspace/alknet-channels-poc/src/mpsc_stream.rs` | Split mpsc-backed halves fed to `Connection::from_stream`. The channels POC's `TunnelHandler` consumes them directly as two `tokio::io::copy` pumps — the split shape is right for the tunnel, wrong for HTTP. |
|
||||
|
||||
The two Phase 6 issues are symptoms of one root: **the leaf type is
|
||||
@@ -108,21 +108,21 @@ accepts any of these directly, no per-handler wrapper.
|
||||
|
||||
### The two-pump shape is unaffected
|
||||
|
||||
The tunnel handler (ADR-078) and the SSH `direct-tcpip` handler
|
||||
The tunnel handler (alknet ADR-078, provenance — in-crate: ADR-050) and the SSH `direct-tcpip` handler
|
||||
(future) use the split shape — two `tokio::io::copy` pumps, one per
|
||||
direction. With `BiStream` as the leaf, these handlers call
|
||||
`tokio::io::split(bidi)` to get `(ReadHalf, WriteHalf)` — the same
|
||||
stdlib idiom `tokio::io::split` already provides for `TcpStream` and
|
||||
`TlsStream<TcpStream>`. The split is a stdlib call at the handler
|
||||
boundary, not a per-handler trait wrapper. ADR-078's
|
||||
boundary, not a per-handler trait wrapper. alknet ADR-078's (alknet space, provenance)
|
||||
shutdown-on-completion contract applies to the `ReadHalf`/`WriteHalf`
|
||||
unchanged.
|
||||
|
||||
### The TTY named-sub-streams case (ADR-038, ADR-077)
|
||||
### The TTY named-sub-streams case (ADR-038; alknet ADR-077, provenance — TTY inside channels)
|
||||
|
||||
ADR-038 specifies `into_sub_streams()` returning
|
||||
`Vec<(u8, SubStreamHandle)>` where `SubStreamHandle` is
|
||||
`Send(SendStream) | Recv(RecvStream)`. ADR-077's TTY-inside-channels
|
||||
`Send(SendStream) | Recv(RecvStream)`. alknet ADR-077's (alknet space, provenance) TTY-inside-channels
|
||||
mode destructures into five named handles (`stdin`, `stdout`,
|
||||
`stderr`, `ctrl_in`, `ctrl_out`). **Every stream_type is
|
||||
unidirectional** (ADR-034) — the typed-sub-stream leaves are
|
||||
@@ -306,13 +306,13 @@ The `WsStream` trait (`upgrade.rs:44-49`), the bespoke `drive_ws_session`
|
||||
loop, the `handle_inbound_envelope` / `dispatch_envelope_to_pending`
|
||||
helpers, and the `run_ws_session` glue are removed. The session's
|
||||
wire-level invariants (binary-only, protocol-level close on text,
|
||||
`fail_all` pending on disconnect, ADR-048's `EventEnvelope` framing)
|
||||
`fail_all` pending on disconnect, alknet ADR-048's `EventEnvelope` framing (alknet space, provenance))
|
||||
move into `WsBidiStream`'s `AsyncRead` / `AsyncWrite` / `poll_shutdown`
|
||||
impls and the standard call-protocol dispatch path.
|
||||
|
||||
`CallConnection::new_overlay_only` stays — it's the
|
||||
connection-local-overlay construction for non-peer clients
|
||||
(ADR-034 §4, ADR-044 §5), orthogonal to the transport seam. What
|
||||
(alknet ADR-034 §4, alknet ADR-044 §5 — alknet space, provenance), orthogonal to the transport seam. What
|
||||
changes is that the WS session feeds it through a `Connection` rather
|
||||
than a parallel `WsStream` trait.
|
||||
|
||||
@@ -330,7 +330,7 @@ async fn accept_bi(&self) -> Result<BiStream, StreamError> {
|
||||
`into_sub_streams()` is unchanged in shape — it still returns
|
||||
`Vec<(u8, SubStreamHandle)>` with `SubStreamHandle::Send(SendStream) |
|
||||
Recv(RecvStream)`, because TTY's named sub-streams are unidirectional
|
||||
(ADR-034, ADR-077). The two paths (`accept_bi` for handlers that want
|
||||
(ADR-034; alknet ADR-077, provenance). The two paths (`accept_bi` for handlers that want
|
||||
the joined pair, `into_sub_streams` for handlers that want the typed
|
||||
unidirectional sub-streams) are preserved per ADR-038.
|
||||
|
||||
@@ -348,7 +348,7 @@ The `webtransport.md` spec describes the "VPN-like without being a VPN"
|
||||
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.
|
||||
stream. WebTransport is deferred per alknet ADR-044 (alknet space, provenance).
|
||||
|
||||
With `BiStream` as the leaf, the same path exists over WebSocket in
|
||||
v1: a browser opens a WS connection, `WsBidiStream` presents it as a
|
||||
@@ -356,7 +356,7 @@ v1: a browser opens a WS connection, `WsBidiStream` presents it as a
|
||||
runs the channels demux over it, each channel's `accept_bi` yields a
|
||||
`BiStream` that `SshAdapter::handle` receives. The WASM SSH parser
|
||||
runs over a `BiStream`-over-WS-message adapter on the browser side
|
||||
(the same `WsBidiStream` shape, browser-implemented). ADR-044/048's
|
||||
(the same `WsBidiStream` shape, browser-implemented). alknet ADR-044/048's (alknet space, provenance)
|
||||
"WS message stream is another `BiStream`-satisfying transport" becomes
|
||||
literal — the code does what the spec said.
|
||||
|
||||
@@ -379,7 +379,7 @@ ALPN-stream-proxy (ADR-040) is the channels-over-WebTransport shape,
|
||||
not an `alknet-http` shape.
|
||||
|
||||
This is out of scope for this ADR (WebTransport is deferred per
|
||||
ADR-044). It is recorded here because the unification is what makes
|
||||
alknet ADR-044, alknet space — provenance). It is recorded here because the unification is what makes
|
||||
the future extraction clean: once `accept_bi` returns `BiStream`, the
|
||||
`h3` handler's "accept a WebTransport session, yield each stream as a
|
||||
`BiStream` to the ALPN handler" shape is the same code as
|
||||
@@ -405,7 +405,7 @@ redesign.
|
||||
unidirectional sub-streams are the case that justifies keeping
|
||||
`SendStream` / `RecvStream` (as thin newtypes, not quinn-welded
|
||||
enums).
|
||||
- **The two-pump pattern** (ADR-078) — preserved. Tunnel/SSH handlers
|
||||
- **The two-pump pattern** (alknet ADR-078, provenance — in-crate: ADR-050) — preserved. Tunnel/SSH handlers
|
||||
call `tokio::io::split(bidi)` for their two pump halves; the
|
||||
shutdown-on-completion contract applies to the `ReadHalf` /
|
||||
`WriteHalf` unchanged.
|
||||
@@ -436,7 +436,7 @@ redesign.
|
||||
lines of WS-specific dispatch glue are removed from
|
||||
`alknet-http/websocket/upgrade.rs`. The WS session runs through
|
||||
`Connection::from_bidi` + the call-protocol handler like any other
|
||||
transport. ADR-044/048's "WS message stream is `BiStream`-satisfying"
|
||||
transport. alknet ADR-044/048's (alknet space, provenance) "WS message stream is `BiStream`-satisfying"
|
||||
becomes literal.
|
||||
- One abstraction (`BiStream`) replaces five. The leaf type matches the
|
||||
ecosystem convention (`russh::Channel::into_stream()`, `TcpStream`,
|
||||
@@ -550,13 +550,13 @@ compilable:
|
||||
`Connection::from_bidi` + the call-protocol handler. This is the
|
||||
largest single change and can land after (1)-(3) — the WS path is
|
||||
independent of the handler call-site updates.
|
||||
5. **Update ADR-007, ADR-008, ADR-038, ADR-077** to reflect the
|
||||
5. **Update ADR-007, ADR-008, ADR-038, alknet ADR-077 (provenance)** to reflect the
|
||||
`BiStream` return shape. ADR-007's `from_stream` constructor is
|
||||
removed; `from_bidi` is the only public stream constructor (the
|
||||
rule: the split never crosses a crate boundary as part of a
|
||||
constructor). ADR-008's `accept_bi` return type is amended. ADR-038's
|
||||
`ChannelBidiStreamSource::accept_bi` return type is amended;
|
||||
`into_sub_streams()` is unchanged. ADR-077's two-mode TTY design is
|
||||
`into_sub_streams()` is unchanged. alknet ADR-077's (alknet space, provenance) two-mode TTY design is
|
||||
unchanged (the modes differ in *how* the adapter gets sub-streams,
|
||||
not in the leaf type).
|
||||
6. **Update `findings.md` Phase 6.** The "deferred" status is
|
||||
@@ -602,12 +602,12 @@ fix (unify the leaf so the wrapper moves into core).
|
||||
`from_source` extension point are preserved)
|
||||
- ADR-038: `ChannelBidiStreamSource` (amended — `accept_bi` returns
|
||||
`BiStream`; `into_sub_streams()` is unchanged)
|
||||
- ADR-077: TTY inside channels (unchanged — the two-mode design is
|
||||
- alknet ADR-077 (provenance): TTY inside channels (unchanged — the two-mode design is
|
||||
preserved; the modes differ in how the adapter gets sub-streams,
|
||||
not in the leaf type)
|
||||
- ADR-078: two-pump shutdown-on-completion (unchanged — the contract
|
||||
- alknet ADR-078 (provenance — in-crate: ADR-050): two-pump shutdown-on-completion (unchanged — the contract
|
||||
applies to `tokio::io::split(bidi)` halves)
|
||||
- ADR-044, ADR-048: WebSocket is the v1 browser bidirectional path
|
||||
- alknet ADR-044/048 (alknet space, provenance): WebSocket is the v1 browser bidirectional path
|
||||
(this ADR makes the "WS message stream is `BiStream`-satisfying"
|
||||
claim literal)
|
||||
- `docs/research/alknet-crate-extraction/findings.md` Phase 6 — the
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (resolves OQ-42; amends the `AccessControl::check` signature and
|
||||
Accepted (resolves alknet OQ-42 — source-repo space, provenance; the question is retracked in-crate as OQ-30 — amends the `AccessControl::check` signature and
|
||||
adds `OperationSpec.resource_id_path`. Does not amend ADR-017 or ADR-018 —
|
||||
specific #4 confirms the composition authority stays static. Blocks lifted:
|
||||
the alknet-docker, alknet-tty, opencode-runner wrapper, and
|
||||
@@ -36,7 +36,7 @@ None of those crate specs can declare their `AccessControl` shapes until
|
||||
the core model answers: **how does `AccessControl::check` learn whether
|
||||
identity X owns runtime-spawned resource R?**
|
||||
|
||||
OQ-42 tracked this. The OQ resolved five sub-questions:
|
||||
alknet OQ-42 (source-repo space, provenance; in-crate: OQ-30) tracked this. The OQ resolved five sub-questions:
|
||||
|
||||
1. Storage shape — reuse the repo/adapter pattern (ADR-033).
|
||||
2. Integration point — `AccessControl::check` consults an ownership
|
||||
@@ -88,7 +88,7 @@ model has no path from that knowledge to `check`.
|
||||
|
||||
### The options considered
|
||||
|
||||
Three integration points were considered (see OQ-42 for the full
|
||||
Three integration points were considered (see alknet OQ-42, in-crate OQ-30, for the full
|
||||
reasoning):
|
||||
|
||||
- **Option 1 — augment `Identity.resources` with a per-request snapshot.**
|
||||
@@ -592,7 +592,7 @@ provider confirms it owns the container (dynamic).
|
||||
|
||||
## References
|
||||
|
||||
- OQ-42: Dynamic Resource Ownership for Runtime-Spawned Resources
|
||||
- alknet OQ-42 (provenance; in-crate: OQ-30): Dynamic Resource Ownership for Runtime-Spawned Resources
|
||||
(resolved by this ADR — the five sub-questions this ADR writes into
|
||||
decision text)
|
||||
- ADR-003: Auth as Shared Core (`IdentityProvider` — the first instance
|
||||
|
||||
@@ -617,7 +617,7 @@ existing code (which already has the right structure —
|
||||
The door-type classification is updated: `spawn_dispatch` is one-way
|
||||
(the handler-facing surface); ~~`connect` is two-way (additive
|
||||
convenience)~~ `connect` is **removed** (ADR-045 §5). The
|
||||
`AlknetClient` extraction (OQ-55) is **resolved** by ADR-045 — the
|
||||
`AlknetClient` extraction (alknet OQ-55) is **resolved** by ADR-045 — the
|
||||
shared dial seam is `alknet-client`; `spawn_dispatch` is the
|
||||
protocol-crate take-over that consumes the dial's `Connection`.
|
||||
|
||||
|
||||
@@ -177,4 +177,16 @@ endpoint.
|
||||
and no-env-vars invariant ([ADR-010](010-secret-material-flow-and-capability-injection.md))
|
||||
established by `from_openapi`.
|
||||
- Reuses `HttpServiceConfig` and `SharedHttpClient` from
|
||||
`from_openapi` (in `alknet-http`).
|
||||
`from_openapi` (in `alknet-http`).
|
||||
|
||||
## Amendment 2026-10-13: name normalization (`alknet-http` → `alkhttp`)
|
||||
|
||||
This ADR's decision text was authored when the HTTP host crate was
|
||||
named `alknet-http` inside the alknet mono-repo. The HTTP host is now
|
||||
the standalone sibling crate **`alkhttp`**, which wraps alkcall
|
||||
(extraction context: ADR-031; the adapter-location rule is unchanged —
|
||||
the trait lives in alkcall, HTTP-backed adapters live in `alkhttp`).
|
||||
The title's "in alknet-http" and the body's crate-name references
|
||||
should be read as "in `alkhttp`"; the References below link to the
|
||||
source-repo originals (provenance). The `FromJsonSchema` provenance
|
||||
variant remains in alkcall.
|
||||
@@ -140,4 +140,31 @@ The dead `irpc` / `irpc-derive` workspace deps and the `alknet-call` consumer
|
||||
dep were removed in commit `668d777`. See
|
||||
[ADR-014](014-irpc-never-integrated-hand-rolled-framing.md) for the full
|
||||
record (ADR-013, which accepted "irpc as the call protocol foundation," is
|
||||
superseded).
|
||||
superseded).
|
||||
### Amendment 4 (2026-10-13): post-extraction state — alkcall is the unified protocol crate; sibling names normalized
|
||||
|
||||
The decomposition this ADR decided has been executed; this section
|
||||
records where the named crates now live, so the decision text's
|
||||
mono-repo framing reads as history:
|
||||
|
||||
- **alkcall** (this crate) is the unified extraction of `alknet-call`
|
||||
+ `alknet-channels` + the vendored core types (the `alknet-core`
|
||||
surfaces this crate's protocol depends on: `Connection`,
|
||||
`BiStream`, `ProtocolHandler`, `AuthContext`, `IdentityProvider`,
|
||||
`AuthToken`, `Capabilities`, `OwnershipProvider`, `HandlerError`,
|
||||
`StreamError`). Two subsystems: `registry`/`protocol` (call) and the
|
||||
channels subsystem (ADR-044's `channels-core`/`channels-call` split
|
||||
landed as modules here).
|
||||
- **Sibling host crates** (now standalone, all depending on alkcall):
|
||||
`alknet-http` → `alkhttp`; `alknet-tls` → `alktls`;
|
||||
`alknet-tty` → `alktty`; `alknet-tunnels` → `alktunnels`;
|
||||
`alknet-trader` → `alktrader`.
|
||||
- **The mono-repo (alknet) is being replaced** by thin glue over
|
||||
alkcall and its siblings; its remaining roles (dial/accept helper
|
||||
`AlknetClient`, endpoint layer, hub composition) move to the
|
||||
composition layer — post-replacement owner and shape tracked as
|
||||
OQ-43 in this crate's question tracker.
|
||||
- The protocol-foundation rule (Amendment 1) carries over unchanged
|
||||
with current names: `alkhttp` depends on alkcall for operation
|
||||
registry / adapter / client types; peer-handler crates (e.g.
|
||||
`alktty`, `alktunnels`, `alktrader`) depend on alkcall only.
|
||||
@@ -22,7 +22,7 @@ JSON, tunnel's raw bytes, SSH's channel protocol).
|
||||
|
||||
The stream_type decomposition (unidirectional halves, mod 3 formula, 85
|
||||
groups) is **removed from the channels layer**. The stream_type concept
|
||||
survives in TTY's 5-byte format (ADR-052, amended by Phase 7), which the
|
||||
survives in the TTY 5-byte format (alknet ADR-052, provenance — amended by Phase 7), which the
|
||||
channels layer carries transparently in its payload. The total header
|
||||
for a TTY chunk inside channels is 13 bytes (8 channels + 5 TTY), not
|
||||
9; the two length fields are close but not identical
|
||||
@@ -47,7 +47,7 @@ Two prior formats inform this design:
|
||||
1. **SSH's channel multiplexer (RFC 4254)** — `ChannelId(u32)` with
|
||||
string-named types negotiated per channel, all traffic interleaved on one
|
||||
encrypted transport stream.
|
||||
2. **alknet-tty's chunk format (ADR-052)** — `[stream_type: u8][length: u32 be]
|
||||
2. **alknet-tty's chunk format (alknet ADR-052, provenance)** — `[stream_type: u8][length: u32 be]
|
||||
[payload]`, a fixed set of four sub-streams (stdin/stdout/stderr/control)
|
||||
within one bidi stream. Validated by two POCs and in production code.
|
||||
|
||||
@@ -100,18 +100,18 @@ client→server) and 4 (read, server→client), not one "bidirectional" 3.
|
||||
```
|
||||
|
||||
9 bytes of header. The `channel_id` is the addition over TTY's 5-byte
|
||||
format; `stream_type` and `length` are identical to TTY's fields (ADR-052),
|
||||
format; `stream_type` and `length` are identical to TTY's fields (alknet ADR-052, provenance),
|
||||
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 `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). |
|
||||
| `length` | u32 BE | The payload length in bytes. 0 = EOF sentinel (same convention as TTY — alknet ADR-052 §Sentinels (provenance)). |
|
||||
|
||||
### `MAX_CHUNK_LEN`
|
||||
|
||||
`16 * 1024 * 1024` (16 MiB), matching TTY's cap (ADR-052 §5). A chunk with
|
||||
`16 * 1024 * 1024` (16 MiB), matching TTY's cap (alknet ADR-052 §5, provenance). A chunk with
|
||||
`length > MAX_CHUNK_LEN` returns `ChunkTooLarge` and does not corrupt the
|
||||
stream — the demux drops the chunk and continues. The header is always
|
||||
exactly 9 bytes, so the demux can always resync by reading the next 9-byte
|
||||
@@ -214,7 +214,7 @@ the cost of uniformity. For the intended use cases (TTY, SSH, tunnels, call
|
||||
operations), this is noise. The high-throughput escape hatch is
|
||||
multi-connection, not stripping the header.
|
||||
|
||||
### Framing disambiguation (carried from ADR-052 §5)
|
||||
### Framing disambiguation (carried from alknet ADR-052 §5, provenance)
|
||||
|
||||
Channel 0 is just another channel — its chunks have `channel_id=0` in the
|
||||
header. Disambiguation between channel 0 (call protocol) and data channels
|
||||
@@ -225,7 +225,7 @@ first byte of a chunk payload from the server is unambiguous.
|
||||
### Zero-length sentinel = EOF
|
||||
|
||||
A zero-length chunk is delivered as an empty `Bytes`, which the reassembled
|
||||
stream interprets as EOF (same convention as TTY — ADR-052 §Sentinels). This
|
||||
stream interprets as EOF (same convention as TTY — alknet ADR-052 §Sentinels (provenance)). This
|
||||
is the clean-shutdown signal for a `(channel_id, stream_type)` pair.
|
||||
|
||||
### Sync core / async shell split
|
||||
@@ -251,7 +251,7 @@ tokio-dependent shell.
|
||||
- The hub relay correlates by `channel_id` in the header, uniformly across
|
||||
substrates — no stream-ID-to-`channel_id` mapping per leg.
|
||||
- WASM-compatible by construction — the pure core has no platform deps.
|
||||
- The framing-disambiguation property from ADR-052 carries forward unchanged.
|
||||
- The framing-disambiguation property from alknet ADR-052 (provenance) carries forward unchanged.
|
||||
|
||||
**Negative:**
|
||||
- 9 bytes per chunk on all substrates, including QUIC where the transport
|
||||
@@ -288,7 +288,7 @@ within the one-way format.
|
||||
wire format is 8 bytes, not 9; `stream_type` removed from the channels
|
||||
header; the stream_type decomposition is removed from the channels
|
||||
layer; the handler owns its sub-stream multiplexing on the `BiStream`)
|
||||
- ADR-052: alknet-tty wire format (the 5-byte format this generalizes;
|
||||
- Source-repo provenance (not ported): alknet ADR-052 — alknet-tty wire format (the 5-byte format this generalizes;
|
||||
amended by ADR-077 — scoped to direct TTY; **re-amended by ADR-035 —
|
||||
TTY always uses its 5-byte format, carried transparently in the
|
||||
channels payload**)
|
||||
|
||||
@@ -76,7 +76,7 @@ on the `BiStream` the channels layer gives them.
|
||||
framing byte, carried transparently.
|
||||
- **The control channel is handler-internal.** TTY sub-demuxes control
|
||||
from its io `BiStream` using its 5-byte format (`STREAM_CTRL_IN = 3`,
|
||||
`STREAM_CTRL_OUT = 4` — ADR-052 amended by Phase 7). The channels layer
|
||||
`STREAM_CTRL_OUT = 4` — alknet ADR-052 (provenance), amended by Phase 7). The channels layer
|
||||
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
|
||||
@@ -165,7 +165,7 @@ Three things changed since ADR-034/074/077 were accepted:
|
||||
handler boundary — but there may be a generalized reason to expose it
|
||||
(recursive composition, test helpers, the hub relay's `channel_id`
|
||||
rewrite). The exact API shape is an implementation detail for the
|
||||
channels crate, tracked as OQ-68. The *contract* — the channels layer
|
||||
channels crate, tracked as alknet OQ-68 (source-repo space, provenance; in-crate: OQ-24). The *contract* — the channels layer
|
||||
strips its 8-byte header on read and the handler parses its own framing
|
||||
from the payload — is decided here; the *function surface* is not.
|
||||
- **TTY's `wire.rs` adaptation:** TTY's `ChunkReader` currently reads from
|
||||
@@ -174,10 +174,10 @@ Three things changed since ADR-034/074/077 were accepted:
|
||||
stream_type constants, length validation, control message parsing — is
|
||||
unchanged). This is an implementation concern for the channels + TTY
|
||||
integration, not an architecture decision.
|
||||
- **Full channel-level flow-control windowing (OQ-56):** unchanged. The
|
||||
- **Full channel-level flow-control windowing (alknet OQ-56, provenance; in-crate: OQ-23):** unchanged. The
|
||||
bounded-buffer backpressure (ADR-040) is the v1 mechanism; full
|
||||
windowing is an additive extension that doesn't change the wire
|
||||
format. OQ-56 stays deferred(scope).
|
||||
format. alknet OQ-56 stays deferred(scope) in the source repo; this crate tracks it as OQ-23.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -194,7 +194,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 `alk/call` (ADR-036). Channels 1..N are opened dynamically via `channel/open` (ADR-037). |
|
||||
| `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN` (16 MiB, matching TTY's cap — ADR-052 §5). |
|
||||
| `length` | 4 | 4 (BE) | The payload length in bytes. 0 = EOF sentinel. Max `MAX_CHUNK_LEN` (16 MiB, matching TTY's cap — alknet ADR-052 §5 (provenance)). |
|
||||
|
||||
The `stream_type` byte is **removed** from the channels header. The
|
||||
channels layer has no `stream_type` concept — not in its header, not in
|
||||
@@ -206,7 +206,7 @@ tunnel's raw bytes, SSH's channel protocol).
|
||||
This amends ADR-034: the wire format is 8 bytes, not 9; the
|
||||
`stream_type` decomposition (mod 3, unidirectional halves, 85 groups) is
|
||||
removed from the channels layer. The stream_type concept survives in
|
||||
TTY's 5-byte format (ADR-052, amended by Phase 7), which the channels
|
||||
TTY's 5-byte format (alknet ADR-052, provenance — amended by Phase 7), which the channels
|
||||
layer carries transparently.
|
||||
|
||||
### 2. `into_sub_streams()` is removed; `accept_bi` is the only accessor
|
||||
@@ -430,7 +430,7 @@ inside-channels), re-introducing a separate inside-channels mode would be
|
||||
a rewrite of TTY's session driver. The trade is one unified session
|
||||
driver now vs. two-mode maintenance forever.
|
||||
|
||||
The add/strip API shape (OQ-68) is a **two-way door** — whether the
|
||||
The add/strip API shape (alknet OQ-68, provenance; in-crate: OQ-24) is a **two-way door** — whether the
|
||||
header add/strip is built into the read/write path or exposed as a
|
||||
standalone utility is an implementation detail that can change without
|
||||
breaking the wire format or the handler contract.
|
||||
@@ -472,7 +472,7 @@ breaking the wire format or the handler contract.
|
||||
`open_channel` and `Channel`)
|
||||
- ADR-044: sub-crate decomposition (unchanged — 8-byte wire format in
|
||||
`channels-core`; call-protocol coupling in `channels-call`)
|
||||
- ADR-052: alknet-tty wire format (the 5-byte format carried
|
||||
- Source-repo provenance (not ported): alknet ADR-052 — alknet-tty wire format (the 5-byte format carried
|
||||
transparently in the channels payload; the control channel split
|
||||
from Phase 7 is TTY-internal)
|
||||
- `docs/research/stream-unification/findings.md` — the research that
|
||||
|
||||
@@ -15,7 +15,7 @@ Every handler — TTY, tunnel, SSH, call — receives a `Connection`, calls
|
||||
`accept_bi()` once, gets a `BiStream`, and sub-multiplexes it however it
|
||||
wants. The "typed handler path" (this ADR's motivating case for TTY) is
|
||||
replaced by TTY sub-demuxing its `BiStream` via its own 5-byte format
|
||||
(ADR-052) — the same code TTY runs in direct mode. The two-accessor
|
||||
(alknet ADR-052, provenance) — the same code TTY runs in direct mode. The two-accessor
|
||||
design (`accept_bi` for generic handlers, `into_sub_streams` for typed
|
||||
handlers) collapses to one accessor.
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ per `ChannelManager` (`buffer_cap` field). This prevents memory exhaustion
|
||||
without the complexity of SSH's sliding-window protocol.
|
||||
|
||||
Full channel-level windowing (SSH-style sliding-window per channel) is a
|
||||
deferred extension, tracked as [OQ-56](/workspace/@alkdev/alknet/docs/architecture/questions/056-full-channel-level-flow-control-windowing.md)
|
||||
deferred extension, tracked as [alknet OQ-56](/workspace/@alkdev/alknet/docs/architecture/questions/056-full-channel-level-flow-control-windowing.md)
|
||||
(deferred(scope)). It is blocked on a real deployment observing head-of-
|
||||
line blocking where the bounded-buffer mitigation is insufficient. The
|
||||
bounded-buffer decision is made; the extension is not.
|
||||
@@ -201,7 +201,7 @@ DoS defense summary.
|
||||
- Bounded-buffer backpressure does not eliminate head-of-line blocking — it
|
||||
bounds the memory cost. A slow consumer still stalls its own channel's
|
||||
demux reads. For the intended use cases (TTY, SSH, tunnels) this is
|
||||
acceptable; full windowing is tracked as OQ-56 (deferred(scope)).
|
||||
acceptable; full windowing is tracked as alknet OQ-56 (source-repo space, provenance; in-crate: OQ-23) (deferred(scope)).
|
||||
|
||||
## Door type
|
||||
|
||||
|
||||
@@ -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 `alk/channels` per ADR-044), installs channel 0,
|
||||
WebSocket carrying `alk/channels` per alknet ADR-044 — the websocket-native-session ADR, provenance —), 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.
|
||||
@@ -73,7 +73,7 @@ surface:
|
||||
|
||||
The dial+TLS seam (the transport-specific work each dial helper does —
|
||||
verifier selection per ADR-034, handshake, produce a `Connection`) is the
|
||||
correct scope of OQ-55's deferral. `AlknetClient` is the eventual shared
|
||||
correct scope of alknet OQ-55's deferral. `AlknetClient` is the eventual shared
|
||||
*dial*; `from_connection` is the shared *channels-take-over*. Separating
|
||||
them now, before the one-way-door API is cast, is the point — not a
|
||||
deferral. The "QUIC-only initially" framing is removed; it was the
|
||||
@@ -81,7 +81,7 @@ anti-pattern this amendment corrects.
|
||||
|
||||
The door-type classification is unchanged: `from_connection` is one-way
|
||||
(the handler-facing surface), `connect_quic` is two-way (additive
|
||||
convenience). The `AlknetClient` extraction remains deferred (OQ-55) — but
|
||||
convenience). The `AlknetClient` extraction remains deferred (alknet OQ-55) — but
|
||||
what is deferred is the shared *dial*, not a QUIC-welded client API.
|
||||
|
||||
## Context
|
||||
@@ -106,14 +106,14 @@ The phase-0 research (`docs/research/alknet-channels/phase-0-findings.md`
|
||||
seam — the transport-specific work (open socket, TLS handshake, ADR-034
|
||||
verifier selection, produce a `Connection`) that each transport's dial
|
||||
helper rebuilds. Genuinely deferred — blocked on a second *transport's*
|
||||
dial existing (OQ-55 tracks this correctly). A single QUIC dial
|
||||
dial existing (alknet OQ-55 tracks this correctly). A single QUIC dial
|
||||
(`connect_quic`) does not give enough information to extract the
|
||||
transport-polymorphic dial seam; two different transport dials do.
|
||||
|
||||
This ADR decides #1 — `ChannelClient`, with `from_connection` as the
|
||||
transport-agnostic primary constructor and `connect_quic` as a transport-
|
||||
specific dial helper. #2 (the shared `AlknetClient` dial+TLS seam) stays
|
||||
deferred per OQ-55.
|
||||
deferred per alknet OQ-55.
|
||||
|
||||
## Decision
|
||||
|
||||
@@ -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 (alk/call), spawns
|
||||
/// WebSocket per alknet ADR-044 — websocket-native session, provenance —), 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,
|
||||
@@ -223,10 +223,10 @@ sense — it's one endpoint of a bidirectional channels connection. The name
|
||||
`ChannelClient` follows the `CallClient` convention (the side that dialed),
|
||||
not a request/response role.
|
||||
|
||||
### Relationship to `AlknetClient` (OQ-55 — deferred)
|
||||
### Relationship to `AlknetClient` (alknet OQ-55 — deferred)
|
||||
|
||||
`ChannelClient`'s *API* is transport-agnostic — `from_connection` takes
|
||||
a pre-established `Connection`. What is deferred (OQ-55) is the shared
|
||||
a pre-established `Connection`. What is deferred (alknet OQ-55) is the shared
|
||||
*dial+TLS* seam (`AlknetClient`): the transport-specific work each dial
|
||||
helper does — open a socket, run the TLS handshake, apply ADR-034's
|
||||
verifier-selection rule, produce a `Connection`. That dial is genuinely
|
||||
@@ -252,14 +252,14 @@ duplicated capability and not a QUIC-welded client API.
|
||||
matching the call protocol's `CallAdapter`/`CallClient` shape.
|
||||
- Bidirectionality is preserved — the client can both initiate and receive
|
||||
`channel/open`.
|
||||
- The `AlknetClient` deferral (OQ-55) is not blocked by `ChannelClient` —
|
||||
- The `AlknetClient` deferral (alknet OQ-55) is not blocked by `ChannelClient` —
|
||||
they are independent concerns. `ChannelClient` builds standalone; the
|
||||
core extraction happens later when the blocker clears.
|
||||
|
||||
**Negative:**
|
||||
- Each transport-specific dial helper duplicates ~20 lines of
|
||||
verifier-selection boilerplate (from `connect_quic`). This is the known
|
||||
cost of not extracting `AlknetClient` yet (OQ-55). Acceptable until the
|
||||
cost of not extracting `AlknetClient` yet (alknet OQ-55). Acceptable until the
|
||||
second transport's dial exists, at which point `AlknetClient` extracts
|
||||
the shared dial+TLS seam. The `from_connection` API — the one-way-door
|
||||
surface — is unaffected; only the dial helpers carry the duplication.
|
||||
@@ -276,10 +276,10 @@ one-way-door primary constructor (transport-agnostic).
|
||||
`from_connection`. Adding, removing, or changing a dial helper is cheap
|
||||
and does not touch the one-way-door surface.
|
||||
|
||||
The `AlknetClient` extraction is a **deferred decision** (OQ-55,
|
||||
The `AlknetClient` extraction is a **deferred decision** (alknet OQ-55,
|
||||
deferred(scope)), not a door-type attribute. Its door type is two-way (the
|
||||
extraction is a refactor, not a wire-format change), but it is not decided
|
||||
in this ADR — see OQ-55 for the blocking condition. What is deferred is the
|
||||
in this ADR — see alknet OQ-55 for the blocking condition. What is deferred is the
|
||||
shared *dial+TLS* seam; `from_connection`'s transport-agnostic contract is
|
||||
decided now.
|
||||
|
||||
@@ -292,7 +292,7 @@ decided now.
|
||||
removed from `open_channel` and `Channel`; handler owns sub-stream
|
||||
multiplexing)
|
||||
- ADR-039: ChannelManager (the shared state `ChannelClient` holds)
|
||||
- OQ-55: AlknetClient / client establishment extraction (the deferred core
|
||||
- alknet OQ-55: AlknetClient / client establishment extraction (the deferred core
|
||||
concern this ADR does NOT block on)
|
||||
- `docs/research/alknet-channels/phase-0-findings.md` §OQ-CH-14 (the
|
||||
research-scope question this ADR carries forward)
|
||||
|
||||
@@ -74,7 +74,7 @@ alknet-channels-core — the pure multiplexer (wire format, demux/mux,
|
||||
|
||||
There are no `channels-hub` or `channels-worker` sub-crates. The hub and
|
||||
worker are **consumers** of channels, not sub-crates of it. The existing
|
||||
`alknet-hub` crate (`docs/architecture/crates/hub/README.md`) IS the hub —
|
||||
`alknet-hub` crate (source-repo path: docs/architecture/crates/hub/README.md — alknet, provenance) IS the hub —
|
||||
it depends on `channels-call` and uses the channels protocol as its
|
||||
substrate. A worker is just a worker — it depends on `channels-call` and
|
||||
uses `ChannelClient` (ADR-043) to dial. The hub relay logic (ADR-042) lives
|
||||
@@ -168,11 +168,11 @@ The dependency direction is: `alknet-hub` → `channels-call` →
|
||||
|
||||
### Relationship to `alknet-hub`
|
||||
|
||||
The existing `alknet-hub` crate (`docs/architecture/crates/hub/README.md`)
|
||||
The existing `alknet-hub` crate (source-repo path: docs/architecture/crates/hub/README.md — alknet, provenance)
|
||||
is the hub pattern: peer lifecycle, aggregated env, service discovery. With
|
||||
channels as the substrate, `alknet-hub` gains a dependency on
|
||||
`channels-call` and incorporates the relay logic (ADR-042). The hub spec
|
||||
(`crates/hub/README.md`) will be updated to reflect that the hub uses
|
||||
(that source-repo doc) will be updated to reflect that the hub uses
|
||||
channels as its transport substrate — one channels connection per leg
|
||||
(browser↔hub, hub↔spoke), with the relay translating `channel/open` and
|
||||
byte-forwarding data channels. The hub's existing responsibilities (peer
|
||||
@@ -247,5 +247,15 @@ the dependency direction (hub/worker → channels, not channels → hub/worker).
|
||||
- ADR-043: ChannelClient (in `channels-call`; there is no
|
||||
`channels-worker` sub-crate — a worker is any crate that uses
|
||||
`ChannelClient` to dial)
|
||||
- `docs/architecture/crates/hub/README.md` — the existing hub crate (the
|
||||
relay's consumer)
|
||||
- alknet hub crate (source-repo path docs/architecture/crates/hub/README.md, provenance) — the existing hub crate (the
|
||||
relay's consumer)
|
||||
## Amendment 2026-10-13: the split landed inside alkcall, not as separate crates
|
||||
|
||||
This ADR's decision text was authored when the channels protocol lived
|
||||
in the mono-repo crate `alknet-channels` and framed the split as
|
||||
sub-crates of it. The extraction (ADR-031, amendment 4) landed the
|
||||
`channels-core` (pure multiplexer) / `channels-call` (call coupling:
|
||||
channel 0 pre-negotiation, lifecycle ops, `ChannelCore`, `ChannelClient`)
|
||||
decomposition as **modules inside alkcall** — downstream depends on
|
||||
alkcall as a single crate. Hub and worker remain consumers (no
|
||||
worker/hub sub-crate), as decided here.
|
||||
@@ -1,8 +1,18 @@
|
||||
# ADR-045: AlknetClient — the Native Client Dial Seam
|
||||
|
||||
> **Citation-space note (2026-10-13).** This ADR was authored in the
|
||||
> alknet workspace before the extraction. Its `ADR-08x` references
|
||||
> (endpoint types, `TlsClientConfig`, accept-loop runner, crypto
|
||||
> provider) and its `OQ-55`/`OQ-58` references are **source-repo
|
||||
> (alknet) space, not this crate's `decisions/`** — provenance; those
|
||||
> ADRs were never ported. The verifier-selection rule formerly cited as
|
||||
> "alknet ADR-034" is re-owned in-crate by ADR-054. This crate's own
|
||||
> tracker/ADR numbers appear only where explicitly marked.
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (resolves OQ-55; §3 and §5 amended 2026-07-16 by ADR-012 —
|
||||
Accepted (resolves alknet OQ-55 — source-repo space, provenance;
|
||||
§3 and §5 amended 2026-07-16 by ADR-012 —
|
||||
the dial credential bundle is `ConnectionCredentials` (transport-level),
|
||||
not `CallCredentials` (call-protocol-level); all three dial signatures
|
||||
unify on `&ConnectionCredentials`; `dial_iroh`'s `node_id` parameter is
|
||||
@@ -81,7 +91,7 @@ contributing to the confusion that made `AlknetClient` hard to spec:
|
||||
(client). *Server-side accept vs. client-side dial.* The endpoint
|
||||
accepts connections and resolves identity from the incoming
|
||||
connection; the client dials and presents identity (client cert)
|
||||
while verifying the remote (ADR-034).
|
||||
while verifying the remote (alknet ADR-034, provenance).
|
||||
3. **ALPN-level category** — endpoint ALPN / entry-point ALPN
|
||||
(ADR-086 §2). *Identity-gated vs. bootstrap, at the TLS layer.*
|
||||
|
||||
@@ -143,7 +153,7 @@ The symmetry:
|
||||
|---------|---------------------------|-------------------------|
|
||||
| Transports | `with_quinn` / `with_iroh` / `with_tcp_tls` — pre-built by the assembly layer | `with_quinn` / `with_iroh` / `with_tcp_tls` — pre-built by the assembly layer |
|
||||
| Per-connection work | Accept → extract ALPN + fingerprint → `Connection` → `dispatch` | Dial → TLS handshake → `Connection` (ALPN + fingerprint carried) |
|
||||
| Identity | Resolved *from* the incoming connection (fingerprint from client cert, or token on channel 0) | *Presented* (local `TlsIdentity` as client cert) + remote *verified* (ADR-034 — fingerprint pin or CA) |
|
||||
| Identity | Resolved *from* the incoming connection (fingerprint from client cert, or token on channel 0) | *Presented* (local `TlsIdentity` as client cert) + remote *verified* (alknet ADR-034, provenance — fingerprint pin or CA) |
|
||||
| What it does NOT do | Run protocols — handlers do | Run protocols — `CallClient` / `ChannelClient` do |
|
||||
| Config | `TlsServerConfig` (per endpoint type, built by assembly) | `TlsClientConfig` (per-dial, built from `CallCredentials`) |
|
||||
|
||||
@@ -168,7 +178,7 @@ pub struct AlknetClient {
|
||||
|
||||
impl AlknetClient {
|
||||
/// QUIC dial. Builds a `TlsClientConfig` from `credentials`
|
||||
/// (ADR-034 verifier selection + ADR-084 provider), dials `addr`
|
||||
/// (alknet ADR-034 verifier selection, provenance + alknet ADR-084 provider), dials `addr`
|
||||
/// on `alpn`, returns a `Connection` via
|
||||
/// `Connection::from_quinn_with_alpn`. Feature-gated on `quinn`.
|
||||
#[cfg(feature = "quinn")]
|
||||
@@ -215,7 +225,7 @@ identity + auth token bundle, from `Capabilities`). The two rustls dials
|
||||
(QUIC, TCP+TLS) build a `TlsClientConfig` from the credentials; the
|
||||
iroh dial uses the raw `Ed25519SecretKey` directly. This mirrors the
|
||||
server side's "iroh shares the key, not the config" (ADR-082, ADR-087
|
||||
§3) — the consistency is in the rule (ADR-034 verifier selection), not
|
||||
§3) — the consistency is in the rule (the verifier-selection rule — alknet ADR-034, provenance; re-owned in-crate as ADR-054), not
|
||||
in the type.
|
||||
|
||||
> **Amendment 2026-07-16 (ADR-012):** The dial credential bundle is
|
||||
@@ -238,7 +248,7 @@ in the type.
|
||||
> is derived from `creds.remote_identity.fingerprint`
|
||||
> (`ed25519:<hex>` → `NodeId::from_bytes`), the same extraction pattern
|
||||
> the rustls dials use for the verifier. The consistency is now in both
|
||||
> the rule (ADR-034) and the type. `CallCredentials` stays in
|
||||
> the rule (alknet ADR-034, provenance) and the type. `CallCredentials` stays in
|
||||
> `alknet-call` as the call-protocol credential bundle; the
|
||||
> `auth_token` is a per-request field on `call.requested` payloads
|
||||
> (set by the caller or the `from_call` forwarding handler, resolved by
|
||||
@@ -492,7 +502,7 @@ several possible native clients sharing the same wire protocols.
|
||||
graph.
|
||||
- **The iroh dial is the exception.** It does not use
|
||||
`TlsClientConfig` — iroh has its own TLS. The dial helper applies
|
||||
the same ADR-034 rule via iroh's API (NodeId match). The
|
||||
the same verifier rule (alknet ADR-034, provenance) via iroh's API (NodeId match). The
|
||||
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.
|
||||
@@ -554,4 +564,23 @@ dedicated ADR lands.
|
||||
- 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
|
||||
to `AlknetClient`" — the deferral this ADR resolves
|
||||
|
||||
## Amendment 2026-10-13: name normalization (`alknet-client` → the composition layer's dial helper)
|
||||
|
||||
This ADR's decision text was authored when the dial helper lived in the
|
||||
mono-repo crate `alknet-client`. The decision itself is name-independent
|
||||
and unchanged: **the dial lives in the consumer/composition layer; the
|
||||
protocol crates take over pre-established `Connection`s (`spawn_dispatch`
|
||||
/ `from_connection`).** Current naming:
|
||||
|
||||
- `AlknetClient` remains the dial helper's type name, but its host is
|
||||
the **composition layer** (today the alknet glue, which is being
|
||||
replaced). The post-replacement owner and shape of the composition
|
||||
layer — including this dial seam — is tracked as OQ-43.
|
||||
- `ConnectionCredentials` / `RemoteIdentity` (ADR-012) live with the
|
||||
vendored core types in alkcall.
|
||||
- The client-side verifier-selection rule this ADR's dial applies
|
||||
(fingerprint pin vs CA vs fail-closed) is re-owned in-crate by
|
||||
ADR-054; the alknet ADR-034 and ADR-084 references below are
|
||||
provenance.
|
||||
@@ -0,0 +1,125 @@
|
||||
# ADR-054: Outgoing-Only X.509 and the Three Remote Roles (Re-Owned In-Crate)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (re-owns the verifier-selection rule for alkcall's surface;
|
||||
provenance: alknet ADR-034, not ported)
|
||||
|
||||
## Context
|
||||
|
||||
alkcall's connection surface depends on a verifier-selection rule and a
|
||||
role taxonomy that were decided while this crate was a subdirectory of
|
||||
the alknet mono-repo. The deciding ADR (alknet ADR-034, "Outgoing-Only
|
||||
X.509 and Three Peer Roles") was never ported, so the rule's authority
|
||||
lives in a cross-repo link — a dependency on a document outside this
|
||||
crate for a constraint that binds this crate's public surface:
|
||||
|
||||
- `ConnectionCredentials.remote_identity: None` semantics (ADR-022 §7,
|
||||
(ADR-022 §7; alknet ADR-034 §2/§3, provenance): `None` is a load-bearing state — the
|
||||
public-X.509-endpoint path, not a missing field.
|
||||
- Client-side `ServerCertVerifier` selection is by `PeerEntry`
|
||||
presence, not by key type alone (OQ-14 #2).
|
||||
- `PeerEntry` asymmetry: a pure-client connection to a public X.509
|
||||
endpoint is not in the call-protocol peer graph on the client side
|
||||
(no `PeerEntry`, no `PeerId`, no `PeerRef::Specific` routing).
|
||||
|
||||
The alknet mono-repo is being rewritten as a thin glue layer over this
|
||||
crate and its siblings; cross-repo ADR links will not survive that
|
||||
rewrite as authority. The rule must be owned here.
|
||||
|
||||
## Decision
|
||||
|
||||
alkcall re-owns the rule in-crate. Three things are adopted:
|
||||
|
||||
### 1. Three remote roles
|
||||
|
||||
The single label "X.509 endpoint" conflated three structurally
|
||||
different remote roles:
|
||||
|
||||
- **Public X.509 endpoint** — a remote server reachable by domain
|
||||
name, authenticated by a CA-issued X.509 cert. The local node is a
|
||||
*client* of it (a third-party API, a public hub dialed over the
|
||||
open internet, a peer that exposes domain + X.509 instead of an
|
||||
Ed25519 raw key). The client authenticates to the server by bearer
|
||||
token (`auth_token` payload field when riding the call protocol);
|
||||
the server authenticates to the client by CA verification
|
||||
(WebPKI), not fingerprint pinning.
|
||||
- **Transport relay** — a connectivity-assistance node
|
||||
(iroh's DERP-equivalent) that forwards encrypted datagrams between
|
||||
peers who cannot directly connect. It is infrastructure, not an
|
||||
application peer: no registered operations, no peer graph
|
||||
participation, no `PeerEntry`. Its identity is the transport's
|
||||
concern, not this crate's.
|
||||
- **Hub / hosting node** — an application peer acting as a hub in a
|
||||
hub-and-spoke topology. An ordinary `PeerEntry` holding mixed
|
||||
fingerprints (Ed25519 raw key and/or X.509) — fingerprint pinning
|
||||
applies on both cert paths.
|
||||
|
||||
### 2. Verifier selection is by `PeerEntry` presence, not key type
|
||||
|
||||
The client-side `ServerCertVerifier` is selected by whether the local
|
||||
node holds a `PeerEntry` for the remote:
|
||||
|
||||
| Local `PeerEntry` for remote? | Remote cert | Verifier | Peer graph? |
|
||||
|-------------------------------|-------------|----------|-------------|
|
||||
| yes | Ed25519 or X.509 | fingerprint pin | in (`PeerId` resolved from client-cert fingerprint — ADR-025 §5) |
|
||||
| no | X.509 | `WebPkiServerVerifier` (CA) | **not in** (no `PeerId`) |
|
||||
| no | Ed25519 raw key | fail closed (no CA to fall back to) | not in |
|
||||
|
||||
### 3. `remote_identity: None` is load-bearing
|
||||
|
||||
`Some(fingerprint)` means "pin this" (known peer). `None` means "no
|
||||
`PeerEntry` for this remote" — CA verification for X.509 remotes,
|
||||
hard failure for Ed25519 raw-key remotes. An implementation must not
|
||||
default `remote_identity` to a placeholder to satisfy the field, and
|
||||
must not treat `None` as "skip verification."
|
||||
|
||||
### 4. Scope of the re-own
|
||||
|
||||
This ADR is the deciding authority for alkcall's surface
|
||||
(`ConnectionCredentials`, `RemoteIdentity`, the `ServerCertVerifier`
|
||||
selection the dial-composer implements, `PeerEntry` asymmetry in the
|
||||
peer graph). It does not re-own alknet ADR-034's other content (ACME
|
||||
issuance, cert rotation, the alktls config surface) — those remain
|
||||
properties of the transport/composition layer (see OQ-43, composition
|
||||
layer). alknet ADR-034 is demoted to provenance: historical origin of
|
||||
this rule, cited as such.
|
||||
|
||||
## Consequences
|
||||
|
||||
Positive:
|
||||
|
||||
- The constraint binding alkcall's public surface is decided in this
|
||||
crate's decision log; cross-repo link rot cannot strip the authority.
|
||||
- OQ-21's "deciding ADR (alknet ADR-034, not ported)" caveat is
|
||||
resolved — the surviving surfaces (`client-and-adapters.md`
|
||||
§credentials, ADR-025's `PeerEntry` asymmetry) now cite an in-crate
|
||||
ADR.
|
||||
- The alknet rewrite can proceed without a documentation dependency
|
||||
from this crate on the old repo's ADR numbering.
|
||||
|
||||
Negative:
|
||||
|
||||
- One more rule with a historical origin story in a different numbering
|
||||
space; the provenance note (README §References) is the single place
|
||||
that maps old to new.
|
||||
|
||||
## References
|
||||
|
||||
- [client-and-adapters.md](../client-and-adapters.md) — the working
|
||||
spec carrying these semantics (§Credential sources for connections,
|
||||
§Constraints "Pure-client X.509 connections", `remote_identity`
|
||||
load-bearing rule)
|
||||
- [ADR-022](022-call-protocol-client-and-adapter-contract.md) §7 —
|
||||
transport-identity dimensions on the call protocol
|
||||
- [ADR-025](025-peerentry-and-identity-id-decoupling.md) —
|
||||
`PeerEntry` asymmetry, `PeerId` source, client-cert fingerprint →
|
||||
`peer_id` resolution
|
||||
- [ADR-012](012-connectioncredentials-decouple-dial-from-call.md) —
|
||||
`ConnectionCredentials` shape (the dial consumes it; verifier
|
||||
selection is applied there)
|
||||
- OQ-21 (resolved by this ADR), OQ-43 (composition-layer ownership —
|
||||
the transport/composition half mentioned in §4's scope carve-out)
|
||||
- Provenance: alknet ADR-034, "Outgoing-Only X.509 and Three Peer
|
||||
Roles" (`/workspace/@alkdev/alknet/docs/architecture/decisions/`),
|
||||
retained as history, not authority
|
||||
Reference in new issue
Block a user