docs(architecture): eliminate em dashes from the spec docs

Rewrote every prose em dash under docs/architecture/ with natural
grammar (sentence splits, colons, semicolons, parentheses, commas);
wire-format fences, JSON/BAST examples, and byte diagrams untouched.

Verified: grep -rln '—' docs/architecture/ returns only
overview.md's fenced dependency/module-map blocks (12 dashes),
left verbatim per the fenced-code rule; all other files at zero.
This commit is contained in:
glm-5.3-flash committed 2026-11-03 14:02:07 +00:00
1 parent 6b20924f68
commit cd0afe0497
15 files changed
+555 -552

No files matched your search

+25 -25
View File
@@ -3,13 +3,13 @@ status: draft
last_updated: 2026-09-16 (the graduation ADRs 007/008 + the amendments) last_updated: 2026-09-16 (the graduation ADRs 007/008 + the amendments)
--- ---
# alktunnels — Architecture # alktunnels: Architecture
Arbitrary bidirectional tunnels over alkcall channels: the Arbitrary bidirectional tunnels over alkcall channels: the
`alk/tunnel`-family producer/consumer protocol crate. This directory `alk/tunnel`-family producer/consumer protocol crate. This directory
holds the architecture spec docs and the ADRs. It is the sibling of holds the architecture spec docs and the ADRs. It is the sibling of
alktty's architecture (`/workspace/@alkdev/alktty/docs/architecture/`) alktty's architecture (`/workspace/@alkdev/alktty/docs/architecture/`):
— same substrate (alkcall channels), same shape (a producer same substrate (alkcall channels), same shape (a producer
open-handler + a typed consumer session), different service (tunnels: open-handler + a typed consumer session), different service (tunnels:
raw byte/datagram transport, not terminal sessions). raw byte/datagram transport, not terminal sessions).
@@ -18,41 +18,41 @@ raw byte/datagram transport, not terminal sessions).
| Document | Status | Description | | Document | Status | Description |
|----------|--------|-------------| |----------|--------|-------------|
| [overview.md](overview.md) | draft | Crate purpose, the resource model in brief, dependencies, ALPN, feature gates, module map | | [overview.md](overview.md) | draft | Crate purpose, the resource model in brief, dependencies, ALPN, feature gates, module map |
| [wire.md](wire.md) | draft | The wire format: open-op params JSON (ADR-001; the direct op's `{substrate, target}` — ADR-007), the `bound` reply field + the forwarded op (ADR-008), the data-plane codec (raw pass-through for stream substrates; mandatory `[len: u16 BE]` framing for UDP — ADR-003), sentinels, half-close semantics | | [wire.md](wire.md) | draft | The wire format: open-op params JSON (ADR-001; the direct op's `{substrate, target}` per ADR-007), the `bound` reply field + the forwarded op (ADR-008), the data-plane codec (raw pass-through for stream substrates; mandatory `[len: u16 BE]` framing for UDP per ADR-003), sentinels, half-close semantics |
| [producer.md](producer.md) | draft | The producer half: the `channels/tunnel/sub` + `channels/tunnel/direct` open ops, three establisher shapes (dial, accept-first listen, bind-first listen), the pump handler, the forwarded-op opener side, registration, scope gating | | [producer.md](producer.md) | draft | The producer half: the `channels/tunnel/sub` + `channels/tunnel/direct` open ops, three establisher shapes (dial, accept-first listen, bind-first listen), the pump handler, the forwarded-op opener side, registration, scope gating |
| [consumer.md](consumer.md) | draft | The consumer half: `TunnelSession` (stream + datagram variants, `open`/`open_direct`), `TunnelListener` (the listen flow), the reverse-flow initiator, teardown ownership (ADR-005/008) | | [consumer.md](consumer.md) | draft | The consumer half: `TunnelSession` (stream + datagram variants, `open`/`open_direct`), `TunnelListener` (the listen flow), the reverse-flow initiator, teardown ownership (ADR-005/008) |
| [bast.md](bast.md) | draft | The BAST (Binary Abstract Syntax Tree) document for the data-plane's binary framing (the UDP `[len: u16 BE]` codec — ADR-003); the stream pass-through has no binary framing to describe | | [bast.md](bast.md) | draft | The BAST (Binary Abstract Syntax Tree) document for the data-plane's binary framing (the UDP `[len: u16 BE]` codec, ADR-003); the stream pass-through has no binary framing to describe |
| [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (OQ-TN-01..16; the Phase 0 ledger promoted) | | [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (OQ-TN-01..16; the Phase 0 ledger promoted) |
## Applicable ADRs ## Applicable ADRs
alktunnels-native ADRs (001..008). Upstream ADRs (alkcall/alknet) are alktunnels-native ADRs (001..008). Upstream ADRs (alkcall/alknet) are
referenced by their own numbers and are NOT ported — the alkcall referenced by their own numbers and are NOT ported; the alkcall
originals at `/workspace/@alkdev/alkcall/docs/architecture/decisions/` originals at `/workspace/@alkdev/alkcall/docs/architecture/decisions/`
remain authoritative for the shared surfaces (establishment, channels remain authoritative for the shared surfaces (establishment, channels
wire format, `pump_bidi`, identity seam). wire format, `pump_bidi`, identity seam).
| ADR | Title | Status | | ADR | Title | Status |
|-----|-------|--------| |-----|-------|--------|
| [001](decisions/001-open-params-layout.md) | Open-Op Params Layout: Resource + Substrate | Accepted; amended (params layout is per-op — the direct op's params ride ADR-007) | | [001](decisions/001-open-params-layout.md) | Open-Op Params Layout: Resource + Substrate | Accepted; amended (params layout is per-op; the direct op's params ride ADR-007) |
| [002](decisions/002-alpn-strategy.md) | Single `alk/tunnel` ALPN (Option A) | Accepted; amended (flavor-form op ids: `…/sub`, `…/direct`, `…/forwarded` on the one ALPN) | | [002](decisions/002-alpn-strategy.md) | Single `alk/tunnel` ALPN (Option A) | Accepted; amended (flavor-form op ids: `…/sub`, `…/direct`, `…/forwarded` on the one ALPN) |
| [003](decisions/003-codec-and-udp-framing.md) | Data-Plane Codec: Raw Pass-Through (Stream) / Mandatory Length Framing (UDP) | Accepted | | [003](decisions/003-codec-and-udp-framing.md) | Data-Plane Codec: Raw Pass-Through (Stream) / Mandatory Length Framing (UDP) | Accepted |
| [004](decisions/004-no-backend-trait.md) | No TunnelBackend Trait — Halves Functions at the Assembly Layer | Accepted | | [004](decisions/004-no-backend-trait.md) | No TunnelBackend Trait: Halves Functions at the Assembly Layer | Accepted |
| [005](decisions/005-consumer-session-owns-teardown.md) | The Consumer Session Type Owns Teardown | Accepted | | [005](decisions/005-consumer-session-owns-teardown.md) | The Consumer Session Type Owns Teardown | Accepted |
| [006](decisions/006-access-control-posture.md) | Access-Control Posture — the Open Gate Is the Security Boundary | Accepted; amended (`tunnel:direct`/`tunnel:forwarded` scopes; the hub re-produce policy sentence) | | [006](decisions/006-access-control-posture.md) | Access-Control Posture: the Open Gate Is the Security Boundary | Accepted; amended (`tunnel:direct`/`tunnel:forwarded` scopes; the hub re-produce policy sentence) |
| [007](decisions/007-direct-open-op.md) | The Direct Open Op — Dynamic-Target Egress | Accepted | | [007](decisions/007-direct-open-op.md) | The Direct Open Op: Dynamic-Target Egress | Accepted |
| [008](decisions/008-listen-metadata-forwarded-op.md) | Listen Metadata — Bind-First Establisher and the Forwarded Op | Accepted | | [008](decisions/008-listen-metadata-forwarded-op.md) | Listen Metadata: Bind-First Establisher and the Forwarded Op | Accepted |
## Key Design Principles ## Key Design Principles
1. **A tunnel is a resource, not an address — with one additive 1. **A tunnel is a resource, not an address (with one additive
exception.** Base params identify a produced resource + substrate exception).** Base params identify a produced resource + substrate
discriminator; the producer owns the backing (local port, docker discriminator; the producer owns the backing (local port, docker
container, in-process service, unix socket). Rich addressing container, in-process service, unix socket). Rich addressing
enters through `-D`/dynamic composition, which speaks its protocol enters through `-D`/dynamic composition, which speaks its protocol
inside the tunnel payload. The exception (ADR-007): the direct op inside the tunnel payload. The exception (ADR-007): the direct op
lets a consumer ask the producer to dial an arbitrary target — a lets a consumer ask the producer to dial an arbitrary target. That
different capability on its own op and scope (`tunnel:direct`), is a different capability on its own op and scope (`tunnel:direct`),
never implied by `tunnel:open`. See never implied by `tunnel:open`. See
[ADR-001](decisions/001-open-params-layout.md), [ADR-001](decisions/001-open-params-layout.md),
[ADR-007](decisions/007-direct-open-op.md), and [ADR-007](decisions/007-direct-open-op.md), and
@@ -61,7 +61,7 @@ wire format, `pump_bidi`, identity seam).
2. **Role follows the resource; connection direction is independent.** 2. **Role follows the resource; connection direction is independent.**
Whoever can reach the target is the producer (registers openable Whoever can reach the target is the producer (registers openable
channels); whoever wants the bytes is the consumer. `-L` and `-R` channels); whoever wants the bytes is the consumer. `-L` and `-R`
are the same open op with the entry point on different machines — are the same open op with the entry point on different machines;
no protocol-level direction (the hub-owns-the-connection model, no protocol-level direction (the hub-owns-the-connection model,
OQ-TN-03). The composed `-R` flow rides the bind-first establisher OQ-TN-03). The composed `-R` flow rides the bind-first establisher
+ the forwarded op (ADR-008). Validated by the reverse-flow POC: + the forwarded op (ADR-008). Validated by the reverse-flow POC:
@@ -70,15 +70,15 @@ wire format, `pump_bidi`, identity seam).
[consumer.md](consumer.md). [consumer.md](consumer.md).
3. **Raw pass-through for stream substrates; mandatory length framing 3. **Raw pass-through for stream substrates; mandatory length framing
for UDP.** A tunnel has one data stream per direction — no for UDP.** A tunnel has one data stream per direction: no
sub-demux key, no tunnel-level header on stream substrates (0 B sub-demux key, no tunnel-level header on stream substrates (0 B
tunnel overhead; the channels 8-byte header is the only cost). tunnel overhead; the channels 8-byte header is the only cost).
UDP rides `[len: u16 BE]` framing — mandatory for correctness UDP rides `[len: u16 BE]` framing, which is mandatory for correctness
(F-2; the rationale and the executable pin live in ADR-003). See (F-2; the rationale and the executable pin live in ADR-003). See
[ADR-003](decisions/003-codec-and-udp-framing.md) and [ADR-003](decisions/003-codec-and-udp-framing.md) and
[wire.md](wire.md). [wire.md](wire.md).
4. **The two-pump shape is `alkcall::channels::pump_bidi` — never 4. **The two-pump shape is `alkcall::channels::pump_bidi`; it is never
hand-rolled.** The producer's pump handler and the consumer-side hand-rolled.** The producer's pump handler and the consumer-side
pump both consume the upstream helper (ADR-050); each pump shuts pump both consume the upstream helper (ADR-050); each pump shuts
down the opposite sink on completion (half-close semantics fall down the opposite sink on completion (half-close semantics fall
@@ -87,7 +87,7 @@ wire format, `pump_bidi`, identity seam).
[producer.md](producer.md). [producer.md](producer.md).
5. **No backend trait.** Stream substrates need "produce boxed halves 5. **No backend trait.** Stream substrates need "produce boxed halves
for a resource" — a function, not a trait (the UDP POC's OQ-TN-05 for a resource"; that is a function, not a trait (the UDP POC's OQ-TN-05
finding). The inversion point is the assembly layer wiring finding). The inversion point is the assembly layer wiring
substrate halves into the establisher/pump; substrate types stay substrate halves into the establisher/pump; substrate types stay
in feature-gated backend modules. The listen variant (producer in feature-gated backend modules. The listen variant (producer
@@ -96,7 +96,7 @@ wire format, `pump_bidi`, identity seam).
6. **The consumer session type owns teardown.** One session = one 6. **The consumer session type owns teardown.** One session = one
channel = one tunnel (the channel ID is the flow key). The adopted channel = one tunnel (the channel ID is the flow key). The adopted
side has no wrapper awaiting its pump (reverse POC W3) — the side has no wrapper awaiting its pump (reverse POC W3): the
session must hold the pump handle and reap the channel on session must hold the pump handle and reap the channel on
completion/close. See [ADR-005](decisions/005-consumer-session-owns-teardown.md) completion/close. See [ADR-005](decisions/005-consumer-session-owns-teardown.md)
and [consumer.md](consumer.md). and [consumer.md](consumer.md).
@@ -110,13 +110,13 @@ wire format, `pump_bidi`, identity seam).
8. **Security posture: the open gate is the boundary.** Scope-gated 8. **Security posture: the open gate is the boundary.** Scope-gated
opens (`TUNNEL_OPEN_SCOPE = "tunnel:open"`, opens (`TUNNEL_OPEN_SCOPE = "tunnel:open"`,
`TUNNEL_DIRECT_SCOPE = "tunnel:direct"`, `TUNNEL_DIRECT_SCOPE = "tunnel:direct"`,
`TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"` — each stable once `TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"` (each stable once
published; ADR-006 + Amendment 1), alkcall's op-level ACL via published; ADR-006 + Amendment 1), alkcall's op-level ACL via
`register_openable`; caller identity resolves in the 0.7.0 `register_openable`; caller identity resolves in the 0.7.0
precedence order (token > `ServingConfig.identity` > transport); precedence order (token > `ServingConfig.identity` > transport);
the establisher/pump handler see the per-call opener identity. No the establisher/pump handler see the per-call opener identity. No
target allowlists in the protocol (an assembly establisher MAY target allowlists in the protocol (an assembly establisher MAY
enforce them — policy, not protocol). Hub re-produce: the hub's enforce them, but that is policy, not protocol). Hub re-produce: the hub's
ACL governs the final dial (the hub is the network boundary). See ACL governs the final dial (the hub is the network boundary). See
[ADR-006](decisions/006-access-control-posture.md), [ADR-006](decisions/006-access-control-posture.md),
[ADR-007](decisions/007-direct-open-op.md), and [ADR-007](decisions/007-direct-open-op.md), and
@@ -155,10 +155,10 @@ Questions section.
(CF-005/CF-006 = the identity seam, resolved in 0.7.0) (CF-005/CF-006 = the identity seam, resolved in 0.7.0)
- The graduation research (the two new ADRs implement it): - The graduation research (the two new ADRs implement it):
`docs/research/tunnels-graduation.md`; the upstream alkcall asks `docs/research/tunnels-graduation.md`; the upstream alkcall asks
(review 008) **landed in alkcall 0.8.0** — flavor-form discovery (review 008) **landed in alkcall 0.8.0**: flavor-form discovery
derivation + the establisher → reply `bound` projection, plus the derivation + the establisher → reply `bound` projection, plus the
in-tree `ChannelRelay`/`HubLegTemplate` (alkcall ADR-051) in-tree `ChannelRelay`/`HubLegTemplate` (alkcall ADR-051)
- Sibling precedent: alktty (`/workspace/@alkdev/alktty`) — the - Sibling precedent: alktty (`/workspace/@alkdev/alktty`), i.e. the
producer/consumer-on-channels shape, wasm-clean default, producer/consumer-on-channels shape, wasm-clean default,
feature-gated local backend, BAST convention feature-gated local backend, BAST convention
- POC code: `/workspace/alktunnels-udp-poc` (forward), - POC code: `/workspace/alktunnels-udp-poc` (forward),
+17 -17
View File
@@ -3,11 +3,11 @@ status: implemented
last_updated: 2026-09-10 last_updated: 2026-09-10
--- ---
# alktunnels — BAST Document for the `alk/tunnel` Wire Format # alktunnels: BAST Document for the `alk/tunnel` Wire Format
This document is the **BAST (Binary Abstract Syntax Tree)** This document is the **BAST (Binary Abstract Syntax Tree)**
specification for the binary portions of the `alk/tunnel` wire format specification for the binary portions of the `alk/tunnel` wire format.
— per AGENTS.md convention 12, the mandatory UDP length-framing codec Per AGENTS.md convention 12, the mandatory UDP length-framing codec
([ADR-003](decisions/003-codec-and-udp-framing.md)) is binary framing ([ADR-003](decisions/003-codec-and-udp-framing.md)) is binary framing
beyond pass-through, so the BAST document exists even though the beyond pass-through, so the BAST document exists even though the
format is trivial. It is a normative JSON document conforming to the format is trivial. It is a normative JSON document conforming to the
@@ -24,31 +24,31 @@ an optional dep and this document is there to feed it.
## Scope ## Scope
The `alk/tunnel` wire format is: a JSON open op on the channels call The `alk/tunnel` wire format is: a JSON open op on the channels call
plane (a UTF-8 JSON object inside the channels envelope — not a plane (a UTF-8 JSON object inside the channels envelope, not a
binary layout; specified in [wire.md](wire.md) §The Open Op and binary layout; specified in [wire.md](wire.md) §The Open Op and
implemented by `TunnelParams`/`tunnel_open_spec` in the Rust source), implemented by `TunnelParams`/`tunnel_open_spec` in the Rust source),
and a binary data plane inside the channel `BiStream`. and a binary data plane inside the channel `BiStream`.
Per the BAST format spec, a BAST document describes **binary data Per the BAST format spec, a BAST document describes **binary data
layouts** — not JSON payloads layouts**, not JSON payloads
([`validate_bytes` vs `validate_json`](https://alk.dev/bast/v1/spec)). ([`validate_bytes` vs `validate_json`](https://alk.dev/bast/v1/spec)).
This document therefore covers only the data plane's binary framing: This document therefore covers only the data plane's binary framing:
1. **The datagram frame** (`[len: u16 BE][datagram bytes]`) — the 1. **The datagram frame** (`[len: u16 BE][datagram bytes]`), which is the
UDP-substrate codec (ADR-003; mandatory, F-2). Stream substrates UDP-substrate codec (ADR-003; mandatory, F-2). Stream substrates
ride raw pass-through — zero tunnel-level framing, so there is ride raw pass-through, i.e. zero tunnel-level framing, so there is
nothing for BAST to describe there. nothing for BAST to describe there.
What this BAST deliberately does **not** cover: What this BAST deliberately does **not** cover:
- **The open-op params JSON shape** (`{resource, substrate}`) — UTF-8 - **The open-op params JSON shape** (`{resource, substrate}`), which is UTF-8
JSON on the call plane; specified in JSON on the call plane; specified in
[ADR-001](decisions/001-open-params-layout.md) and [ADR-001](decisions/001-open-params-layout.md) and
[wire.md](wire.md), implemented by `TunnelParams`. [wire.md](wire.md), implemented by `TunnelParams`.
- **The channels 8-byte chunk header** — the channels layer's own - **The channels 8-byte chunk header**, the channels layer's own
framing (alknet ADR-071/093); stripped transparently before the framing (alknet ADR-071/093); stripped transparently before the
tunnel data plane sees bytes. Specified upstream, not here. tunnel data plane sees bytes. Specified upstream, not here.
- **The EOF sentinel** — the channels-level zero-length chunk - **The EOF sentinel**, i.e. the channels-level zero-length chunk
(upstream); the tunnel codec never emits it (that is the F-2 (upstream); the tunnel codec never emits it (that is the F-2
invariant). invariant).
@@ -72,12 +72,12 @@ What this BAST deliberately does **not** cover:
## Annotations ## Annotations
### `DatagramFrame` — binary, exact ### `DatagramFrame`: binary, exact
Maps 1:1 to the on-wire bytes. `len` as `uint16` big-endian (2 bytes) Maps 1:1 to the on-wire bytes. `len` as `uint16` big-endian (2 bytes)
followed by exactly `len` bytes of datagram payload. The zero-byte followed by exactly `len` bytes of datagram payload. The zero-byte
payload (`len == 0`) is a legal empty datagram — a real datagram, NOT payload (`len == 0`) is a legal empty datagram (a real datagram, NOT
EOF (the F-2 invariant; EOF is the channels-level sentinel, outside EOF; the F-2 invariant; EOF is the channels-level sentinel, outside
this codec). The `maxLength: 65535` encodes the u16 bound; the codec this codec). The `maxLength: 65535` encodes the u16 bound; the codec
rejects framing a larger datagram at frame time (`Oversize`) rather rejects framing a larger datagram at frame time (`Oversize`) rather
than wrapping the length field. than wrapping the length field.
@@ -85,13 +85,13 @@ than wrapping the length field.
One frame per datagram, in both directions of the channel One frame per datagram, in both directions of the channel
`BiStream`. Frames are self-delimiting on the byte stream; a decoder `BiStream`. Frames are self-delimiting on the byte stream; a decoder
is an incremental state machine across chunk boundaries (chunk is an incremental state machine across chunk boundaries (chunk
splitting/batching is transparent — forward POC, 7-byte chunk splits splitting/batching is transparent, per the forward POC's 7-byte chunk splits
verified). verified).
### Layering note ### Layering note
The codec sits at the substrate/pump boundary (the establisher wraps The codec sits at the substrate/pump boundary (the establisher wraps
the dialed UDP half in the framed adapter — ADR-003's placement, so the dialed UDP half in the framed adapter, per ADR-003's placement, so
the pump handler stays substrate-agnostic). The BAST describes the the pump handler stays substrate-agnostic). The BAST describes the
wire inside the `BiStream` only; the pump/pump_bidi contract wire inside the `BiStream` only; the pump/pump_bidi contract
(ADR-050) is upstream's, not re-specified here. (ADR-050) is upstream's, not re-specified here.
@@ -100,9 +100,9 @@ wire inside the `BiStream` only; the pump/pump_bidi contract
- [ADR-003](decisions/003-codec-and-udp-framing.md) (the codec - [ADR-003](decisions/003-codec-and-udp-framing.md) (the codec
decision + F-2 mandate), [ADR-001](decisions/001-open-params-layout.md) decision + F-2 mandate), [ADR-001](decisions/001-open-params-layout.md)
(the JSON open-op shape — outside BAST scope) (the JSON open-op shape, outside BAST scope)
- [wire.md](wire.md) (the normative prose; §Normative byte diagrams) - [wire.md](wire.md) (the normative prose; §Normative byte diagrams)
- alktty `tty-bast.md` (the BAST precedent — same meta-schema, same - alktty `tty-bast.md` (the BAST precedent, with the same meta-schema and the same
"binary framing only" scope rule) "binary framing only" scope rule)
- Forward POC `docs/research/poc-summary.md` §3 (the codec's - Forward POC `docs/research/poc-summary.md` §3 (the codec's
validation matrix); reverse POC validation matrix); reverse POC
+51 -51
View File
@@ -1,16 +1,16 @@
--- ---
status: draft status: draft
last_updated: 2026-09-18 (`open_direct`, `TunnelListener`, the forwarded-op serving side — ADR-007/008 as amended: the correlation token) last_updated: 2026-09-18 (`open_direct`, `TunnelListener`, the forwarded-op serving side; ADR-007/008 as amended: the correlation token)
--- ---
# alktunnels — Consumer Half # alktunnels: Consumer Half
The consumer half: `TunnelSession` — the typed client for tunnel The consumer half: `TunnelSession`, the typed client for tunnel
channels — plus `TunnelListener` (the listen-flow session, ADR-008) channels, plus `TunnelListener` (the listen-flow session, ADR-008)
and the forwarded-op serving side. The consumer is whoever wants the and the forwarded-op serving side. The consumer is whoever wants the
bytes; it opens tunnel channels toward the producer (forward path), or bytes; it opens tunnel channels toward the producer (forward path), or
per local accept toward a connect-side serving producer (the reverse per local accept toward a connect-side serving producer (the reverse
flow). Both are the same session type — role follows the resource, not flow). Both are the same session type; role follows the resource, not
the connection (OQ-TN-03). Design decisions: the connection (OQ-TN-03). Design decisions:
[ADR-005](decisions/005-consumer-session-owns-teardown.md) (session [ADR-005](decisions/005-consumer-session-owns-teardown.md) (session
owns teardown), [ADR-003](decisions/003-codec-and-udp-framing.md) owns teardown), [ADR-003](decisions/003-codec-and-udp-framing.md)
@@ -20,24 +20,24 @@ metadata + the forwarded op); this document is the normative WHAT.
## Opening a Tunnel ## Opening a Tunnel
### Forward path (`-L`/direct — the consumer dials the producer's transport) ### Forward path (`-L`/direct, the consumer dials the producer's transport)
```rust ```rust
let session = TunnelSession::open(&channel_client, params).await?; let session = TunnelSession::open(&channel_client, params).await?;
``` ```
- `params`: `{resource, substrate}` (ADR-001) — the produced resource - `params`: `{resource, substrate}` (ADR-001), i.e. the produced resource
+ substrate discriminator. + substrate discriminator.
- Flow: `ChannelClient::open_channel("channels/tunnel/sub", params, - Flow: `ChannelClient::open_channel("channels/tunnel/sub", params,
"alk/tunnel")` → adopt the returned channel ID → split the channel "alk/tunnel")` → adopt the returned channel ID → split the channel
`BiStream` → present the substrate-shaped data plane. `BiStream` → present the substrate-shaped data plane.
- Errors: a failed open resolves a typed error — - Errors: a failed open resolves a typed error:
`ChannelOpenError::CallFailed` carrying the wire `CallError`; `ChannelOpenError::CallFailed` carrying the wire `CallError`;
branch on `establishment_reason()` for `channel:open_failed`'s branch on `establishment_reason()` for `channel:open_failed`'s
reason (dial_failed / unknown_resource / resource_shortage / reason (dial_failed / unknown_resource / resource_shortage /
handler_error / timeout), or the pre-establishment codes handler_error / timeout), or the pre-establishment codes
(`FORBIDDEN`, `channel:too_many_channels`). **A failed open never (`FORBIDDEN`, `channel:too_many_channels`). **A failed open never
yields a session** — no phantom session, mirroring the yields a session**; no phantom session, mirroring the
no-phantom-channel property (POC-verified). no-phantom-channel property (POC-verified).
### Direct path (dynamic-target egress, ADR-007) ### Direct path (dynamic-target egress, ADR-007)
@@ -51,20 +51,20 @@ let session = TunnelSession::open_direct(&client, target, substrate).await?;
caller states which one it holds. The session type, data planes, caller states which one it holds. The session type, data planes,
and teardown API are identical. and teardown API are identical.
- `target`: the substrate-shaped JSON object (`{host, port}` / - `target`: the substrate-shaped JSON object (`{host, port}` /
`{path}` — ADR-007 §2); domain-form hosts stay unresolved (the `{path}`; ADR-007 §2); domain-form hosts stay unresolved (the
producer resolves). producer resolves).
- Flow: `open_channel("channels/tunnel/direct", {substrate, target}, - Flow: `open_channel("channels/tunnel/direct", {substrate, target},
"alk/tunnel")` → same adopt/split/data-plane flow as `open`. "alk/tunnel")` → same adopt/split/data-plane flow as `open`.
- Errors: the same typed surface minus `unknown_resource` (no - Errors: the same typed surface minus `unknown_resource` (no
registry lookup); malformed targets are the schema/ registry lookup); malformed targets are the schema/
`invalid_input`-class failure. An old producer rejects the unknown `invalid_input`-class failure. An old producer rejects the unknown
op loudly (`NOT_FOUND` — the SSH posture). op loudly (`NOT_FOUND`, the SSH posture).
### Reverse path (`-R` — the consumer initiates toward a connect-side serving producer) ### Reverse path (`-R`, the consumer initiates toward a connect-side serving producer)
The hub/accept-side shape (the reverse POC's `ReverseTunnel`, the The hub/accept-side shape (the reverse POC's `ReverseTunnel`, the
seed of this path). Construction differs from the forward path only seed of this path). Construction differs from the forward path only
in how the channel is obtained — the session type and teardown API in how the channel is obtained; the session type and teardown API
are identical: are identical:
```rust ```rust
@@ -74,29 +74,29 @@ let session = TunnelSession::adopt(hub_manager, channel_id, substrate, TUNNEL_AL
let pumped = session.pump_against(accepted_halves).await; // or drive the halves directly let pumped = session.pump_against(accepted_halves).await; // or drive the halves directly
``` ```
- `open_reverse_channel` — call the worker's open op on channel 0 - `open_reverse_channel`: call the worker's open op on channel 0
(`call_with_payload`; optional `auth_token`), extract the (`call_with_payload`; optional `auth_token`), extract the
worker-allocated `channel_id`. Caller identity rides the transport worker-allocated `channel_id`. Caller identity rides the transport
by default (alkcall 0.7.0 CF-005 (b): `Connection::set_identity` by default (alkcall 0.7.0 CF-005 (b): `Connection::set_identity`
before dialing); the payload `auth_token` is the optional before dialing); the payload `auth_token` is the optional
hub-forwarding path (precedence: token > `ServingConfig.identity` > hub-forwarding path (precedence: token > `ServingConfig.identity` >
transport). transport).
- `TunnelSession::adopt(manager, channel_id, substrate, alpn)` — - `TunnelSession::adopt(manager, channel_id, substrate, alpn)`:
adopt the worker-allocated ID (connection-owner rule, ADR-047 §5 — adopt the worker-allocated ID (connection-owner rule, ADR-047 §5:
the SERVING side allocates; the initiator adopts; early-arrival the SERVING side allocates; the initiator adopts; early-arrival
parking covers the adoption race), install the session's data plane parking covers the adoption race), install the session's data plane
from the adopted halves. The substrate parameter is required — a from the adopted halves. The substrate parameter is required; a
datagram session cannot exist without it (the data plane variant is datagram session cannot exist without it (the data plane variant is
substrate-shaped). `adopt` takes the same typed-error surface as substrate-shaped). `adopt` takes the same typed-error surface as
`open` (an adopt failure is `AdoptFailed`-class, not an `open` (an adopt failure is `AdoptFailed`-class, not an
establishment error). establishment error).
- `pump_against(accepted_halves)` — spawn `pump_bidi` against the - `pump_against(accepted_halves)`: spawn `pump_bidi` against the
accepted local connection and hold the pump handle (ADR-005). accepted local connection and hold the pump handle (ADR-005).
The session type and its teardown API are identical on both paths — The session type and its teardown API are identical on both paths:
one API, `-L` and `-R` alike. one API, `-L` and `-R` alike.
## The Listen Flow — `TunnelListener` (ADR-008) ## The Listen Flow: `TunnelListener` (ADR-008)
The listen-flow sibling of `TunnelSession`: the session that OPENS a The listen-flow sibling of `TunnelSession`: the session that OPENS a
bind-first listen tunnel and owns its teardown (ADR-005's ownership bind-first listen tunnel and owns its teardown (ADR-005's ownership
@@ -106,25 +106,25 @@ opener's manager (`adopt` gains nothing).
```rust ```rust
let listener = TunnelListener::open(&client, params).await?; // bind-first: fast establishment let listener = TunnelListener::open(&client, params).await?; // bind-first: fast establishment
let bound = listener.bound(); // the resolved OS-chosen address (Option — absent when unknown) let bound = listener.bound(); // the resolved OS-chosen address (Option; absent when unknown)
let token = listener.listen(); // the correlation token (the producer's; from the `listen` reply field) let token = listener.listen(); // the correlation token (the producer's; from the `listen` reply field)
let sessions = listener.forwarded(); // the forwarded sessions, as they arrive let sessions = listener.forwarded(); // the forwarded sessions, as they arrive
listener.close(self); // or Drop — tears the listen channel + far listener down listener.close(self); // or Drop; tears the listen channel + far listener down
``` ```
- **`TunnelListener::open`** — the same `channels/tunnel/sub` open - **`TunnelListener::open`**: the same `channels/tunnel/sub` open
with `{resource, substrate}`; the establisher shape (bind-first vs with `{resource, substrate}`; the establisher shape (bind-first vs
accept-first) is the producer's per-registration choice, invisible accept-first) is the producer's per-registration choice, invisible
to the constructor. What differs: bind-first replies carry to the constructor. What differs: bind-first replies carry
`"bound"` (ADR-008 §2 — the resolved address; informational, never `"bound"` (ADR-008 §2: the resolved address; informational, never
a dial target) and `"listen"` (ADR-008 Amendment 1 — the a dial target) and `"listen"` (ADR-008 Amendment 1: the
correlation token the producer minted). correlation token the producer minted).
- **`bound()`** — `Option`-shaped: `Some` when the producer supplied - **`bound()`**: `Option`-shaped: `Some` when the producer supplied
the field (bind-first), `None` otherwise (accept-first, dial the field (bind-first), `None` otherwise (accept-first, dial
establishers, or old producers). reply#1-class fidelity (BND.ADDR). establishers, or old producers). reply#1-class fidelity (BND.ADDR).
- **`listen()`** — `Option<u32>`: the correlation token (bind-first - **`listen()`**: `Option<u32>`: the correlation token (bind-first
producers). The opener-side correlation registry — token → waiting producers). The opener-side correlation registry, token → waiting
session — is assembly-side bookkeeping (consumer.md's posture: session, is assembly-side bookkeeping (consumer.md's posture:
the protocol provides the op and params, not the wait state the protocol provides the op and params, not the wait state
machine); `TunnelListener` hands the assembly the key. machine); `TunnelListener` hands the assembly the key.
- **The forwarded side:** each accepted connection at the far - **The forwarded side:** each accepted connection at the far
@@ -135,7 +135,7 @@ listener.close(self); // or Drop — tears the listen channel + far li
session, adopts the allocated channel (`TunnelSession::adopt`), session, adopts the allocated channel (`TunnelSession::adopt`),
and pumps against the local BIND client's halves. and pumps against the local BIND client's halves.
- **`peer` fidelity:** the forwarded op's params carry the accepted - **`peer` fidelity:** the forwarded op's params carry the accepted
connection's real remote address (reply#2-class fidelity — the connection's real remote address (reply#2-class fidelity; the
alksocks POC's sentinel retires). Producer-asserted metadata, not alksocks POC's sentinel retires). Producer-asserted metadata, not
transport truth. transport truth.
- **Teardown:** dropping/`close`ing the listener EOFs the listen - **Teardown:** dropping/`close`ing the listener EOFs the listen
@@ -145,18 +145,18 @@ listener.close(self); // or Drop — tears the listen channel + far li
## The Data Plane (substrate-shaped) ## The Data Plane (substrate-shaped)
- **Stream variant (`tcp`, `unix`):** `stream_halves()` — raw - **Stream variant (`tcp`, `unix`):** `stream_halves()`: raw
`AsyncRead`/`AsyncWrite` halves; the halves ARE the tunnel (raw `AsyncRead`/`AsyncWrite` halves; the halves ARE the tunnel (raw
pass-through, ADR-003). For owned access (spawning local pumps), pass-through, ADR-003). For owned access (spawning local pumps),
`take_halves()` consumes the session's halves. `take_halves()` consumes the session's halves.
- **Datagram variant (`udp`):** `send_datagram(&[u8])` / - **Datagram variant (`udp`):** `send_datagram(&[u8])` /
`recv_datagram() -> Option<Bytes>` over the mandatory `recv_datagram() -> Option<Bytes>` over the mandatory
`[len: u16 BE]` codec (ADR-003). `recv_datagram` returns `[len: u16 BE]` codec (ADR-003). `recv_datagram` returns
`Some(bytes)` per datagram — possibly empty (`len=0` is a legal `Some(bytes)` per datagram (possibly empty; `len=0` is a legal
empty datagram) — and `None` only on stream EOF (the channels-level empty datagram), and `None` only on stream EOF (the channels-level
sentinel; the codec never collides with it, F-2). A >65535-byte sentinel; the codec never collides with it, F-2). A >65535-byte
send is rejected at frame time (`Oversize`). `take_halves()` on a send is rejected at frame time (`Oversize`). `take_halves()` on a
datagram session yields the RAW halves — the wire bytes carry the datagram session yields the RAW halves; the wire bytes carry the
codec framing (ADR-005's N-3 amendment: one `take_halves` semantic codec framing (ADR-005's N-3 amendment: one `take_halves` semantic
for every substrate; keep the session for codec-applied I/O, or for every substrate; keep the session for codec-applied I/O, or
compose the framed adapter at your own boundary). compose the framed adapter at your own boundary).
@@ -165,27 +165,27 @@ listener.close(self); // or Drop — tears the listen channel + far li
One session = one channel = one tunnel. The session owns: One session = one channel = one tunnel. The session owns:
- **The adopted channel entry** — nothing upstream awaits the - **The adopted channel entry**: nothing upstream awaits the
adopter's pump (the W3 gap); the session closes it structurally. adopter's pump (the W3 gap); the session closes it structurally.
- **The session-owned pump handle** (when the session spawned one — - **The session-owned pump handle** (when the session spawned one,
the reverse path's `pump_against`). i.e. the reverse path's `pump_against`).
Teardown API: Teardown API:
- `close(self)` — abort the pump (if session-owned), tear down the - `close(self)`: abort the pump (if session-owned), tear down the
adopted channel (`ChannelManager::teardown_channel`), consume the adopted channel (`ChannelManager::teardown_channel`), consume the
session. The ungraceful path. session. The ungraceful path.
- `join(self)` — await pump completion (both directions finished; - `join(self)`: await pump completion (both directions finished;
half-close semantics fall out of `pump_bidi` — one direction EOFs, half-close semantics fall out of `pump_bidi`: one direction EOFs,
the opposite sink shuts down, the other keeps pumping until its own the opposite sink shuts down, the other keeps pumping until its own
EOF; W4-validated), THEN reap the adopted channel. Returns the EOF; W4-validated), THEN reap the adopted channel. Returns the
`(u64, u64)` copy counts for observability. The graceful path. `(u64, u64)` copy counts for observability. The graceful path.
**Pump-less sessions** (after `take_halves`, or a datagram session **Pump-less sessions** (after `take_halves`, or a datagram session
the caller drives directly): there is no session-owned pump — the caller drives directly): there is no session-owned pump, so
`join` completes immediately (nothing to await) and reaps only; `join` completes immediately (nothing to await) and reaps only;
the copy counts are `(0, 0)`. the copy counts are `(0, 0)`.
- `Drop` — aborts the pump and sync-reaps the channel (teardown - `Drop`: aborts the pump and sync-reaps the channel (teardown
semantics of `close`, best-effort — `Drop` cannot await). Dropping semantics of `close`; best-effort, since `Drop` cannot await). Dropping
without close/join never leaks the channel entry. without close/join never leaks the channel entry.
- **`TunnelSession` does not implement `Clone`.** Aliasing a session - **`TunnelSession` does not implement `Clone`.** Aliasing a session
would alias its teardown; multi-channel consumers hold a would alias its teardown; multi-channel consumers hold a
@@ -195,7 +195,7 @@ Teardown API:
A peer-initiated `channel/close` (the generic channel op on the A peer-initiated `channel/close` (the generic channel op on the
producer's serving registry) tears the SERVING side down; the producer's serving registry) tears the SERVING side down; the
consumer's session still owns its adopted entry — `close`/`join` reaps consumer's session still owns its adopted entry, so `close`/`join` reaps
it. The reverse POC pinned this split (the close ran on the worker; it. The reverse POC pinned this split (the close ran on the worker;
the hub reaped its own entry, W3). EOF propagation is symmetric: the hub reaped its own entry, W3). EOF propagation is symmetric:
either side's teardown reaches the other as the channels-level either side's teardown reaches the other as the channels-level
@@ -208,20 +208,20 @@ roles (alkcall ADR-022 §2): a worker dials the hub (connect side),
serves its open ops (serving side), and its own outbound calls still serves its open ops (serving side), and its own outbound calls still
resolve while serving (reverse POC: worker_outbound_calls_still_ resolve while serving (reverse POC: worker_outbound_calls_still_
resolve_while_serving). The consumer half adds nothing to the resolve_while_serving). The consumer half adds nothing to the
connection's serving posture — a pure consumer (`from_connection`) connection's serving posture: a pure consumer (`from_connection`)
never dispatches inbound requests; a reverse-flow initiator needs no never dispatches inbound requests; a reverse-flow initiator needs no
serving registry of its own. serving registry of its own.
## Open Questions ## Open Questions
- **OQ-TN-12**: hub re-produce composition (deferred — a hub proxy - **OQ-TN-12**: hub re-produce composition (deferred; a hub proxy
re-exporting a consumed resource would add a re-exporting a consumed resource would add a
`register_openable_with_establisher` whose establisher opens a `register_openable_with_establisher` whose establisher opens a
consumer channel; no concrete consumer yet. The re-produce policy consumer channel; no concrete consumer yet. The re-produce policy
sentence is pinned: the hub's ACL governs the final dial — ADR-006 sentence is pinned: the hub's ACL governs the final dial, per ADR-006
Amendment 1) Amendment 1)
- **OQ-TN-13**: UDP truncation (resolved — fail-loud, ADR-003) - **OQ-TN-13**: UDP truncation (resolved, fail-loud, ADR-003)
- **OQ-TN-15/16**: resolved (ADR-007/008 — `open_direct`, - **OQ-TN-15/16**: resolved (ADR-007/008: `open_direct`,
`TunnelListener`, the forwarded op) `TunnelListener`, the forwarded op)
## References ## References
@@ -232,8 +232,8 @@ serving registry of its own.
`ReverseTunnel` open_and_pump / close / join_and_reap; W3, W4) `ReverseTunnel` open_and_pump / close / join_and_reap; W3, W4)
- `docs/research/tunnels-graduation.md` (the graduation research) - `docs/research/tunnels-graduation.md` (the graduation research)
- Upstream asks (review 008): `alkcall/docs/reviews/ - Upstream asks (review 008): `alkcall/docs/reviews/
008-tunnels-graduation-upstream-review.md` — landed in alkcall 008-tunnels-graduation-upstream-review.md`; landed in alkcall
0.8.0 0.8.0
- alkcall ADR-047 §5 (allocation — the initiator adopts), ADR-050 - alkcall ADR-047 §5 (allocation; the initiator adopts), ADR-050
(`pump_bidi`), ledger (`pump_bidi`), ledger
CF-005/CF-006 (identity on the reverse path) CF-005/CF-006 (identity on the reverse path)
@@ -1,8 +1,8 @@
# ADR-001: Open-Op Params Layout — Resource + Substrate # ADR-001: Open-Op Params Layout (Resource + Substrate)
## Status ## Status
Accepted (2026-09-07); amended 2026-09-16 (params layout is per-op — Accepted (2026-09-07); amended 2026-09-16 (params layout is per-op:
the direct op pins its own params, see Amendment 1) the direct op pins its own params, see Amendment 1)
## Context ## Context
@@ -13,7 +13,7 @@ carrier of "what to open." alknet ADR-071 §ALPN table noted
`alknet/tunnel` as `[0, 1]` data in/out only; the addressing scheme `alknet/tunnel` as `[0, 1]` data in/out only; the addressing scheme
was never decided there. Phase 0 resolved the framing of the problem was never decided there. Phase 0 resolved the framing of the problem
(the OQ-TN-01 reframe, 2026-09-05): the rich "remote addressing" (the OQ-TN-01 reframe, 2026-09-05): the rich "remote addressing"
framing was an XY problem — except in the `-D`/dynamic case (which framing was an XY problem. Except in the `-D`/dynamic case (which
composes at the assembly layer), a tunnel is either TCP or UDP, and composes at the assembly layer), a tunnel is either TCP or UDP, and
`params` need only *identify a produced resource*, not carry a `params` need only *identify a produced resource*, not carry a
general-purpose address. general-purpose address.
@@ -21,7 +21,7 @@ general-purpose address.
Constraints that shaped the decision: Constraints that shaped the decision:
- `params` is ALPN-specific JSON, interpreted by the open op's - `params` is ALPN-specific JSON, interpreted by the open op's
establisher/handler — never by the channels layer (alkcall establisher/handler, and never by the channels layer (alkcall
ADR-039). The layout is a one-way door once a consumer exists ADR-039). The layout is a one-way door once a consumer exists
(wire-stable once published; additive changes only). (wire-stable once published; additive changes only).
- The identifier must be substrate-extensible without format changes - The identifier must be substrate-extensible without format changes
@@ -34,7 +34,7 @@ Constraints that shaped the decision:
address. A `host:port` in params would leak the producer's address. A `host:port` in params would leak the producer's
topology into the wire and pin the wire to address-shaped targets. topology into the wire and pin the wire to address-shaped targets.
- alkcall's registry schema-validates the open op's input - alkcall's registry schema-validates the open op's input
(`input_schema` runs before the establisher — review 007 Part B), (`input_schema` runs before the establisher, per review 007 Part B),
so the layout must be schema-describable (no per-substrate so the layout must be schema-describable (no per-substrate
polymorphic payloads in v1). polymorphic payloads in v1).
- Both POCs used exactly this shape (`TunnelParams {resource, - Both POCs used exactly this shape (`TunnelParams {resource,
@@ -52,21 +52,21 @@ JSON object:
} }
``` ```
- **`resource`** (string, required) — the produced resource - **`resource`** (string, required): the produced resource
identifier: the producer's stable name for the tunnel target. NOT identifier: the producer's stable name for the tunnel target. NOT
an address; the producer's registry (an assembly-side construct, an address; the producer's registry (an assembly-side construct,
OQ-TN-11) maps the name to its backing (a local port, a docker OQ-TN-11) maps the name to its backing (a local port, a docker
container's port, an in-process service, a unix socket path). container's port, an in-process service, a unix socket path).
- **`substrate`** (string, required) — the extensible discriminator: - **`substrate`** (string, required): the extensible discriminator:
`"tcp"` | `"udp"` | `"unix"` in v1. A new substrate is a new value, `"tcp"` | `"udp"` | `"unix"` in v1. A new substrate is a new value,
not a format change — the same additive property SSH gets from not a format change, the same additive property SSH gets from
channel-type strings and SOCKS5 gets from ATYP values. The channel-type strings and SOCKS5 gets from ATYP values. The
discriminator also selects the data-plane framing (ADR-003): the discriminator also selects the data-plane framing (ADR-003): the
establisher and both pumps must agree on it, so it rides the open establisher and both pumps must agree on it, so it rides the open
op rather than being inferred. op rather than being inferred.
- The open op's `input_schema` pins both fields as required strings; - The open op's `input_schema` pins both fields as required strings;
`substrate` is enum-validated against the v1 set. Unknown `substrate` is enum-validated against the v1 set. Unknown
substrate values are a schema failure (typed open error) — a substrate values are a schema failure (typed open error): a
consumer speaking a newer substrate to an older producer fails consumer speaking a newer substrate to an older producer fails
loudly, not silently. loudly, not silently.
- Substrate-specific detail (the path of a unix socket, the UDP - Substrate-specific detail (the path of a unix socket, the UDP
@@ -75,12 +75,12 @@ JSON object:
- The open-failure error path rides alkcall ADR-049 (typed - The open-failure error path rides alkcall ADR-049 (typed
`channel:open_failed` with `details.reason` ∈ `dial_failed` / `channel:open_failed` with `details.reason` ∈ `dial_failed` /
`unknown_resource` / `resource_shortage` / `handler_error` / `unknown_resource` / `resource_shortage` / `handler_error` /
`timeout`): an unknown resource name is `unknown_resource` — no `timeout`): an unknown resource name is `unknown_resource`; there is no
params-level error field, no establishment frame (the pre-ADR-049 params-level error field, no establishment frame (the pre-ADR-049
OQ-TN-09 hunch is superseded). OQ-TN-09 hunch is superseded).
Rich in-band addressing (SOCKS5 ATYP, per-datagram remote addresses) Rich in-band addressing (SOCKS5 ATYP, per-datagram remote addresses)
enters only through the `-D`/dynamic-target composition path — it is enters only through the `-D`/dynamic-target composition path; it is
carried INSIDE the tunnel payload by whatever protocol the tunnel carried INSIDE the tunnel payload by whatever protocol the tunnel
carries (socks5, udpgw-style framing), never in base params. carries (socks5, udpgw-style framing), never in base params.
@@ -90,7 +90,7 @@ carries (socks5, udpgw-style framing), never in base params.
renames are a breaking wire change (do not). Adding an optional renames are a breaking wire change (do not). Adding an optional
field is additive-safe; adding a `substrate` value is field is additive-safe; adding a `substrate` value is
consumer-progressive (an older producer rejects with consumer-progressive (an older producer rejects with
`invalid_input`-class schema error — the SSH "unknown channel type" `invalid_input`-class schema error, in the SSH "unknown channel type"
posture, loud not silent). posture, loud not silent).
- **The BAST question is moot for params.** Params is JSON inside the - **The BAST question is moot for params.** Params is JSON inside the
channels open op (which is already a JSON envelope); the data channels open op (which is already a JSON envelope); the data
@@ -101,7 +101,7 @@ carries (socks5, udpgw-style framing), never in base params.
- **Producers own a naming discipline.** Resource ids are - **Producers own a naming discipline.** Resource ids are
producer-scoped names (OQ-TN-11: collision domain = the producer's producer-scoped names (OQ-TN-11: collision domain = the producer's
registry). Consumers learn available resources through the registry). Consumers learn available resources through the
ACL-filtered ops listing (OQ-TN-08) — the spec's ACL-filtered ops listing (OQ-TN-08), so the spec's
`OperationSpec.description` SHOULD carry a human-readable hint `OperationSpec.description` SHOULD carry a human-readable hint
(e.g. "postgres on the worker's tailnet") since it round-trips (e.g. "postgres on the worker's tailnet") since it round-trips
through discovery. through discovery.
@@ -125,10 +125,10 @@ carries (socks5, udpgw-style framing), never in base params.
The `{resource, substrate}` layout pins the `channels/tunnel/sub` open The `{resource, substrate}` layout pins the `channels/tunnel/sub` open
op. It is **per-op, not per-ALPN**: ADR-007 adds the op. It is **per-op, not per-ALPN**: ADR-007 adds the
`channels/tunnel/direct` op on the same `alk/tunnel` ALPN with its own `channels/tunnel/direct` op on the same `alk/tunnel` ALPN with its own
params (`{substrate, target}` — a dynamic target, a different params (`{substrate, target}`, i.e. a dynamic target, a different
capability with its own scope; the resource-not-address invariant is capability with its own scope; the resource-not-address invariant is
unchanged because a dynamic target is not a produced resource). The unchanged because a dynamic target is not a produced resource). The
per-op reading was always latent in the layout's basis — params are per-op reading was always latent in the layout's basis, since params are
"the `input` of the operation" (alkcall ADR-047), and ops are "the `input` of the operation" (alkcall ADR-047), and ops are
per-flavor once flavor-form op ids exist (ADR-002 Amendment 1). per-flavor once flavor-form op ids exist (ADR-002 Amendment 1).
Additive-only evolution per op stands: this amendment adds nothing to Additive-only evolution per op stands: this amendment adds nothing to
@@ -25,7 +25,7 @@ enum) and the ALPN namespace (a published string is wire-stable).
Prior art surveyed in Phase 0 (`ssh-socks5-survey.md`): Prior art surveyed in Phase 0 (`ssh-socks5-survey.md`):
- SSH uses ONE channel mechanism for all forwarding flavors — the - SSH uses ONE channel mechanism for all forwarding flavors; the
channel type string (`direct-tcpip`, `forwarded-tcpip`, channel type string (`direct-tcpip`, `forwarded-tcpip`,
`direct-streamlocal`) is per-open metadata on one transport, not a `direct-streamlocal`) is per-open metadata on one transport, not a
separate transport per flavor. separate transport per flavor.
@@ -35,7 +35,7 @@ Prior art surveyed in Phase 0 (`ssh-socks5-survey.md`):
stream (length-framed datagrams inside a TCP tunnel). stream (length-framed datagrams inside a TCP tunnel).
- With OQ-TN-02 resolved as endpoint-at-open for base UDP resources, - With OQ-TN-02 resolved as endpoint-at-open for base UDP resources,
the substrate discriminator in `params` tells the establisher and the substrate discriminator in `params` tells the establisher and
pumps which framing to expect — exactly option A's shape. The pumps which framing to expect, exactly option A's shape. The
discriminator is load-bearing (it selects the framing, ADR-003), discriminator is load-bearing (it selects the framing, ADR-003),
so it must ride the open op; the ALPN carries no substrate so it must ride the open op; the ALPN carries no substrate
information at all. information at all.
@@ -49,16 +49,16 @@ pump handler, and consumer session type are shared across substrates
session types). session types).
- The open op id is `channels/tunnel/sub` (the `channels/<alpn>/sub` - The open op id is `channels/tunnel/sub` (the `channels/<alpn>/sub`
convention of alkcall ADR-047 — the open op is a `Sub`-typed convention of alkcall ADR-047 (the open op is a `Sub`-typed
operation: the reply carries `channel_id` once, the data plane operation: the reply carries `channel_id` once, the data plane
flows on the channel's `BiStream`). flows on the channel's `BiStream`).
- The channel's ALPN marker on the open-op spec is `alk/tunnel` - The channel's ALPN marker on the open-op spec is `alk/tunnel`
(`ChannelOpenSpec::new("alk/tunnel")`) — the value the (`ChannelOpenSpec::new("alk/tunnel")`, which is the value the
per-ALPN dispatch and the adopted-side manager record for per-ALPN dispatch and the adopted-side manager record for
observability. observability.
- If a future tunnel flavor needs structurally different framing from - If a future tunnel flavor needs structurally different framing from
byte zero with no params-dependent dispatch, that is a NEW ALPN byte zero with no params-dependent dispatch, that is a NEW ALPN
decided by a new ADR before its first consumer — never a change to decided by a new ADR before its first consumer, and never a change to
`alk/tunnel`'s meaning. ALPN strings are wire-stable once `alk/tunnel`'s meaning. ALPN strings are wire-stable once
published. published.
@@ -71,12 +71,12 @@ session types).
`(resource, substrate)`. `(resource, substrate)`.
- **Framing is params-dependent, not ALPN-dependent.** The establisher - **Framing is params-dependent, not ALPN-dependent.** The establisher
validates `substrate` semantically (the registry lookup); the pump validates `substrate` semantically (the registry lookup); the pump
handler is framing-agnostic (raw pass-through halves — the codec handler is framing-agnostic (raw pass-through halves; the codec
lives on the consumer/producer edge for UDP, ADR-003). A mismatched lives on the consumer/producer edge for UDP, ADR-003). A mismatched
`substrate` value fails at schema/establisher time, loudly. `substrate` value fails at schema/establisher time, loudly.
- **ALPN namespace hygiene:** `alk/tunnel` is published here. The - **ALPN namespace hygiene:** `alk/tunnel` is published here. The
`alknet/tunnel` spelling in the alknet ADR-071 table predates the `alknet/tunnel` spelling in the alknet ADR-071 table predates the
`alknet/` → `alk/` prefix swap (alkcall v0.1.1) — docs and code `alknet/` → `alk/` prefix swap (alkcall v0.1.1), so docs and code
must not perpetuate the old prefix. must not perpetuate the old prefix.
## References ## References
@@ -88,20 +88,20 @@ session types).
## Amendment 1 (flavor-form op ids, 2026-09-16) ## Amendment 1 (flavor-form op ids, 2026-09-16)
The decision stands — one ALPN, no ALPN split per flavor. What The decision stands: one ALPN, no ALPN split per flavor. What
changes: the one-ALPN rule is realized as **one ALPN, several changes: the one-ALPN rule is realized as **one ALPN, several
`Sub`-typed open ops**, because ADR-007 (the direct op) and ADR-008 `Sub`-typed open ops**, because ADR-007 (the direct op) and ADR-008
(the forwarded op) are different capabilities sharing the tunnel (the forwarded op) are different capabilities sharing the tunnel
transport — ssh's one-transport-many-channel-types shape. transport, ssh's one-transport-many-channel-types shape.
- The op-id convention extends to flavor form: `channels/<alpn>/sub` - The op-id convention extends to flavor form: `channels/<alpn>/sub`
(the original resource op, reserved and unchanged), (the original resource op, reserved and unchanged),
`channels/<alpn>/direct` (dial an arbitrary target), and `channels/<alpn>/direct` (dial an arbitrary target), and
`channels/<alpn>/forwarded` (accept-as-open toward the listen `channels/<alpn>/forwarded` (accept-as-open toward the listen
opener). New flavors are new op ids — additive; the `…/sub` op is opener). New flavors are new op ids, which is additive; the `…/sub` op is
never reused for a different meaning. never reused for a different meaning.
- The alternative — sibling ALPNs (`alk/tunnel-direct`, - The alternative, sibling ALPNs (`alk/tunnel-direct`,
`alk/tunnel-forwarded`) with standard `…/sub` op ids — was rejected: `alk/tunnel-forwarded`) with standard `…/sub` op ids, was rejected:
it fragments the tunnel namespace per flavor, forces hub relay to it fragments the tunnel namespace per flavor, forces hub relay to
track three ALPNs, and amends this ADR's own new-ALPN justification track three ALPNs, and amends this ADR's own new-ALPN justification
clause (the flavors share framing; none needs structurally different clause (the flavors share framing; none needs structurally different
@@ -109,7 +109,7 @@ transport — ssh's one-transport-many-channel-types shape.
- Upstream coordination: alkcall ADR-047 pins discovery's ALPN - Upstream coordination: alkcall ADR-047 pins discovery's ALPN
derivation to exactly the `…/sub`/`…/pub` suffix shapes derivation to exactly the `…/sub`/`…/pub` suffix shapes
(`derive_alpn_from_op_name`); flavor-form op ids need that (`derive_alpn_from_op_name`); flavor-form op ids need that
derivation (and the hub-relay reconstruction riding it) extended — derivation (and the hub-relay reconstruction riding it) extended; it was
filed upstream filed upstream
(`alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md`, (`alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md`,
ask #1) before the first consumer. Runtime was never blocked: ask #1) before the first consumer. Runtime was never blocked:
@@ -1,11 +1,11 @@
# ADR-003: Data-Plane Codec — Raw Pass-Through (Stream) / Mandatory Length Framing (UDP) # ADR-003: Data-Plane Codec, Raw Pass-Through (Stream) / Mandatory Length Framing (UDP)
## Status ## Status
Accepted (2026-09-07; supersedes the Phase 0 "codec direction" hunch Accepted (2026-09-07; supersedes the Phase 0 "codec direction" hunch
by making the UDP framing mandatory — reverse POC finding F-2). by making the UDP framing mandatory, per reverse POC finding F-2).
Amended (2026-09-08, review 001 U-1 remediation): the substrate- Amended (2026-09-08, review 001 U-1 remediation): the substrate-
boundary placement is pinned mechanically — the `local` feature's boundary placement is pinned mechanically, so the `local` feature's
`UdpHalf` composes the codec at the boundary (channel side framed, `UdpHalf` composes the codec at the boundary (channel side framed,
target side raw), with the pump-window and recv-scratch disciplines target side raw), with the pump-window and recv-scratch disciplines
the composition requires. See §Amendment (2026-09-08). the composition requires. See §Amendment (2026-09-08).
@@ -14,10 +14,10 @@ the composition requires. See §Amendment (2026-09-08).
The tunnel data plane rides a channels data channel: raw bytes inside The tunnel data plane rides a channels data channel: raw bytes inside
the channel `BiStream` (the channels layer strips its 8-byte header the channel `BiStream` (the channels layer strips its 8-byte header
transparently — alknet ADR-093/ADR-071; AGENTS.md convention 5). The transparently, per alknet ADR-093/ADR-071 and AGENTS.md convention 5). The
tunnel protocol owns whatever framing it puts inside the `BiStream`. tunnel protocol owns whatever framing it puts inside the `BiStream`.
A tunnel has one data stream per direction, so there is no A tunnel has one data stream per direction, so there is no
sub-demux key — a 5-byte header like alktty's (with `stream_type`) sub-demux key (a 5-byte header like alktty's, with `stream_type`,
would be pure overhead here. would be pure overhead here.
The Phase 0 direction (`phase-0-findings.md` checklist, 2026-09-06): The Phase 0 direction (`phase-0-findings.md` checklist, 2026-09-06):
@@ -29,14 +29,14 @@ Two validation passes sharpened this:
1. **Forward POC (2026-09-06, 17 tests):** the codec round-trips 1. **Forward POC (2026-09-06, 17 tests):** the codec round-trips
datagrams split across chunks, batched in one chunk, partial datagrams split across chunks, batched in one chunk, partial
headers, and empty datagrams (`len=0` is a legal payload, NOT EOF headers, and empty datagrams (`len=0` is a legal payload, NOT EOF;
— EOF is the channels-level `length=0` chunk sentinel; the two EOF is the channels-level `length=0` chunk sentinel; the two
coexist at different layers). The 1400-byte (max ethernet MTU coexist at different layers). The 1400-byte (max ethernet MTU
payload) datagram rides the bounded-buffer path; >65535 is payload) datagram rides the bounded-buffer path; >65535 is
rejected at frame time. rejected at frame time.
2. **Reverse POC finding F-2 (2026-09-07, executable-pinned):** in 2. **Reverse POC finding F-2 (2026-09-07, executable-pinned):** in
the RAW pass-through pump shape, an empty UDP datagram is a the RAW pass-through pump shape, an empty UDP datagram is a
zero-byte read from the substrate adapter — `tokio::io::copy` zero-byte read from the substrate adapter, and `tokio::io::copy`
treats `Ok(0)` as end-of-stream and shuts the pump down. An empty treats `Ok(0)` as end-of-stream and shuts the pump down. An empty
datagram and the zero-length EOF sentinel are the SAME wire shape datagram and the zero-length EOF sentinel are the SAME wire shape
at the pump level. The test pins the non-round-trip so the at the pump level. The test pins the non-round-trip so the
@@ -49,7 +49,7 @@ The data plane inside a `alk/tunnel` channel is:
- **Stream substrates (`tcp`, `unix`): raw pass-through.** The halves - **Stream substrates (`tcp`, `unix`): raw pass-through.** The halves
are the tunnel; zero tunnel-level framing. The only wire overhead are the tunnel; zero tunnel-level framing. The only wire overhead
is the channels 8-byte chunk header. A zero-length read on a stream is the channels 8-byte chunk header. A zero-length read on a stream
substrate is genuinely EOF (stream semantics) — no collision. Both substrate is genuinely EOF (stream semantics), so there is no collision. Both
pumps run `alkcall::channels::pump_bidi` (ADR-050) over the raw pumps run `alkcall::channels::pump_bidi` (ADR-050) over the raw
halves. halves.
- **Datagram substrate (`udp`): mandatory `[len: u16 BE][datagram]` - **Datagram substrate (`udp`): mandatory `[len: u16 BE][datagram]`
@@ -57,7 +57,7 @@ The data plane inside a `alk/tunnel` channel is:
length-prefixed frame, on BOTH directions: length-prefixed frame, on BOTH directions:
- The **establisher** wraps the dialed UDP socket in the framed - The **establisher** wraps the dialed UDP socket in the framed
adapter BEFORE returning the plan (the plan payload is the adapter BEFORE returning the plan (the plan payload is the
adapter, not a raw socket) — the pump handler stays adapter, not a raw socket), so the pump handler stays
substrate-agnostic (ADR-004's invariant; producer.md's placement). substrate-agnostic (ADR-004's invariant; producer.md's placement).
- Consumer side: the `TunnelSession`'s datagram variant does the - Consumer side: the `TunnelSession`'s datagram variant does the
same over the adopted halves. same over the adopted halves.
@@ -66,29 +66,29 @@ The data plane inside a `alk/tunnel` channel is:
POC). EOF is exclusively the channels-level sentinel; the codec POC). EOF is exclusively the channels-level sentinel; the codec
never emits zero-length reads, so the layers never collide. This never emits zero-length reads, so the layers never collide. This
is the load-bearing property F-2 validated from a second angle: is the load-bearing property F-2 validated from a second angle:
the length prefix makes an empty datagram two bytes — unambiguous the length prefix makes an empty datagram two bytes, which is unambiguous
with EOF. with EOF.
- **MTU discipline:** the codec rejects framing a >65535-byte - **MTU discipline:** the codec rejects framing a >65535-byte
datagram (`Oversize` error at frame time, never a wire overflow — datagram (`Oversize` error at frame time, never a wire overflow;
the u16 length field would wrap). A truncated receive (caller the u16 length field would wrap). A truncated receive (caller
buffer smaller than the datagram) fails loudly at the adapter buffer smaller than the datagram) fails loudly at the adapter
level — resolved 2026-09-07 (OQ-TN-13, fail-loud): silent level, resolved 2026-09-07 (OQ-TN-13, fail-loud): silent
truncation would corrupt the framing invariants (the same truncation would corrupt the framing invariants (the same
invariant class F-2 protects). invariant class F-2 protects).
- **Chunking is transparent.** Datagrams split across channel chunks, - **Chunking is transparent.** Datagrams split across channel chunks,
batch into single chunks, and survive partial headers — the batch into single chunks, and survive partial headers; the
incremental decoder is the only consumer-visible decode path incremental decoder is the only consumer-visible decode path
(forward POC: maximally awkward 7-byte chunk splits verified). (forward POC: maximally awkward 7-byte chunk splits verified).
## Consequences ## Consequences
### Amendment (2026-09-08 — the boundary mechanics, from review 001 U-1's remediation) ### Amendment (2026-09-08, the boundary mechanics from review 001 U-1's remediation)
The placement sentence ("the establisher wraps the dialed UDP socket The placement sentence ("the establisher wraps the dialed UDP socket
in the framed adapter before returning the plan") leaves the adapter's in the framed adapter before returning the plan") leaves the adapter's
*mechanics* open; review 001 found `UdpHalf` shipping as a raw *mechanics* open; review 001 found `UdpHalf` shipping as a raw
boundary-preserving adapter (the codec on one end only — real targets boundary-preserving adapter (the codec on one end only, so real targets
saw the tunnel's length prefixes; empty datagrams collapsed to EOF at saw the tunnel's length prefixes; empty datagrams collapsed to EOF at
the pump's zero-read check; >8 KiB datagrams split at the copy the pump's zero-read check; >8 KiB datagrams split at the copy
buffer). The remediation pins the mechanics: buffer). The remediation pins the mechanics:
@@ -98,29 +98,29 @@ buffer). The remediation pins the mechanics:
sends each decoded payload as one raw datagram; `poll_read` **frames** sends each decoded payload as one raw datagram; `poll_read` **frames**
(`frame_datagram`) each raw target datagram and hands frame bytes to (`frame_datagram`) each raw target datagram and hands frame bytes to
the pump. "Frame on write, de-frame on read" describes the TARGET's the pump. "Frame on write, de-frame on read" describes the TARGET's
face and is the inverted (wrong) reading — the review's original face and is the inverted (wrong) reading; the review's original
fix sketch had it inverted and was corrected in re-validation. fix sketch had it inverted and was corrected in re-validation.
- **The write half accepts all offered bytes and reports them - **The write half accepts all offered bytes and reports them
consumed.** The pump's copy engine re-offers a *full* window consumed.** The pump's copy engine re-offers a *full* window
unsatisfied until it drains (tokio `copy`'s `read_done` path never unsatisfied until it drains (tokio `copy`'s `read_done` path never
tops the window up), so a partial-frame tail could sit forever in a tops the window up), so a partial-frame tail could sit forever in a
full window if the adapter accepted only complete frames — the full window if the adapter accepted only complete frames; the
>8 KiB frame would deadlock the pump. Completed datagrams queue for >8 KiB frame would deadlock the pump. Completed datagrams queue for
the socket; a trailing partial frame waits in the `DatagramReader` the socket; a trailing partial frame waits in the `DatagramReader`
(its state is bounded by construction — the u16 cap, review 001 (its state is bounded by construction, by the u16 cap, review 001
N-2). Backpressure is an accepted-byte high-water mark on N-2). Backpressure is an accepted-byte high-water mark on
`poll_write`, and the socket drain pumps opportunistically on `poll_write`, and the socket drain pumps opportunistically on
write/flush/shutdown (tokio's copy polls flush after every window write/flush/shutdown (tokio's copy polls flush after every window
drain). drain).
- **The recv scratch is full-size (65535) and lazy.** `try_recv` - **The recv scratch is full-size (65535) and lazy.** `try_recv`
truncates SILENTLY at the buffer size — a smaller scratch corrupts truncates SILENTLY at the buffer size, and a smaller scratch corrupts
the framing before any size check could reject it (the review's the framing before any size check could reject it (the review's
"size to the MTU" suggestion is unsafe as written). The 64 KiB "size to the MTU" suggestion is unsafe as written). The 64 KiB
eager allocation is instead lazy (first recv), the U-3 mitigation. eager allocation is instead lazy (first recv), the U-3 mitigation.
- **A >caller-buffer frame is staged, not an error.** The frame's - **A >caller-buffer frame is staged, not an error.** The frame's
length prefix reaches the pump on the first read; the remainder length prefix reaches the pump on the first read; the remainder
hands out across reads. The pump's far-side de-framer reassembles hands out across reads. The pump's far-side de-framer reassembles
the true length — the raw adapter's read-level `InvalidData` is the true length; the raw adapter's read-level `InvalidData` is
replaced by staging (the fail-loud posture now protects the *frame replaced by staging (the fail-loud posture now protects the *frame
stream*, and a wire-level over-bound datagram remains a loud stream*, and a wire-level over-bound datagram remains a loud
`Oversize`-class error at the frame path). `Oversize`-class error at the frame path).
@@ -131,9 +131,9 @@ recording-target suite).
## Consequences (original) ## Consequences (original)
- **TCP/unix tunnels have 0 B tunnel overhead** — the SSH - **TCP/unix tunnels have 0 B tunnel overhead**, matching the SSH
`direct-tcpip` experience (the payload is the bytes). `direct-tcpip` experience (the payload is the bytes).
- **UDP correctness requires the codec** — raw pass-through over UDP - **UDP correctness requires the codec**, since raw pass-through over UDP
is structurally broken for empty datagrams (F-2). The substrate is structurally broken for empty datagrams (F-2). The substrate
discriminator (ADR-001) is what tells both sides to engage the discriminator (ADR-001) is what tells both sides to engage the
codec; a consumer bypassing it for UDP is a spec violation, not a codec; a consumer bypassing it for UDP is a spec violation, not a
@@ -141,8 +141,8 @@ recording-target suite).
- **`tokio::io::copy`-shaped pumps fit UDP only through the framed - **`tokio::io::copy`-shaped pumps fit UDP only through the framed
adapter** (the `UdpHalf` + codec wrapper presents adapter** (the `UdpHalf` + codec wrapper presents
`AsyncRead`/`AsyncWrite` whose zero-length read never occurs for a `AsyncRead`/`AsyncWrite` whose zero-length read never occurs for a
well-formed stream — `len=0` yields a 2-byte wire frame). well-formed stream; `len=0` yields a 2-byte wire frame).
- **The BAST document exists** (AGENTS.md convention 12 — the - **The BAST document exists** (AGENTS.md convention 12; the
mandatory framing IS binary framing beyond pass-through, so the mandatory framing IS binary framing beyond pass-through, so the
trigger fired): `docs/architecture/bast.md` describes the trigger fired): `docs/architecture/bast.md` describes the
`[len: u16 BE]` datagram frame per the BAST meta-schema; the `[len: u16 BE]` datagram frame per the BAST meta-schema; the
@@ -160,6 +160,6 @@ recording-target suite).
sentinel layering + MTU); reverse POC: sentinel layering + MTU); reverse POC:
`docs/research/reverse-poc-summary.md` §F-2 (the mandate) `docs/research/reverse-poc-summary.md` §F-2 (the mandate)
- `docs/research/ssh-socks5-survey.md` §UDP (tun2proxy udpgw framing - `docs/research/ssh-socks5-survey.md` §UDP (tun2proxy udpgw framing
precedent — no CONN_ID, no FLAGS; boundary framing only) precedent (no CONN_ID, no FLAGS; boundary framing only)
- alknet ADR-071/093 (channels wire format; the zero-length sentinel) - alknet ADR-071/093 (channels wire format; the zero-length sentinel)
- alkcall `channels::pump_bidi` (ADR-050) - alkcall `channels::pump_bidi` (ADR-050)
@@ -1,4 +1,4 @@
# ADR-004: No TunnelBackend Trait — Halves Functions at the Assembly Layer # ADR-004: No TunnelBackend Trait (Halves Functions at the Assembly Layer)
## Status ## Status
@@ -8,8 +8,8 @@ re-produce thread)
## Context ## Context
alktty has a `TtyBackend` trait because backends (local PTY, docker, alktty has a `TtyBackend` trait because backends (local PTY, docker,
SSH) produce complex handles — `TtyHandle` carries SSH) produce complex handles: `TtyHandle` carries
stdin/stdout/stderr/exit-code quadruple + control — and the adapter stdin/stdout/stderr/exit-code quadruple + control, and the adapter
pumps them. The Phase 0 question (OQ-TN-05): does alktunnels need a pumps them. The Phase 0 question (OQ-TN-05): does alktunnels need a
`TunnelBackend` trait, or is the two-pump handler + feature-gated `TunnelBackend` trait, or is the two-pump handler + feature-gated
substrate modules the whole story? substrate modules the whole story?
@@ -24,7 +24,7 @@ backend's:
connection is the same boxed-halves shape. connection is the same boxed-halves shape.
The UDP POC gave the executable answer for dial flows (2026-09-06): The UDP POC gave the executable answer for dial flows (2026-09-06):
`pump_halves` is generic over boxed `(AsyncRead, AsyncWrite)` halves — `pump_halves` is generic over boxed `(AsyncRead, AsyncWrite)` halves:
TCP contributes `into_split()` halves, UDP contributes the `UdpHalf` TCP contributes `into_split()` halves, UDP contributes the `UdpHalf`
socket adapter, the pump never knows which. "Produce boxed halves for socket adapter, the pump never knows which. "Produce boxed halves for
a resource" is a function, not a trait. a resource" is a function, not a trait.
@@ -33,7 +33,7 @@ The one remaining thread was the hub re-produce composition (does a
hub proxy that re-produces a consumed resource need a trait or a hub proxy that re-produces a consumed resource need a trait or a
composition helper?). The reverse POC (2026-09-07) resolved the composition helper?). The reverse POC (2026-09-07) resolved the
mechanics: a hub proxy is a consumer + producer composed at the mechanics: a hub proxy is a consumer + producer composed at the
assembly layer — the reverse POC's hub is exactly this shape (it assembly layer; the reverse POC's hub is exactly this shape (it
consumes a channels connection and produces reverse-tunnel opens consumes a channels connection and produces reverse-tunnel opens
toward the worker); re-exporting (registering an openable for a toward the worker); re-exporting (registering an openable for a
resource the hub itself consumes) would add a resource the hub itself consumes) would add a
@@ -51,7 +51,7 @@ assembly layer:
- **The produce function.** The producer's establisher resolves - **The produce function.** The producer's establisher resolves
`(resource, substrate)` → boxed halves `(resource, substrate)` → boxed halves
(`Box<dyn AsyncRead + Send + Sync + Unpin>` ×2 — the `+ Sync` is (`Box<dyn AsyncRead + Send + Sync + Unpin>` ×2; the `+ Sync` is
the `ChannelPlan` bound, reverse POC F-1) or a framed UDP adapter. the `ChannelPlan` bound, reverse POC F-1) or a framed UDP adapter.
The dial itself is substrate code living in feature-gated backend The dial itself is substrate code living in feature-gated backend
modules (`local` feature: real sockets), injected at assembly: modules (`local` feature: real sockets), injected at assembly:
@@ -64,12 +64,12 @@ assembly layer:
open op; its establisher pops the next accepted connection from an open op; its establisher pops the next accepted connection from an
assembly-owned listener queue and returns it as the plan payload. assembly-owned listener queue and returns it as the plan payload.
The listener lifecycle (bind, accept loop, cancel) is assembly The listener lifecycle (bind, accept loop, cancel) is assembly
code — the protocol never binds (OQ-TN-04). The reverse POC's code; the protocol never binds (OQ-TN-04). The reverse POC's
worker (dial) and the forward POC's producer (dial) both rode this worker (dial) and the forward POC's producer (dial) both rode this
shape; the listen establisher is the same plan-flow with a shape; the listen establisher is the same plan-flow with a
different halves source. different halves source.
- **The pump handler is substrate-agnostic by construction:** - **The pump handler is substrate-agnostic by construction:**
`pump_bidi(bidi, t_read, t_write)` over the downcast plan payload — `pump_bidi(bidi, t_read, t_write)` over the downcast plan payload;
it cannot know whether the halves came from TCP, UDP, a unix it cannot know whether the halves came from TCP, UDP, a unix
socket, or an in-process pipe, and must not. socket, or an in-process pipe, and must not.
- **Datagram substrates** contribute a framed adapter (ADR-003) - **Datagram substrates** contribute a framed adapter (ADR-003)
@@ -78,9 +78,9 @@ assembly layer:
every assembly layer re-implements the same glue (registry + dial + every assembly layer re-implements the same glue (registry + dial +
establisher construction + pump wiring) three or more times, extract establisher construction + pump wiring) three or more times, extract
a `TunnelBackend`-shaped trait THEN (the alknet ADR-078 a `TunnelBackend`-shaped trait THEN (the alknet ADR-078
convergence-test threshold — extract when the shapes have converged convergence-test threshold: extract when the shapes have converged
across consumers, not before) across consumers, not before)
— as an additive layer, not a v1 wire/ABI commitment. Until then and do it as an additive layer, not a v1 wire/ABI commitment. Until then
the function-plus-closure shape keeps the crate dependency-light the function-plus-closure shape keeps the crate dependency-light
and wasm-clean. and wasm-clean.
@@ -89,14 +89,14 @@ assembly layer:
- **The default crate stays wasm-clean** with no socket/platform - **The default crate stays wasm-clean** with no socket/platform
deps; `local` (sockets) and future backends (docker, process) are deps; `local` (sockets) and future backends (docker, process) are
feature-gated modules implementing dial/listen functions. feature-gated modules implementing dial/listen functions.
- **Assembly layers write more glue than a trait would save** — the - **Assembly layers write more glue than a trait would save**, which is
accepted cost, validated twice (both POCs' producers are ~200 lines the accepted cost, validated twice (both POCs' producers are ~200 lines
including the UDP adapter). including the UDP adapter).
- **Plan payloads are `Send + Sync`** (reverse POC F-1, documented on - **Plan payloads are `Send + Sync`** (reverse POC F-1, documented on
alkcall's `ChannelPlan` type in 0.7.0): socket-backed handles carry alkcall's `ChannelPlan` type in 0.7.0): socket-backed handles carry
`+ Sync` naturally; non-Sync handles (process pipes as boxed trait `+ Sync` naturally; non-Sync handles (process pipes as boxed trait
objects) need a wrapper. The spec documents the constraint. objects) need a wrapper. The spec documents the constraint.
- **The listen variant needs no new wire surface** — same open op, - **The listen variant needs no new wire surface**: same open op,
same params, same typed errors (an empty listener queue during same params, same typed errors (an empty listener queue during
establishment is `resource_shortage`; a closed listener is establishment is `resource_shortage`; a closed listener is
`dial_failed`-class). `dial_failed`-class).
@@ -108,8 +108,8 @@ assembly layer:
- Forward POC: `docs/research/poc-summary.md` §5 (halves-not-trait); - Forward POC: `docs/research/poc-summary.md` §5 (halves-not-trait);
reverse POC: `docs/research/reverse-poc-summary.md` (the hub shape, reverse POC: `docs/research/reverse-poc-summary.md` (the hub shape,
F-1) F-1)
- alktty ADR-002 (`TtyBackend` — the contrast case: when a trait IS - alktty ADR-002 (`TtyBackend`, the contrast case: when a trait IS
warranted), alktty ADR-003 (backend placement — the feature-gate warranted), alktty ADR-003 (backend placement, the feature-gate
pattern) pattern)
- AGENTS.md conventions 4/7/14 (wasm-clean, substrate-agnostic, - AGENTS.md conventions 4/7/14 (wasm-clean, substrate-agnostic,
feature flags) feature flags)
@@ -4,7 +4,7 @@
Accepted (2026-09-07; resolves the reverse POC W3 finding). Accepted (2026-09-07; resolves the reverse POC W3 finding).
Amended (2026-09-08, review 001 N-3 decision): the datagram Amended (2026-09-08, review 001 N-3 decision): the datagram
`take_halves` posture is pinned — raw framed halves stay the only `take_halves` posture is pinned: raw framed halves stay the only
owned-halves shape (`take_halves` keeps its semantics for every owned-halves shape (`take_halves` keeps its semantics for every
substrate), the datagram accessors remain the codec path, and a substrate), the datagram accessors remain the codec path, and a
framed-halves convenience is deferred to the first consumer that framed-halves convenience is deferred to the first consumer that
@@ -17,23 +17,23 @@ A tunnel channel's two ends have asymmetric lifecycle machinery:
- **The serving side (the side the open op ran on)** is - **The serving side (the side the open op ran on)** is
wrapper-managed: the open wrapper awaits the pump handler's wrapper-managed: the open wrapper awaits the pump handler's
`JoinHandle`, and its completion triggers channel teardown (drop of `JoinHandle`, and its completion triggers channel teardown (drop of
the demux sender = EOF to the handler's read half — alkcall ADR-049 the demux sender = EOF to the handler's read half, per alkcall ADR-049
+ review 007 R-02). Out-of-band `channel/close` also tears it down. + review 007 R-02). Out-of-band `channel/close` also tears it down.
Nothing leaks. Nothing leaks.
- **The adopting side (the side that called the open op and adopted - **The adopting side (the side that called the open op and adopted
the channel ID — the consumer)** has NO such machinery: the channel ID, i.e. the consumer)** has NO such machinery:
`ChannelManager::adopt_channel` installs routing state nothing `ChannelManager::adopt_channel` installs routing state nothing
awaits. The consumer-side pump (`pump_bidi` spawned locally) is awaits. The consumer-side pump (`pump_bidi` spawned locally) is
somebody's `JoinHandle`; the adopted channel state is nobody's to somebody's `JoinHandle`; the adopted channel state is nobody's to
reap. If the assembly layer drops both, the channel entry leaks in reap. If the assembly layer drops both, the channel entry leaks in
the manager until transport EOF (`clear_all`) — and worse, a the manager until transport EOF (`clear_all`); and worse, a
consumer that drops the pump handle mid-flight aborts the pump consumer that drops the pump handle mid-flight aborts the pump
without reaping, leaving half-open state on the peer. without reaping, leaving half-open state on the peer.
The reverse POC found this by construction (W3, 2026-09-07): the hub The reverse POC found this by construction (W3, 2026-09-07): the hub
must hold its pump handle and reap (`teardown_channel`) itself — must hold its pump handle and reap (`teardown_channel`) itself.
`ReverseTunnel::join_and_reap`/`close` in the POC. The lifecycle is `ReverseTunnel::join_and_reap`/`close` in the POC. The lifecycle is
assembly-layer by design (OQ-TN-04 — no forced binding means no assembly-layer by design (OQ-TN-04: no forced binding means no
protocol-owned listener lifecycle either), but the POC demonstrated protocol-owned listener lifecycle either), but the POC demonstrated
that leaving teardown discipline to each assembly layer's memory is that leaving teardown discipline to each assembly layer's memory is
exactly the kind of gap the spec must close with a type, not a doc exactly the kind of gap the spec must close with a type, not a doc
@@ -49,31 +49,31 @@ plane (raw halves vs datagram codec, ADR-003).
The consumer half is a typed session, **`TunnelSession`**, and it owns The consumer half is a typed session, **`TunnelSession`**, and it owns
its teardown: its teardown:
- **Construction:** `TunnelSession::open(client, params)` — calls the - **Construction:** `TunnelSession::open(client, params)`: calls the
open op (`ChannelClient::open_channel` on the forward path; the open op (`ChannelClient::open_channel` on the forward path; the
reverse path's `open_reverse_channel` + `adopt_channel` equivalent), reverse path's `open_reverse_channel` + `adopt_channel` equivalent),
adopts the returned channel ID, splits the channel `BiStream`, and adopts the returned channel ID, splits the channel `BiStream`, and
presents the substrate-shaped data plane: presents the substrate-shaped data plane:
- **Stream variant:** raw halves (`AsyncRead`/`AsyncWrite`) — the - **Stream variant:** raw halves (`AsyncRead`/`AsyncWrite`); the
halves ARE the tunnel. halves ARE the tunnel.
- **Datagram variant:** `send_datagram`/`recv_datagram` over the - **Datagram variant:** `send_datagram`/`recv_datagram` over the
mandatory codec (ADR-003) — boundary-preserving, `len=0` is an mandatory codec (ADR-003), which is boundary-preserving; `len=0` is an
empty datagram, `recv_datagram` returns `None` only on stream EOF. empty datagram, `recv_datagram` returns `None` only on stream EOF.
- **Pump ownership:** the session's `pump_against(accepted_halves)` - **Pump ownership:** the session's `pump_against(accepted_halves)`
(the reverse-flow use) spawns `pump_bidi` and holds the returned (the reverse-flow use) spawns `pump_bidi` and holds the returned
`JoinHandle`. For the pure consumer (no local pump — the session `JoinHandle`. For the pure consumer (no local pump; the session
halves ARE handed to the caller), the session does not spawn; the halves ARE handed to the caller), the session does not spawn; the
caller drives the halves and the session still owns the channel caller drives the halves and the session still owns the channel
entry. entry.
- **Teardown API (the point of the ADR):** - **Teardown API (the point of the ADR):**
- `close(self)` — abort the pump (if session-owned), tear down the - `close(self)`: abort the pump (if session-owned), tear down the
adopted channel (`teardown_channel`), consume the session. The adopted channel (`teardown_channel`), consume the session. The
abort path (ungraceful). abort path (ungraceful).
- `join(self)` — await pump completion (both directions finished), - `join(self)`: await pump completion (both directions finished),
THEN reap the adopted channel, return the `(u64, u64)` copy THEN reap the adopted channel, return the `(u64, u64)` copy
counts from `pump_bidi` for observability. The graceful path. counts from `pump_bidi` for observability. The graceful path.
- Dropping the session without either: the `Drop` impl tears the - Dropping the session without either: the `Drop` impl tears the
channel down (the panic-free fallback — never leak the entry). channel down (the panic-free fallback, never leaking the entry).
`Drop` cannot await, so it aborts the pump and calls the `Drop` cannot await, so it aborts the pump and calls the
sync `teardown_channel`; this is semantically `close`. sync `teardown_channel`; this is semantically `close`.
- **Half-open semantics fall out of `pump_bidi`** (ADR-050): one - **Half-open semantics fall out of `pump_bidi`** (ADR-050): one
@@ -81,7 +81,7 @@ its teardown:
crosses the mux) → the other pump keeps running until its own EOF. crosses the mux) → the other pump keeps running until its own EOF.
The session's `join` completes when BOTH pumps finish. Half-close The session's `join` completes when BOTH pumps finish. Half-close
semantics validated end-to-end (reverse POC W4). semantics validated end-to-end (reverse POC W4).
- **Error surface:** a failed open resolves a typed error — - **Error surface:** a failed open resolves a typed error:
`ChannelOpenError::CallFailed` carrying the wire `CallError`; `ChannelOpenError::CallFailed` carrying the wire `CallError`;
branch on `establishment_reason()` for `channel:open_failed`'s branch on `establishment_reason()` for `channel:open_failed`'s
reason (ADR-049 §4). A failed open never yields a session (no reason (ADR-049 §4). A failed open never yields a session (no
@@ -92,19 +92,19 @@ its teardown:
## Consequences ## Consequences
### Amendment (2026-09-08 — the N-3 datagram `take_halves` posture, decided before the first external consumer) ### Amendment (2026-09-08, the N-3 datagram `take_halves` posture, decided before the first external consumer)
Review 001 N-3: a datagram session exposes codec-applied accessors Review 001 N-3: a datagram session exposes codec-applied accessors
(`send_datagram`/`recv_datagram`) until `take_halves` — then the (`send_datagram`/`recv_datagram`) until `take_halves`, at which point the
caller owns RAW halves whose wire bytes carry the `[len: u16 BE]` caller owns RAW halves whose wire bytes carry the `[len: u16 BE]`
framing. Documented, but a footgun asymmetry the API window framing. Documented, but a footgun asymmetry the API window
(this ADR's surface becomes ABI-stable at the first external (this ADR's surface becomes ABI-stable at the first external
consumer) forced a decision on. Decided: **option 1 — keep as-is, consumer) forced a decision on. Decided: **option 1 (keep as-is,
carried by docs**, with the rationale: carried by docs)**, with the rationale:
- **No external consumer exists** (verified 2026-09-08 — no crate in - **No external consumer exists** (verified 2026-09-08; no crate in
the workspace depends on alktunnels); the window is open, so a the workspace depends on alktunnels); the window is open, so a
breaking reshape was *available* — but nothing yet demonstrates a breaking reshape was *available*, but nothing yet demonstrates a
need for a different shape. need for a different shape.
- **The framing asymmetry is ADR-003's spec, not drift.** The - **The framing asymmetry is ADR-003's spec, not drift.** The
datagram data plane is mandatory-framing by substrate datagram data plane is mandatory-framing by substrate
@@ -113,23 +113,23 @@ carried by docs**, with the rationale:
boundary." `take_halves` yielding raw halves keeps ONE semantic boundary." `take_halves` yielding raw halves keeps ONE semantic
across substrates (the tunnel's wire bytes, uninterpreted); a across substrates (the tunnel's wire bytes, uninterpreted); a
per-substrate `take_halves` (option 3) diverges semantics by per-substrate `take_halves` (option 3) diverges semantics by
substrate — the worse confusion at the type level even as it substrate (the worse confusion at the type level even as it
removes the byte-level footgun. removes the byte-level footgun.
- **`pump_against` needs the framed adapter, not framed halves.** The - **`pump_against` needs the framed adapter, not framed halves.** The
adjacent seam (N-11, resolved in-review with the doc note): a UDP adjacent seam (N-11, resolved in-review with the doc note): a UDP
session pumped against an accepted local half requires the FRAMED session pumped against an accepted local half requires the FRAMED
adapter on the accepted side — the `local` feature's `UdpHalf` (or adapter on the accepted side, namely the `local` feature's `UdpHalf` (or
an assembly equivalent), not a raw socket. What composes there is an assembly equivalent), not a raw socket. What composes there is
the adapter AT the boundary, not a session-side wrapper pair; a the adapter AT the boundary, not a session-side wrapper pair; a
`take_framed_halves` convenience would not feed this use. `take_framed_halves` convenience would not feed this use.
- **The convenience is additive and can land later.** Option 2's - **The convenience is additive and can land later.** Option 2's
`take_framed_halves` breaks nothing when added after the first `take_framed_halves` breaks nothing when added after the first
consumer (unlike a `take_halves` reshape), so its timing is consumer (unlike a `take_halves` reshape), so its timing is
decoupled from this window — deferring it costs nothing now, and decoupled from this window, so deferring it costs nothing now, and
the first real consumer's reach defines its shape (a the first real consumer's reach defines its shape (a
`DatagramReader`-wrapped pair has an open design question: an `DatagramReader`-wrapped pair has an open design question: an
`AsyncRead` face re-collides with the F-2 empty-datagram/EOF `AsyncRead` face re-collides with the F-2 empty-datagram/EOF
ambiguity the codec exists to prevent — the codec-side framing ambiguity the codec exists to prevent: the codec-side framing
wants accessor-shaped surface, which the session already has). wants accessor-shaped surface, which the session already has).
- **The docs carry the contract.** `take_halves`'s doc states the - **The docs carry the contract.** `take_halves`'s doc states the
framing (landed with the implementation); consumer.md's datagram framing (landed with the implementation); consumer.md's datagram
@@ -138,14 +138,14 @@ carried by docs**, with the rationale:
**Consequence:** datagram callers wanting codec-applied I/O after **Consequence:** datagram callers wanting codec-applied I/O after
taking ownership should keep the session (drive `send_datagram`/ taking ownership should keep the session (drive `send_datagram`/
`recv_datagram`; the session IS the owner) or compose `UdpHalf` at `recv_datagram`; the session IS the owner) or compose `UdpHalf` at
their own boundary — `take_halves` is the raw-wire escape hatch and their own boundary; `take_halves` is the raw-wire escape hatch and
stays documented as such. Revisit only on a demonstrated consumer stays documented as such. Revisit only on a demonstrated consumer
need (file an OQ at that point). need (file an OQ at that point).
### Consequences (original) ### Consequences (original)
- **Assembly layers cannot leak adopted channels** by forgetting to - **Assembly layers cannot leak adopted channels** by forgetting to
reap — the type is the discipline (the W3 gap closes structurally). reap; the type is the discipline (the W3 gap closes structurally).
- **The reverse-flow initiator gets the same session shape** as the - **The reverse-flow initiator gets the same session shape** as the
forward consumer: the reverse POC's hub-side `ReverseTunnel` is the forward consumer: the reverse POC's hub-side `ReverseTunnel` is the
seed; the spec generalizes it so `-L` and `-R` consumers share one seed; the spec generalizes it so `-L` and `-R` consumers share one
@@ -156,7 +156,7 @@ need (file an OQ at that point).
already wrapper-managed (R-02); the session only fixes the adopter already wrapper-managed (R-02); the session only fixes the adopter
asymmetry. asymmetry.
- **`Drop`-based teardown is best-effort** (abort + sync reap, no - **`Drop`-based teardown is best-effort** (abort + sync reap, no
await) — the documented contract; graceful flows call `join` or await): the documented contract; graceful flows call `join` or
`close` explicitly. `close` explicitly.
## References ## References
@@ -165,7 +165,7 @@ need (file an OQ at that point).
finding), §W4 (half-close validation) finding), §W4 (half-close validation)
- OQ-TN-03, OQ-TN-04, OQ-TN-09 (promoted) - OQ-TN-03, OQ-TN-04, OQ-TN-09 (promoted)
- alkcall ADR-049 + review 007 R-02 (wrapper-managed serving side), - alkcall ADR-049 + review 007 R-02 (wrapper-managed serving side),
ADR-050 (`pump_bidi` + copy counts), ADR-047 §5 (allocation — the ADR-050 (`pump_bidi` + copy counts), ADR-047 §5 (allocation; the
initiator adopts) initiator adopts)
- AGENTS.md convention 10 (limits inherited — the session adds no - AGENTS.md convention 10 (limits inherited; the session adds no
second bookkeeping layer; it owns exactly one channel's lifecycle) second bookkeeping layer; it owns exactly one channel's lifecycle)
@@ -1,8 +1,8 @@
# ADR-006: Access-Control Posture — the Open Gate Is the Security Boundary # ADR-006: Access-Control Posture: the Open Gate Is the Security Boundary
## Status ## Status
Accepted (2026-09-07; records the OQ-TN-08 resolution as a decision — Accepted (2026-09-07; records the OQ-TN-08 resolution as a decision;
the posture was resolved in Phase 0 but the consumer-visible constants the posture was resolved in Phase 0 but the consumer-visible constants
had no ADR backing); amended 2026-09-16 (scope separation for the had no ADR backing); amended 2026-09-16 (scope separation for the
direct/forwarded ops + the hub re-produce policy sentence, see direct/forwarded ops + the hub re-produce policy sentence, see
@@ -11,7 +11,7 @@ Amendment 1)
## Context ## Context
Tunnels reach local networks: opening a tunnel channel instructs the Tunnels reach local networks: opening a tunnel channel instructs the
producer to dial (or accept for) a target — potentially anything the producer to dial (or accept for) a target, potentially anything the
producer's host can reach. The open gate is therefore the security producer's host can reach. The open gate is therefore the security
boundary of this crate, in the same sense that SSH's boundary of this crate, in the same sense that SSH's
`AllowTcpForwarding`/`PermitOpen` govern `direct-tcpip` opens. The `AllowTcpForwarding`/`PermitOpen` govern `direct-tcpip` opens. The
@@ -20,13 +20,13 @@ this crate vs alkcall's op-level ACL vs the assembly layer.
Phase 0 resolved the conceptual tangle (2026-09-05, the OQ-TN-08 Phase 0 resolved the conceptual tangle (2026-09-05, the OQ-TN-08
resolution): at protocol-crate level, a produced resource belongs to resolution): at protocol-crate level, a produced resource belongs to
the far side of the connection — the protocol works under that the far side of the connection, and the protocol works under that
assumption, so the ACL story is exactly alkcall's existing op-level assumption, so the ACL story is exactly alkcall's existing op-level
ACL. No new policy layer. The hub/overlay mechanism (workers ACL. No new policy layer. The hub/overlay mechanism (workers
connecting in, the hub re-exposing resources per ITS policy) is a connecting in, the hub re-exposing resources per ITS policy) is a
downstream, assembly-layer concern (OQ-TN-12's scope). downstream, assembly-layer concern (OQ-TN-12's scope).
What was left implicit — and needs pinning, because it is What was left implicit, and needs pinning because it is
consumer-visible and stable-once-published: consumer-visible and stable-once-published:
- The scope constant governing tunnel opens (`tunnel:open`) is - The scope constant governing tunnel opens (`tunnel:open`) is
@@ -39,10 +39,10 @@ consumer-visible and stable-once-published:
## Decision ## Decision
1. **The open gate is the boundary, and it is alkcall's ACL.** The 1. **The open gate is the boundary, and it is alkcall's ACL.** The
open op's `AccessControl` rides `ChannelCore::register_openable` — open op's `AccessControl` rides `ChannelCore::register_openable`;
the registry runs the ACL before the establisher. The base gate is the registry runs the ACL before the establisher. The base gate is
the scope constant **`TUNNEL_OPEN_SCOPE = "tunnel:open"`** the scope constant **`TUNNEL_OPEN_SCOPE = "tunnel:open"`**
(required scope, exact string — stable once published; consumers (required scope, exact string; stable once published, and consumers
will request grants by it). An identity without the scope is will request grants by it). An identity without the scope is
`FORBIDDEN` before any establisher code runs. `FORBIDDEN` before any establisher code runs.
2. **The scope string is wire/ACL-stable** from the first consumer: 2. **The scope string is wire/ACL-stable** from the first consumer:
@@ -51,7 +51,7 @@ consumer-visible and stable-once-published:
3. **No target allowlists and no tunnel-specific policy machinery in 3. **No target allowlists and no tunnel-specific policy machinery in
v1.** The resource is owned by the far side (the Phase 0 v1.** The resource is owned by the far side (the Phase 0
resolution); a producer that wants finer granularity wires resolution); a producer that wants finer granularity wires
`OwnershipProvider` checks into ITS establisher — the protocol `OwnershipProvider` checks into ITS establisher; the protocol
provides the seam (the establisher sees the per-call opener provides the seam (the establisher sees the per-call opener
identity, alkcall 0.7.0 CF-006), not the policy. This mirrors identity, alkcall 0.7.0 CF-006), not the policy. This mirrors
alktty's posture (`TTY_OPEN_SCOPE` + op-level ACL, no alktty's posture (`TTY_OPEN_SCOPE` + op-level ACL, no
@@ -80,17 +80,17 @@ consumer-visible and stable-once-published:
## Consequences ## Consequences
- **One security knob per producer deployment** (the op-level ACL + - **One security knob per producer deployment** (the op-level ACL +
scope grants) instead of a tunnel-specific policy language — the scope grants) instead of a tunnel-specific policy language, which is
accepted simplicity; per-resource granularity is an establisher the accepted simplicity; per-resource granularity is an establisher
customization (the seam), not a v1 surface. customization (the seam), not a v1 surface.
- **The scope constant is a compatibility commitment:** treat - **The scope constant is a compatibility commitment:** treat
`tunnel:open` like the ALPN string. Changing it after consumers `tunnel:open` like the ALPN string. Changing it after consumers
request grants is a breaking ACL change. request grants is a breaking ACL change.
- **No allowlist concept** — a wildcard-egress producer - **No allowlist concept**: a wildcard-egress producer
(`0.0.0.0/0`-style egress) is expressed by registering a resource (`0.0.0.0/0`-style egress) is expressed by registering a resource
whose backing is that egress; gating it is the assembly layer's whose backing is that egress; gating it is the assembly layer's
ownership check at the seam. The protocol stays out of the business. ownership check at the seam. The protocol stays out of the business.
- **Tunnels reach local networks** — producers MUST treat the gate as - **Tunnels reach local networks**, so producers MUST treat the gate as
the security boundary; the spec docs state this posture (producer.md). the security boundary; the spec docs state this posture (producer.md).
## References ## References
@@ -99,7 +99,7 @@ consumer-visible and stable-once-published:
- alkcall ADR-047 (open ops + ACL wiring), ADR-017 §7 (the - alkcall ADR-047 (open ops + ACL wiring), ADR-017 §7 (the
`auth_token` path), ledger CF-005/CF-006 (identity precedence, `auth_token` path), ledger CF-005/CF-006 (identity precedence,
per-call opener); alknet ADR-024/ADR-050 (registry layering, per-call opener); alknet ADR-024/ADR-050 (registry layering,
ownership — the alkcall ports ADR-019/ADR-011) ownership; the alkcall ports ADR-019/ADR-011)
- alktty's `TTY_OPEN_SCOPE` posture (the sibling precedent) - alktty's `TTY_OPEN_SCOPE` posture (the sibling precedent)
- AGENTS.md convention 13 (access control) - AGENTS.md convention 13 (access control)
@@ -109,19 +109,19 @@ The posture stands; the scope space grows with the ops (ADR-007,
ADR-008): ADR-008):
- **`TUNNEL_DIRECT_SCOPE = "tunnel:direct"`** gates - **`TUNNEL_DIRECT_SCOPE = "tunnel:direct"`** gates
`channels/tunnel/direct` — dynamic-target egress. **Never implied by `channels/tunnel/direct` for dynamic-target egress. **Never implied by
`tunnel:open`**: "dial `postgres-primary`" and "dial anything from `tunnel:open`**: "dial `postgres-primary`" and "dial anything from
my network" are different capabilities, and the separation is my network" are different capabilities, and the separation is
structural (scopes attach to op specs, not params). A deployment structural (scopes attach to op specs, not params). A deployment
granting both grants both explicitly. granting both grants both explicitly.
- **`TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"`** gates - **`TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"`** gates
`channels/tunnel/forwarded` on the opener side — the grant for `channels/tunnel/forwarded` on the opener side; it is the grant for
RECEIVING producer-initiated forwarded connections (ingress). RECEIVING producer-initiated forwarded connections (ingress).
Ingress and egress are different grants (the alksocks POC #5 Ingress and egress are different grants (the alksocks POC #5
`socks5:bind` gate proved the distinction matters in practice). `socks5:bind` gate proved the distinction matters in practice).
- **Hub re-produce policy sentence (the security model):** a hub that - **Hub re-produce policy sentence (the security model):** a hub that
re-produces the direct op applies ITS OWN target policy at the re-produces the direct op applies ITS OWN target policy at the
re-produce hop — the hub's ACL, not the original opener's, governs re-produce hop; the hub's ACL, not the original opener's, governs
the final dial. The hub is the network boundary; the original the final dial. The hub is the network boundary; the original
opener's grant gets it to the hub, the hub's own grant decides what opener's grant gets it to the hub, the hub's own grant decides what
it will dial on the far side. Correct-by-default; pinned here it will dial on the far side. Correct-by-default; pinned here
@@ -1,4 +1,4 @@
# ADR-007: The Direct Open Op — Dynamic-Target Egress # ADR-007: The Direct Open Op (Dynamic-Target Egress)
## Status ## Status
@@ -11,7 +11,7 @@ the backing; rich addressing rides the `-D`/dynamic composition path,
never the wire. That decision stands. The use case that justifies an never the wire. That decision stands. The use case that justifies an
*additive* revisit is the one the alksocks graduation research *additive* revisit is the one the alksocks graduation research
(`docs/research/tunnels-graduation.md`) named: a consumer asking the (`docs/research/tunnels-graduation.md`) named: a consumer asking the
producer to dial an arbitrary target chosen at session time — ssh's producer to dial an arbitrary target chosen at session time, i.e. ssh's
`direct-tcpip` shape, SOCKS5 CONNECT's egress, and the vpn-like `direct-tcpip` shape, SOCKS5 CONNECT's egress, and the vpn-like
dial-callback translation all need it. A dynamic target is a different dial-callback translation all need it. A dynamic target is a different
capability from opening a named resource, and ssh carries it as its own capability from opening a named resource, and ssh carries it as its own
@@ -19,7 +19,7 @@ channel type precisely because the two capabilities must not share one
grant. grant.
The capability is dangerous in exactly the way ADR-001's "resource, not The capability is dangerous in exactly the way ADR-001's "resource, not
address" framing protects against — arbitrary egress from the address" framing protects against: arbitrary egress from the
producer's network. The mitigation is not refusing the capability (a producer's network. The mitigation is not refusing the capability (a
SOCKS5 service IS the capability) but separating it at the ACL: "dial SOCKS5 service IS the capability) but separating it at the ACL: "dial
`postgres-primary`" and "dial anything" must not be the same grant. `postgres-primary`" and "dial anything" must not be the same grant.
@@ -27,7 +27,7 @@ The alksocks POC #5 scope gate (`socks5:bind`) proved the pattern
matters in practice. matters in practice.
Timing: alksocks is the first real consumer and nothing is published Timing: alksocks is the first real consumer and nothing is published
against the direct shape yet — this is the correct moment to pin the against the direct shape yet, so this is the correct moment to pin the
wire-stable surface. wire-stable surface.
## Decision ## Decision
@@ -35,19 +35,19 @@ wire-stable surface.
### 1. One ALPN, a second open op ### 1. One ALPN, a second open op
The direct op rides the same `alk/tunnel` ALPN (ADR-002 stands: one The direct op rides the same `alk/tunnel` ALPN (ADR-002 stands: one
ALPN, one protocol identity — SSH's one-transport-many-channel-types ALPN, one protocol identity, i.e. SSH's one-transport-many-channel-types
shape). The op id is **`channels/tunnel/direct`** — the flavor-form shape). The op id is **`channels/tunnel/direct`**, the flavor-form
op-id convention (ADR-002 Amendment 1): `channels/<alpn>/<flavor>`, op-id convention (ADR-002 Amendment 1): `channels/<alpn>/<flavor>`,
`Sub`-typed, same establishment phase and wrapper as `…/sub`. An older `Sub`-typed, same establishment phase and wrapper as `…/sub`. An older
producer rejects the unknown op loudly (registry lookup fails — the producer rejects the unknown op loudly (registry lookup fails, which is the
SSH "unknown channel type" posture, `NOT_FOUND`). SSH "unknown channel type" posture, `NOT_FOUND`).
The op-id convention itself needed an upstream alkcall amendment The op-id convention itself needed an upstream alkcall amendment
(ADR-047 amendment: discovery's ALPN derivation and the (ADR-047 amendment: discovery's ALPN derivation and the
`rebuild_spec_for` marker path recognize flavor-form op ids; hub relay `rebuild_spec_for` marker path recognize flavor-form op ids; hub relay
reconstructs them). Runtime was never blocked — registration sets the reconstructs them). Runtime was never blocked, since registration sets the
ALPN explicitly and `open_channel(op_id, params, alpn)` takes the ALPN ALPN explicitly and `open_channel(op_id, params, alpn)` takes the ALPN
as an argument — but discovery + hub relay are the re-produce path as an argument; discovery + hub relay are the re-produce path
(alknodes, OQ-TN-12), so the convention is pinned upstream before the (alknodes, OQ-TN-12), so the convention is pinned upstream before the
first consumer. **Landed in alkcall 0.8.0** (review 008 U-1, ADR-047 first consumer. **Landed in alkcall 0.8.0** (review 008 U-1, ADR-047
amendment 3). amendment 3).
@@ -61,27 +61,27 @@ amendment 3).
} }
``` ```
- **`substrate`** (string, required) — same discriminator semantics as - **`substrate`** (string, required): same discriminator semantics as
ADR-001: `"tcp"` | `"udp"` | `"unix"` in v1; selects the data-plane ADR-001: `"tcp"` | `"udp"` | `"unix"` in v1; selects the data-plane
framing (ADR-003). Enum-validated; unknown values fail schema framing (ADR-003). Enum-validated; unknown values fail schema
validation loudly. validation loudly.
- **`target`** (object, required) — a self-describing JSON object keyed - **`target`** (object, required): a self-describing JSON object keyed
by the sibling `substrate` field: by the sibling `substrate` field:
- `tcp` → `{ "host": <string>, "port": <u16> }` — `host` is a - `tcp` → `{ "host": <string>, "port": <u16> }`, where `host` is a
hostname, IPv4/IPv6 literal, or the JSON form of hostname, IPv4/IPv6 literal, or the JSON form of
`SocketAddr`-shaped values; **domain-form targets stay unresolved `SocketAddr`-shaped values; **domain-form targets stay unresolved
on the wire** (producer-side resolution — the ssh `-D` semantic; on the wire** (producer-side resolution, the ssh `-D` semantic;
both POCs validated egress-side resolution). No DNS dependency on both POCs validated egress-side resolution). No DNS dependency on
the consumer. the consumer.
- `udp` → `{ "host": <string>, "port": <u16> }` — the connected-UDP - `udp` → `{ "host": <string>, "port": <u16> }`: the connected-UDP
destination (endpoint-at-open; ADR-003 framing applies as for base destination (endpoint-at-open; ADR-003 framing applies as for base
udp). udp).
- `unix` → `{ "path": <string> }` — the socket path on the - `unix` → `{ "path": <string> }`: the socket path on the
producer's filesystem. producer's filesystem.
- The schema pins both fields required; the target sub-schema is - The schema pins both fields required; the target sub-schema is
per-substrate (an `allOf` of two `if`/`then` branches keyed on the per-substrate (an `allOf` of two `if`/`then` branches keyed on the
sibling `substrate` value, validated **registry-side** — the schema sibling `substrate` value, validated **registry-side**: the schema
gate runs before the establisher; the establisher's serde parse gate runs before the establisher; the establisher's serde parse
re-checks as the backstop; see the Erratum below and Consequences). re-checks as the backstop; see the Erratum below and Consequences).
Malformed targets are the typed `invalid_input`-class failure; Malformed targets are the typed `invalid_input`-class failure;
**`unknown_resource` can never fire on the direct op** (there is no **`unknown_resource` can never fire on the direct op** (there is no
@@ -97,16 +97,16 @@ amendment 3).
*wire* encodes. *wire* encodes.
- **Dial-only: no listen/direct-BIND form.** The reverse shape - **Dial-only: no listen/direct-BIND form.** The reverse shape
(`-R`-style ingress) is the listen-tunnel + the forwarded op (`-R`-style ingress) is the listen-tunnel + the forwarded op
(ADR-008) — a direct-BIND form would duplicate its capability with (ADR-008); a direct-BIND form would duplicate its capability with
none of its metadata fidelity. none of its metadata fidelity.
### 3. Scope: `TUNNEL_DIRECT_SCOPE = "tunnel:direct"` ### 3. Scope: `TUNNEL_DIRECT_SCOPE = "tunnel:direct"`
The op's `AccessControl.required_scopes` is `["tunnel:direct"]` — a The op's `AccessControl.required_scopes` is `["tunnel:direct"]`, a
separate grant from `tunnel:open`, never implied by it. "Dial separate grant from `tunnel:open`, never implied by it. "Dial
`postgres-primary`" and "dial anything from my network" are different `postgres-primary`" and "dial anything from my network" are different
capabilities; the ACL separation is structural (scopes attach to op capabilities; the ACL separation is structural (scopes attach to op
specs, not params — ADR-006 §2's additivity rule). Scope strings are specs, not params, per ADR-006 §2's additivity rule). Scope strings are
ACL-stable once published (ADR-006 §2): renames are breaking; new ACL-stable once published (ADR-006 §2): renames are breaking; new
gates are new scopes on new ops. gates are new scopes on new ops.
@@ -115,13 +115,13 @@ gates are new scopes on new ops.
Same establisher contract as the dial shape (producer.md), minus the Same establisher contract as the dial shape (producer.md), minus the
lookup: lookup:
1. Parse and validate `target` against `substrate` (a mismatch — 1. Parse and validate `target` against `substrate` (a mismatch, such as
`unix` without `path`, `tcp` with a bad port — is `invalid_input` `unix` without `path` or `tcp` with a bad port, is `invalid_input`
class). class).
2. Inject the parsed `TunnelTarget` into the assembly-provided 2. Inject the parsed `TunnelTarget` into the assembly-provided
`DialFn` (the same halves function the base op's registry backings `DialFn` (the same halves function the base op's registry backings
resolve to; ADR-004 — the protocol never sees a socket type). resolve to; ADR-004, which is why the protocol never sees a socket type).
3. Return the dialed handle via `Establishment::new(plan)` — unchanged 3. Return the dialed handle via `Establishment::new(plan)`: unchanged
plan-flow. plan-flow.
Error mapping (the dial table, minus the registry row): Error mapping (the dial table, minus the registry row):
@@ -163,12 +163,12 @@ sentence (also ADR-006 amendment).
a target object must be optional-with-default. a target object must be optional-with-default.
- **`SubstrateAddr`'s untagged leniency is schema-gated** (review 002 - **`SubstrateAddr`'s untagged leniency is schema-gated** (review 002
N-4): untagged serde variants cannot `deny_unknown_fields`, so a N-4): untagged serde variants cannot `deny_unknown_fields`, so a
cross-shape target (e.g. `{path, host}`) parses at the serde layer — cross-shape target (e.g. `{path, host}`) parses at the serde layer;
the SCHEMA's `additionalProperties: false` per branch is the real the SCHEMA's `additionalProperties: false` per branch is the real
shape-vs-substrate gate. One caution for future registry variants: a shape-vs-substrate gate. One caution for future registry variants: a
schema-less registry would leave the establisher's serde backstop as schema-less registry would leave the establisher's serde backstop as
the only gate, and it keys `render_target` off the sibling the only gate, and it keys `render_target` off the sibling
`substrate` — a cross-shape target that sneaked past would render `substrate`; a cross-shape target that sneaked past would render
*something* (`/p` for the unix case) rather than fail; the serde *something* (`/p` for the unix case) rather than fail; the serde
backstop renders the sibling-keyed target, so cross-shape garbage is backstop renders the sibling-keyed target, so cross-shape garbage is
the schema's absence, not a serde failure. If this crate is ever the schema's absence, not a serde failure. If this crate is ever
@@ -177,15 +177,15 @@ sentence (also ADR-006 amendment).
- **`SubstrateAddr` must not grow overlapping untagged variants.** If - **`SubstrateAddr` must not grow overlapping untagged variants.** If
a future variant's shapes overlap (e.g. a `{host, port, flowlabel}` a future variant's shapes overlap (e.g. a `{host, port, flowlabel}`
form), untagged disambiguation starts ordering (first-match wins form), untagged disambiguation starts ordering (first-match wins
becomes load-bearing and silent) — switch to an internally-tagged becomes load-bearing and silent), which means switching to an internally-tagged
or untagged-with-content form at that point, deliberately (an ADR or untagged-with-content form at that point, deliberately (an ADR
line), never by accident. Pre-consumer and additive until then. line), never by accident. Pre-consumer and additive until then.
- **The BAST question is moot** — params is JSON inside the channels - **The BAST question is moot**: params is JSON inside the channels
open op; the data plane is unchanged (raw pass-through for tcp/unix, open op; the data plane is unchanged (raw pass-through for tcp/unix,
the `[len: u16 BE]` codec for udp — ADR-003). the `[len: u16 BE]` codec for udp per ADR-003).
- **Discovery exposure**: the direct op's `input_schema` carries - **Discovery exposure**: the direct op's `input_schema` carries
`{substrate, target}`, so a discovery listing reveals that dynamic `{substrate, target}`, so a discovery listing reveals that dynamic
egress exists and of which substrates — acceptable; the target egress exists and of which substrates; that is acceptable, since the target
values (what was dialed) never enter discovery (they ride per-call values (what was dialed) never enter discovery (they ride per-call
params). A producer wanting to hide the capability entirely simply params). A producer wanting to hide the capability entirely simply
does not register the op (and old producers reject it loudly anyway). does not register the op (and old producers reject it loudly anyway).
@@ -196,7 +196,7 @@ sentence (also ADR-006 amendment).
direct op itself is protocol-layer (default crate, wasm-clean). direct op itself is protocol-layer (default crate, wasm-clean).
- **SOCKS5 CONNECT composes directly:** the alksocks adapter's - **SOCKS5 CONNECT composes directly:** the alksocks adapter's
`DialFn` seam maps the SOCKS5 CONNECT target (ATYP-decoded) onto `DialFn` seam maps the SOCKS5 CONNECT target (ATYP-decoded) onto
`open_direct` — the graduation's §4 table, now with the stand-in `open_direct`, which is the graduation's §4 table with the stand-in
dissolved. dissolved.
## Erratum (2026-09-20, review 002 D-2) ## Erratum (2026-09-20, review 002 D-2)
@@ -204,7 +204,7 @@ sentence (also ADR-006 amendment).
§2's original wording said the target sub-schema is "`oneOf` on the §2's original wording said the target sub-schema is "`oneOf` on the
sibling `substrate` value, validated at establisher time". Both sibling `substrate` value, validated at establisher time". Both
halves of that sentence were stale against the shipped schema halves of that sentence were stale against the shipped schema
(the decision itself was not — the code is the wire-stable side and (the decision itself was not; the code is the wire-stable side and
is correct): is correct):
1. **The shipped structure is an `allOf` of two `if`/`then` 1. **The shipped structure is an `allOf` of two `if`/`then`
@@ -225,14 +225,14 @@ is correct):
## References ## References
- `docs/research/tunnels-graduation.md` §2 (the ask, options, and the - `docs/research/tunnels-graduation.md` §2 (the ask, options, and the
lean) — the design discussion this ADR records lean): the design discussion this ADR records
- alksocks `poc-bind-findings.md` (the scope-gate precedent) and - alksocks `poc-bind-findings.md` (the scope-gate precedent) and
`poc-connect-wasm-findings.md` (the dial-at-command-read layering) `poc-connect-wasm-findings.md` (the dial-at-command-read layering)
- alkcall ADR-047 (open ops + `input_schema` + discovery marker), - alkcall ADR-047 (open ops + `input_schema` + discovery marker),
ADR-049 (establishment/typed errors), Gap F (op-name → ALPN ADR-049 (establishment/typed errors), Gap F (op-name → ALPN
derivation — the flavor-form amendment target) derivation, the flavor-form amendment target)
- alktunnels ADR-001 (the resource-params invariant the direct op - alktunnels ADR-001 (the resource-params invariant the direct op
extends), ADR-002 Amendment 1 (flavor-form op ids), ADR-006 extends), ADR-002 Amendment 1 (flavor-form op ids), ADR-006
(scope posture + the hub-policy amendment) (scope posture + the hub-policy amendment)
- AGENTS.md conventions 9 (no forced binding — dial-only, no listen - AGENTS.md conventions 9 (no forced binding; dial-only, no listen
form), 13 (access control), 17 (ALPN naming) form), 13 (access control), 17 (ALPN naming)
@@ -1,9 +1,9 @@
# ADR-008: Listen Metadata — Bind-First Establisher and the Forwarded Op # ADR-008: Listen Metadata: Bind-First Establisher and the Forwarded Op
## Status ## Status
Accepted (2026-09-16); amended 2026-09-18 (the correlation token — Accepted (2026-09-16); amended 2026-09-18 (the correlation token,
`listen` reply field + forwarded-params key, see Amendment 1); amended the `listen` reply field + forwarded-params key, see Amendment 1); amended
2026-11-03 (the forwarded op's serving-side establishment phase + the 2026-11-03 (the forwarded op's serving-side establishment phase + the
correlation-miss vocabulary, see Amendment 2) correlation-miss vocabulary, see Amendment 2)
@@ -12,12 +12,12 @@ correlation-miss vocabulary, see Amendment 2)
alksocks' BIND POC (`poc-bind-findings.md` in the alksocks repo) pinned alksocks' BIND POC (`poc-bind-findings.md` in the alksocks repo) pinned
two upstream asks for composed (`-R`-style) BIND fidelity: two upstream asks for composed (`-R`-style) BIND fidelity:
1. **Bound-addr discovery without consuming an accept** — reply#1's 1. **Bound-addr discovery without consuming an accept**: reply#1's
`BND.ADDR` must precede the app server's dial, but the listen `BND.ADDR` must precede the app server's dial, but the listen
establisher is accept-first: its establishment phase pops an establisher is accept-first: its establishment phase pops an
already-accepted connection, so the open cannot complete (and already-accepted connection, so the open cannot complete (and
cannot report a bound address) until the app server dials. cannot report a bound address) until the app server dials.
2. **Peer metadata on accepted handles** — reply#2's peer address is 2. **Peer metadata on accepted handles**: reply#2's peer address is
lost through the composed path; `TargetHandle` carries boxed halves lost through the composed path; `TargetHandle` carries boxed halves
only, so the composed variant reports the sentinel. only, so the composed variant reports the sentinel.
@@ -25,8 +25,8 @@ The graduation research (`docs/research/tunnels-graduation.md` §3)
framed the option space: per-ask carriers (a reply-metadata projection framed the option space: per-ask carriers (a reply-metadata projection
plus one of three addr-discovery shapes) versus ssh's plus one of three addr-discovery shapes) versus ssh's
`forwarded-tcpip` shape as the combined alternative. Decision: the `forwarded-tcpip` shape as the combined alternative. Decision: the
`forwarded-tcpip` shape — **bind-first establisher + accept-as-open** `forwarded-tcpip` shape (**bind-first establisher + accept-as-open**),
— because it resolves both asks *and* two structural problems the POC because it resolves both asks *and* two structural problems the POC
recorded, with zero special carriers: recorded, with zero special carriers:
- The address-first-vs-accept-first **deadlock dissolves** instead of - The address-first-vs-accept-first **deadlock dissolves** instead of
@@ -35,9 +35,9 @@ recorded, with zero special carriers:
OS-chosen bound address (ask #1 solved natively). OS-chosen bound address (ask #1 solved natively).
- The **10s establishment-bound problem dissolves**: the accept wait - The **10s establishment-bound problem dissolves**: the accept wait
moves out of the establishment phase into the pump-handler phase moves out of the establishment phase into the pump-handler phase
(whose lifetime is teardown-governed, not establishment-bounded) — (whose lifetime is teardown-governed, not establishment-bounded), so
FTP-style long BIND waits stop resolving as spurious refusals. FTP-style long BIND waits stop resolving as spurious refusals.
- Ask #2 (peer metadata) rides the reverse channel's open-op params — - Ask #2 (peer metadata) rides the reverse channel's open-op params:
the wire's native "this connection is for X" moment. the wire's native "this connection is for X" moment.
All the machinery is POC-validated: `open_reverse_channel` + All the machinery is POC-validated: `open_reverse_channel` +
@@ -50,18 +50,18 @@ over a serving connection (the reverse POC's hub shape).
### 1. Third establisher shape: bind-first ### 1. Third establisher shape: bind-first
The listen establisher gains a bind-first variant alongside the The listen establisher gains a bind-first variant alongside the
accept-first shape (which stays — see §5): accept-first shape (which stays; see §5):
- The establisher **binds the listener** (via the assembly-provided - The establisher **binds the listener** (via the assembly-provided
listen function) and returns `Establishment::new(plan)` carrying the listen function) and returns `Establishment::new(plan)` carrying the
listener handle. Establishment is fast — bounded by the bind, not by listener handle. Establishment is fast, bounded by the bind, not by
any app-server dial. any app-server dial.
- **The establisher mints a correlation token** (Amendment 1): a fresh - **The establisher mints a correlation token** (Amendment 1): a fresh
u32 unique per open on this producer, carried twice — inside the u32 unique per open on this producer, carried twice: inside the
plan (the pump handler needs it to correlate per-accept forwarded plan (the pump handler needs it to correlate per-accept forwarded
opens) and as the additive `listen` reply field (§2). The plan also opens) and as the additive `listen` reply field (§2). The plan also
carries the listen tunnel's substrate, stamped at establisher time carries the listen tunnel's substrate, stamped at establisher time
from the one successful params parse — the accept dispatcher reads from the one successful params parse; the accept dispatcher reads
it from the plan and stamps it into each forwarded open's it from the plan and stamps it into each forwarded open's
`substrate` param; it never re-parses wire input (review 002 M-1: `substrate` param; it never re-parses wire input (review 002 M-1:
the removed silent `Tcp` fallback). The original the removed silent `Tcp` fallback). The original
@@ -71,24 +71,25 @@ accept-first shape (which stays — see §5):
a generic hub relay forwards params untouched (it strips/rewrites a generic hub relay forwards params untouched (it strips/rewrites
only the reply's `channel_id`), so a spoke's forwarded open would only the reply's `channel_id`), so a spoke's forwarded open would
carry an id the end opener never allocated. A producer-minted token carry an id the end opener never allocated. A producer-minted token
rides both hops untouched — reply fields pass through relays rides both hops untouched, since reply fields pass through relays
per-hop-truthful (alkcall ADR-051), and the same minted token per-hop-truthful (alkcall ADR-051), and the same minted token
appears in every hop's reply. appears in every hop's reply.
- The establisher MAY contribute the observed bound address to the - The establisher MAY contribute the observed bound address to the
open-op reply (§2): `"bound": { ... }` — the real OS-chosen address open-op reply (§2): `"bound": { ... }`, which is the real OS-chosen address
(port `0` resolved), substrate-shaped like the direct op's target (port `0` resolved), substrate-shaped like the direct op's target
(`{host, port}` for tcp/udp, `{path}` for unix). (`{host, port}` for tcp/udp, `{path}` for unix).
- The pump handler for this shape is an **accept dispatcher**, not - The pump handler for this shape is an **accept dispatcher**, not
`pump_bidi`: it accepts the channel's `BiStream` (the R-02 contract) `pump_bidi`: it accepts the channel's `BiStream` (the R-02 contract)
and runs the assembly-provided accept loop concurrently — each and runs the assembly-provided accept loop concurrently; each
accepted connection produces a forwarded channel (§3); the channel's accepted connection produces a forwarded channel (§3); the channel's
own `BiStream` stays idle and its EOF (the opener dropping the own `BiStream` stays idle and its EOF (the opener dropping the
listener session) cancels the loop and tears the listener down. The listener session) cancels the loop and tears the listener down. The
idle-read check is deliberately WIDER than a bare EOF comparison idle-read check is deliberately WIDER than a bare EOF comparison
(review 002 N-2): the watch treats ANY read as teardown — `Ok(0)` (review 002 N-2): the watch treats ANY read as teardown, whether
(the EOF sentinel), an error, **and** any non-empty read (a peer `Ok(0)`
that writes to the idle listen channel is misbehaving; the channel (the EOF sentinel), an error, **or** any non-empty read (a peer
is not a data plane — fail closed). An implementer reading only that writes to the idle listen channel is misbehaving; the channel
is not a data plane, so fail closed). An implementer reading only
"its EOF cancels the loop" and checking `Ok(0)` alone would hang on "its EOF cancels the loop" and checking `Ok(0)` alone would hang on
a write-happy peer; the shipped posture is the any-byte one, a write-happy peer; the shipped posture is the any-byte one,
pinned by the write-happy-peer test. pinned by the write-happy-peer test.
@@ -102,42 +103,42 @@ The open-op reply gains an **optional, additive** field:
``` ```
- Present only when the establisher supplies it (bind-first listeners); - Present only when the establisher supplies it (bind-first listeners);
absent for dial establishers — old consumers ignore unknown reply absent for dial establishers; old consumers ignore unknown reply
fields (plain JSON, alkcall `ResponseEnvelope`), so the change is fields (plain JSON, alkcall `ResponseEnvelope`), so the change is
wire-additive. wire-additive.
- The carrier is an upstream alkcall ask (ADR-049 amendment 3: - The carrier is an upstream alkcall ask (ADR-049 amendment 3:
establisher → reply projection — `Establishment` gains optional establisher → reply projection, with `Establishment` gaining optional
reply fields the wrapper merges into the success output; the reply fields the wrapper merges into the success output; the
`channel_id` key is wrapper-reserved). Filed in `channel_id` key is wrapper-reserved). Filed in
`alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md`. `alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md`.
**Landed in alkcall 0.8.0** (review 008 U-2, ADR-049 amendment 3: **Landed in alkcall 0.8.0** (review 008 U-2, ADR-049 amendment 3:
`Establishment::with_reply_field` + the wrapper merge with the `Establishment::with_reply_field` + the wrapper merge with the
reserved-key guard). reserved-key guard).
- **`listen` — the correlation token (Amendment 1).** A bind-first - **`listen`: the correlation token (Amendment 1).** A bind-first
establisher also contributes `"listen": <u32>`: the token the establisher also contributes `"listen": <u32>`: the token the
forwarded op's params key on (§3, as amended). Same projection forwarded op's params key on (§3, as amended). Same projection
carrier, same additive posture — a second reply field on the same carrier, same additive posture: a second reply field on the
opens that carry `bound`. `bound` and `listen` are independent same opens that carry `bound`. `bound` and `listen` are independent
fields (an accept-first establisher could supply `listen`-shaped fields (an accept-first establisher could supply `listen`-shaped
correlation too — the accept queue's establisher mints its own — correlation too, since the accept queue's establisher mints its
but v1 pins the pair on bind-first only). own, but v1 pins the pair on bind-first only).
- `bound` is informational surface, not addressing: a consumer MUST - `bound` is informational surface, not addressing: a consumer MUST
NOT treat it as a dial target (the producer owns the backing — NOT treat it as a dial target (the producer owns the backing,
ADR-001's invariant). It exists for RFC-1928-reply-class fidelity per ADR-001's invariant). It exists for RFC-1928-reply-class fidelity
(BND.ADDR) and observability. (BND.ADDR) and observability.
### 3. The forwarded op: `channels/tunnel/forwarded` ### 3. The forwarded op: `channels/tunnel/forwarded`
When the listener accepts, the **producer opens a channel back toward When the listener accepts, the **producer opens a channel back toward
the listen-opener** — accept-as-open, the `forwarded-tcpip` shape: the listen-opener**: accept-as-open, the `forwarded-tcpip` shape:
- Op id **`channels/tunnel/forwarded`** (flavor-form, ADR-002 - Op id **`channels/tunnel/forwarded`** (flavor-form, ADR-002
Amendment 1), `Sub`-typed, ALPN marker `alk/tunnel`. It is served by Amendment 1), `Sub`-typed, ALPN marker `alk/tunnel`. It is served by
the **listen-opener side** (the initiator of the listen-tunnel the **listen-opener side** (the initiator of the listen-tunnel
open): the producer *calls* it per accept open): the producer *calls* it per accept
(`open_reverse_channel`-shaped — the call plane on channel 0), the (`open_reverse_channel`-shaped; the call plane on channel 0), the
opener's registry allocates the `channel_id`, the producer adopts it opener's registry allocates the `channel_id`, the producer adopts it
(the ADR-047 §5 connection-owner rule — the serving side of the (the ADR-047 §5 connection-owner rule, i.e. the serving side of the
forwarded call allocates; the producer-side initiator adopts) and forwarded call allocates; the producer-side initiator adopts) and
pumps the accepted connection's halves against the adopted channel. pumps the accepted connection's halves against the adopted channel.
- Params: - Params:
@@ -150,19 +151,19 @@ the listen-opener** — accept-as-open, the `forwarded-tcpip` shape:
} }
``` ```
- **`listen`** (u32, required) — the correlation token the - **`listen`** (u32, required): the correlation token the
bind-first establisher minted and returned in the listen open's bind-first establisher minted and returned in the listen open's
reply (§2, as amended). The opener matches it against the reply (§2, as amended). The opener matches it against the
`listen` field of its open listener sessions — a `HashMap<u32, ..>` `listen` field of its open listener sessions, a `HashMap<u32, ..>`
on the opener side; no registry op, no extra wire field. The on the opener side; no registry op, no extra wire field. The
token space is the producer's: fresh per open, unique on that token space is the producer's: fresh per open, unique on that
producer (the opener-side map is keyed by the producer's token, producer (the opener-side map is keyed by the producer's token,
and one opener's map may hold tokens from several producers — a and one opener's map may hold tokens from several producers; a
hash collision across producers is the opener's bookkeeping hash collision across producers is the opener's bookkeeping
choice, e.g. keying `(token)` uniquely or `(producer, token)`; choice, e.g. keying `(token)` uniquely or `(producer, token)`;
v1 leaves it assembly-side). **Amends the original `channel` v1 leaves it assembly-side). **Amends the original `channel`
key** (the listen channel's ID): that semantic is unimplementable key** (the listen channel's ID): that semantic is unimplementable
as pinned — the serving-side wrapper never exposes the allocated as pinned, since the serving-side wrapper never exposes the allocated
channel ID to the establisher or handler (alkcall 0.8.0's channel ID to the establisher or handler (alkcall 0.8.0's
`OpenHandler` surface), and generic hub relays forward params `OpenHandler` surface), and generic hub relays forward params
untouched, so a spoke's forwarded open would carry an id the end untouched, so a spoke's forwarded open would carry an id the end
@@ -171,34 +172,35 @@ the listen-opener** — accept-as-open, the `forwarded-tcpip` shape:
the reply field passes through per-hop truthful (alkcall ADR-051), the reply field passes through per-hop truthful (alkcall ADR-051),
and the forwarded open carries the same minted value at every and the forwarded open carries the same minted value at every
hop. hop.
- **`substrate`** (string, required) — the listen tunnel's - **`substrate`** (string, required): the listen tunnel's
substrate; shapes the forwarded channel's data-plane framing substrate; shapes the forwarded channel's data-plane framing
(ADR-003), same discriminator semantics as everywhere else. (ADR-003), same discriminator semantics as everywhere else.
- **`peer`** (object, required) — the accepted connection's remote - **`peer`** (object, required): the accepted connection's remote
address, substrate-shaped (`{host, port}` / `{path}`). This is ask address, substrate-shaped (`{host, port}` / `{path}`). This is ask
#2's carrier: reply#2-class fidelity rides the open op, the wire's #2's carrier: reply#2-class fidelity rides the open op, the wire's
native metadata moment. Optional in kind, required in v1 — a native metadata moment. Optional in kind, required in v1: a
producer that cannot know the peer (rare; all POC'd listeners producer that cannot know the peer (rare; all POC'd listeners
can) omits... no: **required**; a producer that cannot determine can) omits... no: **required**; a producer that cannot determine
the peer does not serve a bind-first listener over that substrate. the peer does not serve a bind-first listener over that substrate.
- Scope: **`TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"`** — the - Scope: **`TUNNEL_FORWARDED_SCOPE = "tunnel:forwarded"`**, the
opener-side grant for RECEIVING forwarded connections (ingress). opener-side grant for RECEIVING forwarded connections (ingress).
Separate from `tunnel:open` and `tunnel:direct` (ADR-006 amendment): Separate from `tunnel:open` and `tunnel:direct` (ADR-006 amendment):
exposing a listener and dialing egress are different capabilities. exposing a listener and dialing egress are different capabilities.
- The opener-side serving shape: `register_openable_with_establisher` - The opener-side serving shape: `register_openable_with_establisher`
as-is (no establisher — the forwarded channel needs no as-is (no establisher, because the forwarded channel needs no
establishment; its data plane starts immediately — **amended by establishment; its data plane starts immediately; this clause was
Amendment 2**: the served shape gains an establisher variant, the **amended by Amendment 2**: the served shape gains an establisher
injected correlation gate; this sentence records the pre-amendment variant, the injected correlation gate; this sentence records the
posture, which stays a valid shape — see Amendment 2), ACL-gated by pre-amendment posture, which stays a valid shape per Amendment 2),
ACL-gated by
the scope, handler = correlate the open (via `listen`, the the scope, handler = correlate the open (via `listen`, the
correlation token) to the waiting correlation token) to the waiting
session, accept the `BiStream`, pump against the local BIND client's session, accept the `BiStream`, pump against the local BIND client's
halves. The correlation registry is assembly-side bookkeeping — halves. The correlation registry is assembly-side bookkeeping;
the protocol provides the op and the params, not the the protocol provides the op and the params, not the
wait/quiet-state machine. wait/quiet-state machine.
- An older opener that does not serve the forwarded op rejects the - An older opener that does not serve the forwarded op rejects the
producer's per-accept call loudly (`NOT_FOUND`) — the SSH posture. producer's per-accept call loudly (`NOT_FOUND`), the SSH posture.
A producer serving bind-first SHOULD prefer openers that advertise A producer serving bind-first SHOULD prefer openers that advertise
the forwarded op (discovery, alkcall ADR-047). the forwarded op (discovery, alkcall ADR-047).
@@ -210,7 +212,7 @@ let bound = listener.bound(); // the resolved ad
// ... per forwarded accept (opener side, served op): // ... per forwarded accept (opener side, served op):
// correlate by `listen`, accept the BiStream, pump against the // correlate by `listen`, accept the BiStream, pump against the
// local session's halves // local session's halves
listener.close(self); // or Drop — tears the listen channel + far listener down listener.close(self); // or Drop; tears the listen channel + far listener down
``` ```
`TunnelListener` is the listen-flow sibling of `TunnelSession`: it owns `TunnelListener` is the listen-flow sibling of `TunnelSession`: it owns
@@ -218,7 +220,7 @@ the listen channel's teardown (ADR-005's ownership posture extended to
the listen flow); it presents `bound()` and the teardown API, and the the listen flow); it presents `bound()` and the teardown API, and the
forwarded channels themselves arrive as separate adopted-side forwarded channels themselves arrive as separate adopted-side
`TunnelSession`s on the opener's manager. `TunnelSession::adopt` gains `TunnelSession`s on the opener's manager. `TunnelSession::adopt` gains
nothing — forwarded channels are ordinary adopted channels. The nothing; forwarded channels are ordinary adopted channels. The
opener-side correlation registry (token → waiting listener session) opener-side correlation registry (token → waiting listener session)
is assembly-side bookkeeping (consumer.md's posture); the crate's is assembly-side bookkeeping (consumer.md's posture); the crate's
`TunnelListener` type provides the `listen` token it needs to key it. `TunnelListener` type provides the `listen` token it needs to key it.
@@ -230,7 +232,7 @@ remains a valid establisher shape for producers whose resource backing
IS an accept queue (an externally-fed or pre-bound listener shared IS an accept queue (an externally-fed or pre-bound listener shared
across opens). Bind-first is the shape for producers that own the across opens). Bind-first is the shape for producers that own the
bind decision (the common hub case; alksocks' composed BIND). The two bind decision (the common hub case; alksocks' composed BIND). The two
shapes are per-registration assembly choices over the same op — no shapes are per-registration assembly choices over the same op; there is no
wire distinction beyond `bound`'s presence. wire distinction beyond `bound`'s presence.
## Consequences ## Consequences
@@ -243,7 +245,7 @@ wire distinction beyond `bound`'s presence.
(ADR-049 amendment 3) and the flavor-form discovery derivation (ADR-049 amendment 3) and the flavor-form discovery derivation
(ADR-047 amendment) **landed in alkcall 0.8.0** (review 008, Units (ADR-047 amendment) **landed in alkcall 0.8.0** (review 008, Units
1–2); the in-tree `ChannelRelay` + `HubLegTemplate` (ADR-051) came 1–2); the in-tree `ChannelRelay` + `HubLegTemplate` (ADR-051) came
with them — the hub re-produce machinery §6 assumed assembly-layer with them, so the hub re-produce machinery §6 assumed assembly-layer
is now upstream, and the relay passes reply fields through per-hop is now upstream, and the relay passes reply fields through per-hop
truthful (which is what makes the Amendment 1 token compose through truthful (which is what makes the Amendment 1 token compose through
hubs with zero tunnel-specific relay behavior). The bind-first hubs with zero tunnel-specific relay behavior). The bind-first
@@ -253,21 +255,21 @@ wire distinction beyond `bound`'s presence.
carrier from the graduation doc is superseded: with accept-as-open, carrier from the graduation doc is superseded: with accept-as-open,
the peer rides the forwarded op's params; no process-local metadata the peer rides the forwarded op's params; no process-local metadata
field, no sentinel. (If a future non-forwarded accept shape needs field, no sentinel. (If a future non-forwarded accept shape needs
metadata, revisit then — additive.) metadata, revisit then, and the change is additive.)
- **The BIND reply#2 fidelity is end-to-end:** the opener-side - **The BIND reply#2 fidelity is end-to-end:** the opener-side
correlation writes reply#2 with the REAL peer from the forwarded correlation writes reply#2 with the REAL peer from the forwarded
op's params. reply#1's `BND.ADDR` is the `bound` reply field. Both op's params. reply#1's `BND.ADDR` is the `bound` reply field. Both
POC stand-ins (carried `bind_addr`; sentinel `0.0.0.0:0`) retire. POC stand-ins (carried `bind_addr`; sentinel `0.0.0.0:0`) retire.
- **Hub re-produce:** a hub re-producing a listen resource binds its - **Hub re-produce:** a hub re-producing a listen resource binds its
own listener (bind-first, its own `bound` + minted `listen` token) own listener (bind-first, its own `bound` + minted `listen` token)
and re-opens forwarded channels with the same token — the spoke's and re-opens forwarded channels with the same token, so the spoke's
forwarded opens carry the hub's token untouched (params are not forwarded opens carry the hub's token untouched (params are not
rewritten by the generic relay), and the metadata is per-hop rewritten by the generic relay), and the metadata is per-hop
truthful, matching alkcall ADR-042's terminate-and-re-produce truthful, matching alkcall ADR-042's terminate-and-re-produce
posture. The end opener correlates on the hub's minted token — the posture. The end opener correlates on the hub's minted token, the
value it read from its own open's reply. value it read from its own open's reply.
- **Local-variant BIND needs none of this** (the listener and the - **Local-variant BIND needs none of this** (the listener and the
accept are in-process — POC #5 variant 1, unchanged); the asks and accept are in-process; POC #5 variant 1, unchanged); the asks and
this ADR are scoped to the composed variant. this ADR are scoped to the composed variant.
## References ## References
@@ -278,18 +280,18 @@ wire distinction beyond `bound`'s presence.
deadlock and establishment-bound findings this shape dissolves) deadlock and establishment-bound findings this shape dissolves)
- alkcall ADR-047 (open ops; connection-owner allocation; the - alkcall ADR-047 (open ops; connection-owner allocation; the
flavor-form amendment), ADR-049 (establishment; the reply-projection flavor-form amendment), ADR-049 (establishment; the reply-projection
amendment), ADR-042 (hub relay — re-produce id semantics) amendment), ADR-042 (hub relay; re-produce id semantics)
- alktunnels ADR-001 (params per-op amendment), ADR-002 Amendment 1 - alktunnels ADR-001 (params per-op amendment), ADR-002 Amendment 1
(flavor-form op ids), ADR-005 (teardown ownership — extended to (flavor-form op ids), ADR-005 (teardown ownership, extended to
`TunnelListener`), ADR-006 (scope posture amendment) `TunnelListener`), ADR-006 (scope posture amendment)
- AGENTS.md conventions 9 (no forced binding — the bind decision is - AGENTS.md conventions 9 (no forced binding, so the bind decision is
the producer's), 13 (access control), 8 (two-pump contract — the the producer's), 13 (access control), 8 (two-pump contract, where the
accept dispatcher is a per-accept `pump_bidi`, not a hand-rolled accept dispatcher is a per-accept `pump_bidi`, not a hand-rolled
loop over one stream) loop over one stream)
## Amendment 1 (the correlation token, 2026-09-18) ## Amendment 1 (the correlation token, 2026-09-18)
The decision stands — bind-first establisher, accept-as-open, the The decision stands: bind-first establisher, accept-as-open, the
`bound` reply field, the forwarded op. What changes: the forwarded `bound` reply field, the forwarded op. What changes: the forwarded
op's correlation key (§3's `channel` param) and the bind-first op's correlation key (§3's `channel` param) and the bind-first
establisher's reply surface (§2 gains `listen`). establisher's reply surface (§2 gains `listen`).
@@ -301,18 +303,18 @@ establisher's reply surface (§2 gains `listen`).
1. **The serving side never learns its own channel ID.** The 1. **The serving side never learns its own channel ID.** The
open-op wrapper allocates the channel and replies with its ID, open-op wrapper allocates the channel and replies with its ID,
but the `OpenHandler`/establisher surface is but the `OpenHandler`/establisher surface is
`Fn(Value, Option<ChannelPlan>, Connection, AuthContext)` — no `Fn(Value, Option<ChannelPlan>, Connection, AuthContext)`, with no
ID anywhere. The §3 semantic ("the opener knows … without a ID anywhere. The §3 semantic ("the opener knows … without a
registry op") required the producer's accept dispatcher to know registry op") required the producer's accept dispatcher to know
its own listen channel's ID to stamp into forwarded opens; its own listen channel's ID to stamp into forwarded opens;
unimplementable without a new upstream surface. (Filing one — unimplementable without a new upstream surface. (Filing one,
exposing the allocated ID to handlers — was considered and which would expose the allocated ID to handlers, was considered and
rejected: it is an alkcall-surface change for a correlation rejected: it is an alkcall-surface change for a correlation
need this crate can solve in-protocol, and it would still not need this crate can solve in-protocol, and it would still not
fix (2).) fix (2).)
2. **Generic hub relays do not rewrite params.** The in-tree 2. **Generic hub relays do not rewrite params.** The in-tree
`ChannelRelay` (ADR-051) forwards the open-op input untouched — `ChannelRelay` (ADR-051) forwards the open-op input untouched;
it strips/rewrites only the reply's `channel_id`. A spoke's it strips/rewrites only the reply's `channel_id`. A spoke's
forwarded opens would carry the spoke-side listen channel's ID, forwarded opens would carry the spoke-side listen channel's ID,
which the end opener never allocated and cannot correlate ("hubs which the end opener never allocated and cannot correlate ("hubs
re-map the id naturally" assumed tunnel-specific relay behavior re-map the id naturally" assumed tunnel-specific relay behavior
@@ -323,23 +325,23 @@ establisher's reply surface (§2 gains `listen`).
reply field via the same `Establishment::with_reply_field` carrier reply field via the same `Establishment::with_reply_field` carrier
`bound` rides. Both fields pass through relays untouched `bound` rides. Both fields pass through relays untouched
(reply fields ride per-hop-truthful, ADR-051), and the forwarded (reply fields ride per-hop-truthful, ADR-051), and the forwarded
open's `listen` param carries the minted token at every hop — the open's `listen` param carries the minted token at every hop; the
end opener correlates on the value from its own open's reply. end opener correlates on the value from its own open's reply.
- **Why not a consumer-minted token:** the opener choosing the token - **Why not a consumer-minted token:** the opener choosing the token
and passing it in the listen open's params would work on the direct and passing it in the listen open's params would work on the direct
path, but fails hub re-produce the same way (the hub's own path, but fails hub re-produce the same way (the hub's own
bind-first open is a NEW open — it cannot adopt the end opener's bind-first open is a NEW open, and it cannot adopt the end opener's
token without a pass-through mechanism, which is exactly the token without a pass-through mechanism, which is exactly the
params-rewriting relays don't do). Producer-minted is the direction params-rewriting relays don't do). Producer-minted is the direction
information already flows (producer → opener via reply fields); information already flows (producer → opener via reply fields);
no new flow is needed. no new flow is needed.
- **Token lifecycle:** minted at establisher time (one per open — - **Token lifecycle:** minted at establisher time (one per open;
two opens never share a token, so the opener's correlation map two opens never share a token, so the opener's correlation map
needs no session disambiguation beyond the token); retired with needs no session disambiguation beyond the token); retired with
the listener session (the token's only consumer is the per-accept the listener session (the token's only consumer is the per-accept
forwarded opens of that listener). Uniqueness is per-producer; forwarded opens of that listener). Uniqueness is per-producer;
the opener-side map keys on the token as read from its own reply. the opener-side map keys on the token as read from its own reply.
- **Scope names, op ids, `bound`, `peer` — unchanged.** The wire - **Scope names, op ids, `bound`, and `peer` are unchanged.** The wire
delta vs the pre-amendment ADR is exactly: the reply gains the delta vs the pre-amendment ADR is exactly: the reply gains the
optional `listen` field, the forwarded params rename `channel` → optional `listen` field, the forwarded params rename `channel` →
`listen` with token semantics. Nothing else shipped against either `listen` with token semantics. Nothing else shipped against either
@@ -349,17 +351,17 @@ establisher's reply surface (§2 gains `listen`).
Resolves review 003 (`docs/reviews/003-correlation-miss-arrival-class-alkssh-input.md`) Resolves review 003 (`docs/reviews/003-correlation-miss-arrival-class-alkssh-input.md`)
via `docs/plans/003-correlation-miss-resolve.md` §Decisions 1–4. The via `docs/plans/003-correlation-miss-resolve.md` §Decisions 1–4. The
decision stands — bind-first establisher, accept-as-open, the decision stands: bind-first establisher, accept-as-open, the
`bound`/`listen` reply fields, the forwarded op, `tunnel:forwarded`. `bound`/`listen` reply fields, the forwarded op, `tunnel:forwarded`.
What changes: **the serving side of the forwarded op (§3) gains an What changes: **the serving side of the forwarded op (§3) gains an
establishment phase** — §3's sentence "no establisher — the forwarded establishment phase**: §3's sentence, "no establisher; the forwarded
channel needs no establishment; its data plane starts immediately" channel needs no establishment; its data plane starts immediately"
(marked inline above) is the text this amendment amends. (marked inline above) is the text this amendment amends.
### The miss class this closes (the tri-vocabulary history) ### The miss class this closes (the tri-vocabulary history)
A forwarded open whose `listen` token no longer routes — the A forwarded open whose `listen` token no longer routes, i.e. the
stale/missing-token arrival class — is currently served stale/missing-token arrival class, is currently served
establisher-less everywhere (alkssh's door, alksocks' composed BIND, establisher-less everywhere (alkssh's door, alksocks' composed BIND,
and this crate's built-in shape). That is the one posture exception and this crate's built-in shape). That is the one posture exception
to ADR-049's "a refused open is a typed call error, never a phantom to ADR-049's "a refused open is a typed call error, never a phantom
@@ -367,16 +369,16 @@ channel": the open succeeds at the call plane, declines post-open in
the assembly, and the producer-side pump EOFs with no signal. The the assembly, and the producer-side pump EOFs with no signal. The
family has named the class three ways: family has named the class three ways:
- **alkssh** — `correlation-miss` at `debug`: - **alkssh** uses `correlation-miss` at `debug`:
`observe::decline_arrival(row, "correlation-miss", listen)` + `observe::decline_arrival(row, "correlation-miss", listen)` +
teardown-at-birth (`src/tunnel_map.rs`; tokens removed at session teardown-at-birth (`src/tunnel_map.rs`; tokens removed at session
teardown by the shared per-fabric-leg `ListenRouter`). teardown by the shared per-fabric-leg `ListenRouter`).
- **alksocks** — prose at `warn`: "forwarded open with no waiting - **alksocks** uses prose at `warn`: "forwarded open with no waiting
session" (`src/composed/mod.rs:497`). session" (`src/composed/mod.rs:497`).
- **alktunnels** — the `handler_error`-class generic arm at `warn` - **alktunnels** has the `handler_error`-class generic arm at `warn`
(the bind-first accept loop's failed-open branch, (the bind-first accept loop's failed-open branch,
`src/producer.rs:838-854`), which fires only for the `src/producer.rs:838-854`), which fires only for the
connection-gone/`NOT_FOUND`/ACL-refused subclass — an in-session connection-gone/`NOT_FOUND`/ACL-refused subclass; an in-session
miss (route removed while the fabric connection lives) succeeds at miss (route removed while the fabric connection lives) succeeds at
the call plane and logs nothing at all. the call plane and logs nothing at all.
@@ -384,7 +386,7 @@ Amendment 2 retires the prose variant and the fabric-side silence.
### The decisions ### The decisions
1. **The serving side gains an establisher variant — the gate is 1. **The serving side gains an establisher variant, and the gate is
injected.** The protocol crate cannot hold the assembly's injected.** The protocol crate cannot hold the assembly's
correlation map, so the establisher's check is an correlation map, so the establisher's check is an
assembly-provided gate closure over the forwarded params assembly-provided gate closure over the forwarded params
@@ -392,38 +394,39 @@ Amendment 2 retires the prose variant and the fabric-side silence.
alkcall `OpenEstablisher` dance and registers via alkcall `OpenEstablisher` dance and registers via
`register_openable_with_establisher` (alkcall 0.10.x, verified `register_openable_with_establisher` (alkcall 0.10.x, verified
present). Assemblies must NOT hand-roll the alkcall establisher present). Assemblies must NOT hand-roll the alkcall establisher
plumbing — the crate-owned registration surface is the only plumbing; the crate-owned registration surface is the only
supported path. The gate is a lookup, not a dial: establishment supported path. The gate is a lookup, not a dial: establishment
stays fast, bounded by the usual establisher rules only. stays fast, bounded by the usual establisher rules only.
2. **A stale/missing token fails the open as a typed 2. **A stale/missing token fails the open as a typed
`channel:open_failed`.** The crate maps the gate's failure to `channel:open_failed`.** The crate maps the gate's failure to
alkcall's `EstablishmentError::UnknownResource`, so the reply is alkcall's `EstablishmentError::UnknownResource`, so the reply is
`channel:open_failed` with `details.reason = "unknown_resource"` — `channel:open_failed` with `details.reason = "unknown_resource"`,
no phantom channel is ever born (ADR-049's posture, extended here and no phantom channel is ever born (ADR-049's posture, extended here
to the forwarded op's serving side). The waiting session IS a to the forwarded op's serving side). The waiting session IS a
resource that is gone; **`unknown_resource` is reused as-is and resource that is gone; **`unknown_resource` is reused as-is and
the wire vocabulary is NOT extended**: `wire.md`'s open-op sections the wire vocabulary is NOT extended**: `wire.md`'s open-op sections
(the pinned `details.reason ∈ dial_failed / unknown_resource / (the pinned `details.reason ∈ dial_failed / unknown_resource /
resource_shortage / handler_error / timeout` list and its resource_shortage / handler_error / timeout` list and its
§The Forwarded Op) are untouched by this amendment — there is no §The Forwarded Op) are untouched by this amendment; there is no
reason-class ADR, no new wire value. (The one serving-shape prose reason-class ADR, no new wire value. (The one serving-shape prose
line in `wire.md` — "no establisher" under §The Forwarded Op — line in `wire.md`, "no establisher" under §The Forwarded Op,
syncs with the API task's in-code doc updates, plan 003 W1; this syncs with the API task's in-code doc updates, plan 003 W1; this
ADR does not touch `wire.md`.) ADR does not touch `wire.md`.)
3. **Gate order is pinned: the scope gate precedes the establisher.** 3. **Gate order is pinned: the scope gate precedes the establisher.**
The registry runs `tunnel:forwarded` ACL before the open op's The registry runs `tunnel:forwarded` ACL before the open op's
establishment phase (alkcall's open-op wrapper order: registry establishment phase (alkcall's open-op wrapper order: registry
gates — schema, then ACL — then the wrapper's allocate → gates (schema, then ACL), then the wrapper's allocate →
establisher → handler). A scope-less producer is `FORBIDDEN` establisher → handler). A scope-less producer is `FORBIDDEN`
before any gate lookup; the gate never sees an unauthorized open. before any gate lookup; the gate never sees an unauthorized open.
4. **The residual post-open race keeps teardown-at-birth, named 4. **The residual post-open race keeps teardown-at-birth, named
`correlation-miss` at `debug`.** A token that vanishes between `correlation-miss` at `debug`.** A token that vanishes between
establisher pass and handler run — the narrow race the gate cannot establisher pass and handler run, the narrow race the gate cannot
close — keeps §3's opener-half posture, unchanged: the assembly close, keeps §3's opener-half posture, unchanged: the assembly
handler declines, its early return reaps the channel handler declines, its early return reaps the channel
teardown-at-birth (R-02). Its event name is **`correlation-miss`** teardown-at-birth (R-02). Its event name is **`correlation-miss`**
(alkssh's shipped vocabulary wins — it is published; alksocks' (alkssh's shipped vocabulary wins; it is published, and alksocks'
prose variant retires), at `debug` **assembly-side** — the prose variant retires), at `debug` **assembly-side**:
the
protocol crate's built-in handler holds no map, so this is the protocol crate's built-in handler holds no map, so this is the
assemblies' convention this ADR records, not code this crate runs. assemblies' convention this ADR records, not code this crate runs.
The producer-side generic arm (the accept loop's failed-open The producer-side generic arm (the accept loop's failed-open
@@ -436,7 +439,7 @@ The base `register_tunnel_forwarded_openable` (establisher-less, the
post-open shape) stays for compatibility; the establisher variant post-open shape) stays for compatibility; the establisher variant
(`register_tunnel_forwarded_openable_with_establisher`) is the (`register_tunnel_forwarded_openable_with_establisher`) is the
recommended posture, and new assemblies take it by default. Both ride recommended posture, and new assemblies take it by default. Both ride
the same op id, params, scope, and reply surface — the wire delta of the same op id, params, scope, and reply surface; the wire delta of
this amendment is empty. this amendment is empty.
### References ### References
@@ -452,5 +455,5 @@ this amendment is empty.
`EstablishmentError::UnknownResource → unknown_resource` (verified) `EstablishmentError::UnknownResource → unknown_resource` (verified)
- alkssh `src/tunnel_map.rs` (`ListenRouter`, `decline_arrival`); - alkssh `src/tunnel_map.rs` (`ListenRouter`, `decline_arrival`);
alksocks `src/composed/mod.rs:497`; alktunnels alksocks `src/composed/mod.rs:497`; alktunnels
`src/producer.rs:838-854` — the three miss sites this amendment `src/producer.rs:838-854`: the three miss sites this amendment
converges (source for §The miss class above) converges (source for §The miss class above)
+49 -49
View File
@@ -1,9 +1,9 @@
--- ---
status: draft status: draft
last_updated: 2026-09-18 (OQ-TN-16 updated — ADR-008 Amendment 1: the correlation token) last_updated: 2026-09-18 (OQ-TN-16 updated per ADR-008 Amendment 1: the correlation token)
--- ---
# alktunnels — Open Questions # alktunnels: Open Questions
Centralized tracker. The Phase 0 OQ ledger (`docs/research/ Centralized tracker. The Phase 0 OQ ledger (`docs/research/
phase-0-findings.md`, OQ-TN-01..10) is promoted here with statuses phase-0-findings.md`, OQ-TN-01..10) is promoted here with statuses
@@ -15,7 +15,7 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
### OQ-TN-01: Target addressing format (open-op `params` shape) ### OQ-TN-01: Target addressing format (open-op `params` shape)
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-01) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-01)
- **Status**: resolved — [ADR-001](decisions/001-open-params-layout.md) - **Status**: resolved (see [ADR-001](decisions/001-open-params-layout.md))
- **Priority**: high - **Priority**: high
- **Resolution**: 2026-09-07 (Phase 1). `params` = self-contained - **Resolution**: 2026-09-07 (Phase 1). `params` = self-contained
JSON object: `{ "resource": <string>, "substrate": <string> }`. JSON object: `{ "resource": <string>, "substrate": <string> }`.
@@ -26,17 +26,17 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
+ survey input (ssh-socks5-survey §OQ-TN-01 residue) + both POCs. + survey input (ssh-socks5-survey §OQ-TN-01 residue) + both POCs.
- **Cross-references**: OQ-TN-02, OQ-TN-03, ADR-001, ADR-002 - **Cross-references**: OQ-TN-02, OQ-TN-03, ADR-001, ADR-002
### OQ-TN-02: Datagram substrates (UDP) — boundary preservation ### OQ-TN-02: Datagram substrates (UDP), boundary preservation
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-02) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-02)
- **Status**: resolved — [ADR-003](decisions/003-codec-and-udp-framing.md) - **Status**: resolved; see [ADR-003](decisions/003-codec-and-udp-framing.md)
- **Priority**: high - **Priority**: high
- **Resolution**: split by path (2026-09-06 survey) + F-2 mandate - **Resolution**: split by path (2026-09-06 survey) + F-2 mandate
(2026-09-07 reverse POC). Base open-op UDP resources: (2026-09-07 reverse POC). Base open-op UDP resources:
endpoint-at-open (one channel = one flow). Dynamic/`-D` UDP: endpoint-at-open (one channel = one flow). Dynamic/`-D` UDP:
per-datagram addressing inside the tunnel payload, assembly-layer. per-datagram addressing inside the tunnel payload, assembly-layer.
Boundary preservation: mandatory `[len: u16 BE]` per-datagram Boundary preservation: mandatory `[len: u16 BE]` per-datagram
framing — mandatory for correctness, not cosmetics (F-2; the full framing; this is mandatory for correctness, not cosmetics (F-2; the full
rationale and the executable pin live in ADR-003). Under the codec, rationale and the executable pin live in ADR-003). Under the codec,
`len=0` is a legal empty datagram (forward POC validated); EOF is `len=0` is a legal empty datagram (forward POC validated); EOF is
the channels-level sentinel; the layers never collide. the channels-level sentinel; the layers never collide.
@@ -46,7 +46,7 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-03) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-03)
- **Priority**: high - **Priority**: high
- **Status**: resolved — hub-owns-the-connection model (2026-09-05) + - **Status**: resolved: the hub-owns-the-connection model (2026-09-05) +
the reverse-flow POC (2026-09-07, `reverse-poc-summary.md`). the reverse-flow POC (2026-09-07, `reverse-poc-summary.md`).
No protocol-level direction: role follows the resource. `-L`/`-R` No protocol-level direction: role follows the resource. `-L`/`-R`
are the same open op with the entry point on different machines; are the same open op with the entry point on different machines;
@@ -60,37 +60,37 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-04) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-04)
- **Priority**: medium - **Priority**: medium
- **Status**: resolved — binding is always assembly-layer and - **Status**: resolved: binding is always assembly-layer and
optional, on either side; the protocol never binds. API surface: optional, on either side; the protocol never binds. API surface:
dial flows with no local bind (both POCs), listen flows where the dial flows with no local bind (both POCs), listen flows where the
binding is the producer's establisher (listen variant, ADR-004), binding is the producer's establisher (listen variant, ADR-004),
unbound/abstract flows (in-process pipes — the pump is generic over unbound/abstract flows (in-process pipes; the pump is generic over
boxed halves). The concrete produce/consume surface is spec'd in boxed halves). The concrete produce/consume surface is spec'd in
producer.md / consumer.md (ADR-005). producer.md / consumer.md (ADR-005).
- **Cross-references**: OQ-TN-03, ADR-004, ADR-005 - **Cross-references**: OQ-TN-03, ADR-004, ADR-005
## Theme 2: Crate architecture ## Theme 2: Crate architecture
### OQ-TN-05: Backend inversion point — `TunnelBackend` trait? ### OQ-TN-05: Backend inversion point (`TunnelBackend` trait?)
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-05) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-05)
- **Status**: resolved — [ADR-004](decisions/004-no-backend-trait.md) - **Status**: resolved: [ADR-004](decisions/004-no-backend-trait.md)
- **Priority**: medium - **Priority**: medium
- **Resolution**: 2026-09-07 (Phase 1). No trait for substrate access: - **Resolution**: 2026-09-07 (Phase 1). No trait for substrate access:
"produce boxed halves for a resource" is a function (UDP POC, "produce boxed halves for a resource" is a function (UDP POC,
OQ-TN-05 executable input). The only remaining thread — the hub OQ-TN-05 executable input). The only remaining thread, the hub
re-produce composition — resolves the same way: a hub proxy is a re-produce composition, resolves the same way: a hub proxy is a
consumer + producer composed at the assembly layer (the reverse consumer + producer composed at the assembly layer (the reverse
POC's hub is exactly this shape minus re-export); a composition POC's hub is exactly this shape minus re-export); a composition
helper is deferred(scope) until a concrete hub-proxy consumer helper is deferred(scope) until a concrete hub-proxy consumer
exists (OQ-TN-12). exists (OQ-TN-12).
- **Cross-references**: OQ-TN-12, ADR-004 - **Cross-references**: OQ-TN-12, ADR-004
### OQ-TN-06: The two-pump helper — extract now? ### OQ-TN-06: The two-pump helper (extract now?)
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-06) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-06)
- **Priority**: medium - **Priority**: medium
- **Status**: resolved (upstream) — `alkcall::channels::pump_bidi` - **Status**: resolved (upstream): `alkcall::channels::pump_bidi`
landed in alkcall 0.6.0 (ADR-050, review 007 R-03), shape as this landed in alkcall 0.6.0 (ADR-050, review 007 R-03), shape as this
OQ sketched it; this crate is cited as convergence input. alktunnels OQ sketched it; this crate is cited as convergence input. alktunnels
consumes the helper; hand-rolling is out. consumes the helper; hand-rolling is out.
@@ -101,7 +101,7 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
### OQ-TN-07: ALPN strategy ### OQ-TN-07: ALPN strategy
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-07) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-07)
- **Status**: resolved — [ADR-002](decisions/002-alpn-strategy.md) - **Status**: resolved: [ADR-002](decisions/002-alpn-strategy.md)
- **Priority**: high - **Priority**: high
- **Resolution**: 2026-09-07 (Phase 1). Option A: single `alk/tunnel` - **Resolution**: 2026-09-07 (Phase 1). Option A: single `alk/tunnel`
ALPN; the substrate discriminator in `params` selects the framing. ALPN; the substrate discriminator in `params` selects the framing.
@@ -113,8 +113,8 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
### OQ-TN-08: Access control and ownership scope ### OQ-TN-08: Access control and ownership scope
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-08) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-08)
- **Status**: resolved — [ADR-006](decisions/006-access-control-posture.md) - **Status**: resolved: [ADR-006](decisions/006-access-control-posture.md)
(2026-09-07 — the posture promoted to a decision). The resource is (2026-09-07; the posture promoted to a decision). The resource is
owned by the far side (alkcall op-level ACL applies as-is; no owned by the far side (alkcall op-level ACL applies as-is; no
tunnel-specific policy layer, no allowlists in v1; the ownership tunnel-specific policy layer, no allowlists in v1; the ownership
seam is the establisher seeing the per-call opener identity). seam is the establisher seeing the per-call opener identity).
@@ -134,7 +134,7 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-09) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-09)
- **Priority**: high - **Priority**: high
- **Status**: resolved (superseded by alkcall ADR-049, 0.5.0; narrowed - **Status**: resolved (superseded by alkcall ADR-049, 0.5.0; narrowed
through 0.6.0/0.7.0) — establishment is the open op's awaited phase; through 0.6.0/0.7.0): establishment is the open op's awaited phase;
dial failure is a typed `channel:open_failed` call error (reason ∈ dial failure is a typed `channel:open_failed` call error (reason ∈
`dial_failed` / `unknown_resource` / `resource_shortage` / `dial_failed` / `unknown_resource` / `resource_shortage` /
`handler_error` / `timeout`), never a phantom channel. Residual `handler_error` / `timeout`), never a phantom channel. Residual
@@ -148,35 +148,35 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
- **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-10) - **Origin**: [phase-0-findings.md](../research/phase-0-findings.md) (OQ-TN-10)
- **Priority**: low - **Priority**: low
- **Status**: resolved — UDP POC DONE (2026-09-06, 17 tests), - **Status**: resolved: the UDP POC is DONE (2026-09-06, 17 tests),
reverse-flow POC DONE (2026-09-07, 14 tests; re-validated over reverse-flow POC DONE (2026-09-07, 14 tests; re-validated over
alkcall 0.7.0, 16 tests). #3 split (OQ-TN-14): unix ships with the alkcall 0.7.0, 16 tests). #3 split (OQ-TN-14): unix ships with the
`local` feature; stdio is OUT — alktty's pipe mode owns process `local` feature; stdio is OUT, since alktty's pipe mode owns process
stdio (exit codes + signals are tty semantics). #4 (helper spike) stdio (exit codes + signals are tty semantics). #4 (helper spike)
resolved by alkcall ADR-050. Findings F-1 (plan payloads resolved by alkcall ADR-050. Findings F-1 (plan payloads
`Send + Sync` — documented upstream) and F-2 (UDP codec mandatory) `Send + Sync`, documented upstream) and F-2 (UDP codec mandatory)
recorded; W1 filed upstream and RESOLVED (CF-005/CF-006). recorded; W1 filed upstream and RESOLVED (CF-005/CF-006).
- **Cross-references**: ADR-003, OQ-TN-05, OQ-TN-06 - **Cross-references**: ADR-003, OQ-TN-05, OQ-TN-06
## Theme 4: Phase 1 residues (new) ## Theme 4: Phase 1 residues (new)
### OQ-TN-11: Resource naming — collision domain and lifecycle of a resource id ### OQ-TN-11: Resource naming (collision domain and lifecycle of a resource id)
- **Origin**: OQ-TN-01's residue (stable resource name vs target - **Origin**: OQ-TN-01's residue (stable resource name vs target
address), carried into Phase 1 address), carried into Phase 1
- **Status**: partially resolved — the collision-domain half is - **Status**: partially resolved: the collision-domain half is
**resolved by [ADR-001](decisions/001-open-params-layout.md)**: the **resolved by [ADR-001](decisions/001-open-params-layout.md)**: the
domain is the producer's registry (per-producer; two producers on domain is the producer's registry (per-producer; two producers on
the same connection may expose the same resource name independently the same connection may expose the same resource name independently;
— the open op resolves against the SERVING side's registry). The the open op resolves against the SERVING side's registry). The
lifecycle half stays open: is a resource registry a producer-side lifecycle half stays open: is a resource registry a producer-side
assembly construct (the POCs' `HashMap` — the leading candidate), assembly construct (the POCs' `HashMap`, the leading candidate),
or does it need protocol-level register/rename/retire ops? or does it need protocol-level register/rename/retire ops?
- **Priority**: medium - **Priority**: medium
- **Impacts**: producer.md's `ResourceRegistry` shape and the - **Impacts**: producer.md's `ResourceRegistry` shape and the
discovery story (OQ-TN-08); a protocol-level registry op would be a discovery story (OQ-TN-08); a protocol-level registry op would be a
new wire surface (one-way door). new wire surface (one-way door).
- **Resolution**: lifecycle half pending — no prior-art pressure - **Resolution**: lifecycle half pending; no prior-art pressure
(SSH lists no forwards in-band; discovery is the ops listing); (SSH lists no forwards in-band; discovery is the ops listing);
decide when a consumer needs shared/lifecycle-managed resources. decide when a consumer needs shared/lifecycle-managed resources.
- **Cross-references**: ADR-001, OQ-TN-08 - **Cross-references**: ADR-001, OQ-TN-08
@@ -184,7 +184,7 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
### OQ-TN-12: Hub re-produce composition helper ### OQ-TN-12: Hub re-produce composition helper
- **Origin**: OQ-TN-05's remaining thread (2026-09-05 hub model) - **Origin**: OQ-TN-05's remaining thread (2026-09-05 hub model)
- **Status**: resolved (upstream, 2026-09-18) — alkcall 0.8.0 ships - **Status**: resolved (upstream, 2026-09-18): alkcall 0.8.0 ships
the in-tree `ChannelRelay` + `HubLegTemplate`/`HubLegImports` the in-tree `ChannelRelay` + `HubLegTemplate`/`HubLegImports`
(alkcall ADR-051, review 008 Unit 3): the relay openable wraps (alkcall ADR-051, review 008 Unit 3): the relay openable wraps
from_call-imported marked specs on the consumer-leg registry via from_call-imported marked specs on the consumer-leg registry via
@@ -192,10 +192,10 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
teardown), the translate hop calls the producer leg with teardown), the translate hop calls the producer leg with
`forwarded_for` from the consumer's per-call identity, the `forwarded_for` from the consumer's per-call identity, the
byte-forward hop is `pump_bidi` awaited inline, the spoke reply's byte-forward hop is `pump_bidi` awaited inline, the spoke reply's
`channel_id` is stripped/replaced (other fields ride through — `channel_id` is stripped/replaced (other fields ride through, so
`bound` flows end-to-end per-hop truthful), and the adopted `bound` flows end-to-end per-hop truthful), and the adopted
producer-leg entry reclaims via a `Drop` guard on every plan path. producer-leg entry reclaims via a `Drop` guard on every plan path.
The composition helper a hub proxy needed is upstream — this crate The composition helper a hub proxy needed is upstream, and this crate
composes it (OQ-TN-05's resolution stands: no tunnel-side trait). composes it (OQ-TN-05's resolution stands: no tunnel-side trait).
Tracker task: `architecture/oq-tn-12-tracker` (tasks/architecture/). Tracker task: `architecture/oq-tn-12-tracker` (tasks/architecture/).
- **Priority**: low - **Priority**: low
@@ -205,9 +205,9 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
### OQ-TN-13: UDP truncation semantics at the codec boundary ### OQ-TN-13: UDP truncation semantics at the codec boundary
- **Origin**: forward POC Issues Surfaced #3 - **Origin**: forward POC Issues Surfaced #3
- **Status**: resolved — [ADR-003](decisions/003-codec-and-udp-framing.md) - **Status**: resolved: [ADR-003](decisions/003-codec-and-udp-framing.md)
(2026-09-07): **fail-loud at the adapter level.** A receive buffer (2026-09-07): **fail-loud at the adapter level.** A receive buffer
smaller than the datagram is an error, never a silent truncation — smaller than the datagram is an error, never a silent truncation;
a truncated datagram would corrupt the length-framed stream's a truncated datagram would corrupt the length-framed stream's
framing invariants (the reverse POC's F-2 rationale: the codec's framing invariants (the reverse POC's F-2 rationale: the codec's
wire discipline is what keeps empty datagrams unambiguous; silent wire discipline is what keeps empty datagrams unambiguous; silent
@@ -226,20 +226,20 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
- **Origin**: OQ-TN-10 #3 (the cheap un-POC'd item) - **Origin**: OQ-TN-10 #3 (the cheap un-POC'd item)
- **Priority**: low - **Priority**: low
- **Status**: **resolved 2026-09-07 (split by substrate):** - **Status**: **resolved 2026-09-07 (split by substrate):**
- **Unix: in scope for the `local` feature v1** — same halves shape - **Unix: in scope for the `local` feature v1**: same halves shape
as TCP (`dial_unix`), cheap, wire enum already carries `unix` as TCP (`dial_unix`), cheap, wire enum already carries `unix`
(ADR-001). Implemented by `tunnels/local-socket-halves`. (ADR-001). Implemented by `tunnels/local-socket-halves`.
- **Stdio: OUT of scope — alktty owns it.** A spawned process's - **Stdio: OUT of scope; alktty owns it.** A spawned process's
stdin/stdout/stderr IS a terminal session in alktty's model: stdin/stdout/stderr IS a terminal session in alktty's model:
`LocalTtyBackend`'s pipe mode (`tokio::process::Command` + `LocalTtyBackend`'s pipe mode (`tokio::process::Command` +
`Stdio::piped()`, alktty `tty-local.md`) is exactly the stdio `Stdio::piped()`, alktty `tty-local.md`) is exactly the stdio
case — three multiplexed logical streams + the exit-code case, with three multiplexed logical streams + the exit-code
control chunk (alktty ADR-004) + signal forwarding (REQ-TTY-02). control chunk (alktty ADR-004) + signal forwarding (REQ-TTY-02).
A "stdio bridge" here would be alktty's runner mode with the A "stdio bridge" here would be alktty's runner mode with the
terminal stripped out: a strictly worse duplicate that also terminal stripped out: a strictly worse duplicate that also
drops the semantics that matter (exit code, signals — a byte drops the semantics that matter (exit code, signals; a byte
tunnel has neither). Consumers needing to run commands remotely tunnel has neither). Consumers needing to run commands remotely
compose: alkcall dial → alktty session (pipe mode) — the two compose: alkcall dial → alktty session (pipe mode); the two
crates are siblings on the same channels substrate, not crates are siblings on the same channels substrate, not
competitors. (The OQ-TN-10 #3 "cheap item" was scoped before the competitors. (The OQ-TN-10 #3 "cheap item" was scoped before the
sibling relationship was this clear.) sibling relationship was this clear.)
@@ -256,16 +256,16 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
- **Origin**: `docs/research/tunnels-graduation.md` §2 (the alksocks - **Origin**: `docs/research/tunnels-graduation.md` §2 (the alksocks
graduation discussion, 2026-09-15) graduation discussion, 2026-09-15)
- **Priority**: high - **Priority**: high
- **Status**: resolved — [ADR-007](decisions/007-direct-open-op.md) - **Status**: resolved: [ADR-007](decisions/007-direct-open-op.md)
(2026-09-16). Option (a): a second `Sub`-typed open op (2026-09-16). Option (a): a second `Sub`-typed open op
`channels/tunnel/direct` on the same `alk/tunnel` ALPN, params `channels/tunnel/direct` on the same `alk/tunnel` ALPN, params
`{substrate, target}` (self-describing JSON target keyed by the `{substrate, target}` (self-describing JSON target keyed by the
sibling substrate — ATYP bytes rejected: not schema-describable, no sibling substrate; ATYP bytes rejected: not schema-describable, no
unix path form), scope `tunnel:direct` (never implied by unix path form), scope `tunnel:direct` (never implied by
`tunnel:open` — structural ACL separation), dial-only (no `tunnel:open`, which is the structural ACL separation), dial-only (no
listen/direct-BIND form — ingress is the listen + forwarded listen/direct-BIND form; ingress is the listen + forwarded
surface). The op-id convention needed an upstream alkcall amendment surface). The op-id convention needed an upstream alkcall amendment
(ADR-047's discovery derivation was `/sub`-shaped) — filed in (ADR-047's discovery derivation was `/sub`-shaped); it was filed in
`alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md` `alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md`
(ask #1) and **landed in alkcall 0.8.0** (ADR-047 amendment 3: (ask #1) and **landed in alkcall 0.8.0** (ADR-047 amendment 3:
flavor-form op ids + the explicit `channel_open_alpn` discovery flavor-form op ids + the explicit `channel_open_alpn` discovery
@@ -279,23 +279,23 @@ OQ-TN numbering. Format per `docs/sdd_process.md`.
BIND POC's two asks: bound-addr without consuming an accept; peer BIND POC's two asks: bound-addr without consuming an accept; peer
metadata on accepted handles) metadata on accepted handles)
- **Priority**: high - **Priority**: high
- **Status**: resolved — [ADR-008](decisions/008-listen-metadata-forwarded-op.md) - **Status**: resolved: [ADR-008](decisions/008-listen-metadata-forwarded-op.md)
(2026-09-16; **amended 2026-09-18** — Amendment 1). The (2026-09-16; **amended 2026-09-18**, Amendment 1). The
`forwarded-tcpip` shape adopted over per-ask carriers: (1) a `forwarded-tcpip` shape adopted over per-ask carriers: (1) a
bind-first listen establisher — establishment ends at bind time, so bind-first listen establisher: establishment ends at bind time, so
the open-op reply carries the real OS-chosen address via an the open-op reply carries the real OS-chosen address via an
additive `bound` field (ask #1; the carrier is the establisher → additive `bound` field (ask #1; the carrier is the establisher →
reply projection — filed as the review's ask #2 and **landed in reply projection, filed as the review's ask #2 and **landed in
alkcall 0.8.0** as ADR-049 amendment 3, alkcall 0.8.0** as ADR-049 amendment 3,
`Establishment::with_reply_field` + `open_channel_with_reply`), `Establishment::with_reply_field` + `open_channel_with_reply`),
and the address-first-vs-accept-first BIND deadlock + the 10s and the address-first-vs-accept-first BIND deadlock + the 10s
establishment-bound problem dissolve with the shape; (2) the establishment-bound problem dissolve with the shape; (2) the
forwarded op `channels/tunnel/forwarded` (accept-as-open) — the forwarded op `channels/tunnel/forwarded` (accept-as-open): the
producer opens a channel per accept with `{listen, substrate, producer opens a channel per accept with `{listen, substrate,
peer}` in params (ask #2's carrier; the `TargetHandle.peer` option peer}` in params (ask #2's carrier; the `TargetHandle.peer` option
is superseded). Amendment 1: the correlation key is a is superseded). Amendment 1: the correlation key is a
**producer-minted token** (`listen` reply field + forwarded **producer-minted token** (`listen` reply field + forwarded
param), not the listen channel's ID — the serving-side handler param), not the listen channel's ID; the serving-side handler
never sees its own channel ID (alkcall 0.8.0's `OpenHandler` never sees its own channel ID (alkcall 0.8.0's `OpenHandler`
surface) and generic relays don't rewrite params, so a surface) and generic relays don't rewrite params, so a
producer-minted token (which rides reply fields and params through producer-minted token (which rides reply fields and params through
+29 -29
View File
@@ -1,12 +1,12 @@
--- ---
status: draft status: draft
last_updated: 2026-09-18 (the graduation: the direct op, listen metadata, the forwarded op — ADR-007/008 as amended: the correlation token) last_updated: 2026-09-18 (the graduation: the direct op, listen metadata, the forwarded op; ADR-007/008 as amended: the correlation token)
--- ---
# alktunnels — Overview # alktunnels: Overview
Arbitrary bidirectional tunnels over alkcall channels: TCP, UDP, unix Arbitrary bidirectional tunnels over alkcall channels: TCP, UDP, unix
sockets, and other stream or datagram substrates — in the `ssh -L` / sockets, and other stream or datagram substrates, all in the `ssh -L` /
`ssh -D` / `ssh -R` sense. A producer/consumer protocol crate riding `ssh -D` / `ssh -R` sense. A producer/consumer protocol crate riding
alkcall channels the same way alktty does (`alk/tty` is the sibling alkcall channels the same way alktty does (`alk/tty` is the sibling
precedent). This document covers the crate's purpose, the resource precedent). This document covers the crate's purpose, the resource
@@ -28,8 +28,8 @@ channels and provides the typed consumer sessions:
halves (ADR-050, R-02's lifetime contract). Three ops on the one halves (ADR-050, R-02's lifetime contract). Three ops on the one
ALPN (ADR-002 Amendment 1): `channels/tunnel/sub` (the resource ALPN (ADR-002 Amendment 1): `channels/tunnel/sub` (the resource
op), `channels/tunnel/direct` (dynamic-target egress, ADR-007), and op), `channels/tunnel/direct` (dynamic-target egress, ADR-007), and
— served by the listen-opener side — `channels/tunnel/forwarded` `channels/tunnel/forwarded` (served by the listen-opener side;
(accept-as-open, ADR-008). Three establisher shapes: dial, listen accept-as-open, ADR-008). Three establisher shapes: dial, listen
(accept-first), and bind-first listen. (accept-first), and bind-first listen.
- **The consumer half** (`TunnelSession` / `TunnelListener`, - **The consumer half** (`TunnelSession` / `TunnelListener`,
ADR-005/008): opens tunnel channels (`ChannelClient::open_channel` ADR-005/008): opens tunnel channels (`ChannelClient::open_channel`
@@ -44,11 +44,11 @@ The guiding insight (the OQ-TN-01 reframe):
> A tunnel is a resource, not an address. Base params identify a > A tunnel is a resource, not an address. Base params identify a
> produced resource + substrate; the producer owns the backing. The > produced resource + substrate; the producer owns the backing. The
> one additive exception is the direct op (ADR-007): a consumer may > one additive exception is the direct op (ADR-007): a consumer may
> ask the producer to dial an arbitrary target — a different > ask the producer to dial an arbitrary target, and that is a
> capability, its own op and its own scope (`tunnel:direct`), never > different capability, its own op and its own scope (`tunnel:direct`), never
> implied by `tunnel:open`. Rich per-datagram addressing (SOCKS5 ATYP, > implied by `tunnel:open`. Rich per-datagram addressing (SOCKS5 ATYP,
> udpgw-style remotes) still enters only through the `-D`/dynamic > udpgw-style remotes) still enters only through the `-D`/dynamic
> composition path — inside the tunnel payload, never in the wire. > composition path, i.e. inside the tunnel payload, never in the wire.
## Why ## Why
@@ -57,15 +57,15 @@ consumers, the same role alktty plays for terminal sessions:
- A hub that runs agent workspaces exposes workers' services - A hub that runs agent workspaces exposes workers' services
(postgres, redis, gitea HTTP) as named resources; a coordinator (postgres, redis, gitea HTTP) as named resources; a coordinator
opens tunnels to them over alkcall channels — the VPN/SSH-tunnel opens tunnels to them over alkcall channels (the VPN/SSH-tunnel
service pattern without SSH. service pattern without SSH).
- A worker behind NAT dials the hub and serves its own open ops - A worker behind NAT dials the hub and serves its own open ops
(the reverse flow, `ssh -R`); the hub opens tunnel channels toward (the reverse flow, `ssh -R`); the hub opens tunnel channels toward
it per local accept. Both POCs validated this end-to-end. it per local accept. Both POCs validated this end-to-end.
- A `-D`-style consumer tunnels a SOCKS5 connection; the socks5 - A `-D`-style consumer tunnels a SOCKS5 connection; the socks5
server (assembly layer) does dynamic target selection inside the server (assembly layer) does dynamic target selection inside the
tunnel payload. tunnel payload.
- The protocol layer is substrate-agnostic by construction — the - The protocol layer is substrate-agnostic by construction: the
bookkeeping (target addressing, direction, lifecycle) never bookkeeping (target addressing, direction, lifecycle) never
hardcodes a substrate (AGENTS.md convention 7). TCP, UDP, unix hardcodes a substrate (AGENTS.md convention 7). TCP, UDP, unix
sockets, and in-process pipes ride the same open op, pump shape, sockets, and in-process pipes ride the same open op, pump shape,
@@ -77,10 +77,10 @@ A tunnel channel's life (full detail in [wire.md](wire.md)):
1. **Open (the establishment phase).** The consumer calls 1. **Open (the establishment phase).** The consumer calls
`channels/tunnel/sub` with `params = {resource, substrate}` `channels/tunnel/sub` with `params = {resource, substrate}`
(ADR-001) — or `channels/tunnel/direct` with (ADR-001), or `channels/tunnel/direct` with
`params = {substrate, target}` (ADR-007, dynamic egress). The `params = {substrate, target}` (ADR-007, dynamic egress). The
producer's establisher — the open op's awaited phase (alkcall producer's establisher (the open op's awaited phase, alkcall
ADR-049) — resolves the resource (or parses the target), dials the ADR-049) resolves the resource (or parses the target), dials the
substrate, and returns the handle in the plan. Failure is a typed substrate, and returns the handle in the plan. Failure is a typed
`channel:open_failed` (`dial_failed` / `unknown_resource` / `channel:open_failed` (`dial_failed` / `unknown_resource` /
`resource_shortage` / `handler_error` / `timeout`); the channel `resource_shortage` / `handler_error` / `timeout`); the channel
@@ -124,15 +124,15 @@ alktunnels (local feature) — non-wasm by design
Same posture as alktty: the default crate is protocol-only and Same posture as alktty: the default crate is protocol-only and
wasm-clean (AGENTS.md convention 4); substrate I/O is feature-gated wasm-clean (AGENTS.md convention 4); substrate I/O is feature-gated
(ADR-004). The wire format carries no alkcall-internal types (ADR-004). The wire format carries no alkcall-internal types
(alktty ADR-006's self-containment rule applies to the tunnel codec — (alktty ADR-006's self-containment rule applies to the tunnel codec,
trivial here, since the codec is a 2-byte length prefix). though it is trivial here, since the codec is a 2-byte length prefix).
## Feature Gates ## Feature Gates
| Feature | Contents | Wasm | | Feature | Contents | Wasm |
|---------|----------|------| |---------|----------|------|
| *(default)* | params, wire codec, open-op spec, establisher shapes, `TunnelSession` — protocol only | yes | | *(default)* | params, wire codec, open-op spec, establisher shapes, `TunnelSession` (protocol only) | yes |
| `local` | TCP/UDP/unix dial + listen helpers — halves-producing dial/listen functions (ADR-004). Unix ships (OQ-TN-14); stdio does NOT — a spawned process's stdio is alktty's pipe mode (exit codes, signals), not a tunnel | no | | `local` | TCP/UDP/unix dial + listen helpers, i.e. halves-producing dial/listen functions (ADR-004). Unix ships (OQ-TN-14); stdio does NOT: a spawned process's stdio is alktty's pipe mode (exit codes, signals), not a tunnel | no |
## Module Map ## Module Map
@@ -164,14 +164,14 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
| ADR | Decision | Summary | | ADR | Decision | Summary |
|-----|----------|---------| |-----|----------|---------|
| [001](decisions/001-open-params-layout.md) | Open-op params: resource + substrate | `{resource, substrate}` — the producer's stable name, not an address; wire-stable from the first consumer | | [001](decisions/001-open-params-layout.md) | Open-op params: resource + substrate | `{resource, substrate}`, the producer's stable name (not an address); wire-stable from the first consumer |
| [002](decisions/002-alpn-strategy.md) | Single `alk/tunnel` ALPN | Substrate in params selects the framing; no ALPN split, no API bifurcation | | [002](decisions/002-alpn-strategy.md) | Single `alk/tunnel` ALPN | Substrate in params selects the framing; no ALPN split, no API bifurcation |
| [003](decisions/003-codec-and-udp-framing.md) | Data-plane codec | Raw pass-through (stream) / mandatory `[len: u16 BE]` (UDP — F-2 mandate); `len=0` is a legal empty datagram | | [003](decisions/003-codec-and-udp-framing.md) | Data-plane codec | Raw pass-through (stream) / mandatory `[len: u16 BE]` (UDP; F-2 mandate); `len=0` is a legal empty datagram |
| [004](decisions/004-no-backend-trait.md) | No TunnelBackend trait | Halves functions at the assembly layer; listen is an establisher shape; trait re-evaluated at the 3-consumer threshold | | [004](decisions/004-no-backend-trait.md) | No TunnelBackend trait | Halves functions at the assembly layer; listen is an establisher shape; trait re-evaluated at the 3-consumer threshold |
| [005](decisions/005-consumer-session-owns-teardown.md) | Consumer session owns teardown | `TunnelSession` with close/join/Drop — the W3 gap closes structurally | | [005](decisions/005-consumer-session-owns-teardown.md) | Consumer session owns teardown | `TunnelSession` with close/join/Drop; the W3 gap closes structurally |
| [006](decisions/006-access-control-posture.md) | Access-control posture | The open gate is the boundary: `tunnel:open` scope (stable once published), op-level ACL, ownership seam, no allowlists in v1; amended — `tunnel:direct`/`tunnel:forwarded` scopes + the hub re-produce policy sentence | | [006](decisions/006-access-control-posture.md) | Access-control posture | The open gate is the boundary: `tunnel:open` scope (stable once published), op-level ACL, ownership seam, no allowlists in v1; amended with the `tunnel:direct`/`tunnel:forwarded` scopes + the hub re-produce policy sentence |
| [007](decisions/007-direct-open-op.md) | The direct open op | `channels/tunnel/direct`: dynamic-target egress, `{substrate, target}` params, `tunnel:direct` scope, dial-only — the ssh `direct-tcpip` capability with structural ACL separation | | [007](decisions/007-direct-open-op.md) | The direct open op | `channels/tunnel/direct`: dynamic-target egress, `{substrate, target}` params, `tunnel:direct` scope, dial-only (the ssh `direct-tcpip` capability with structural ACL separation) |
| [008](decisions/008-listen-metadata-forwarded-op.md) | Listen metadata + the forwarded op | Bind-first establisher (reply `bound` field = BND.ADDR fidelity; `listen` = the correlation token), `channels/tunnel/forwarded` accept-as-open (params carry `{listen, substrate, peer}` = reply#2 fidelity) — the `forwarded-tcpip` shape; amended — producer-minted correlation token supersedes the channel-ID key | | [008](decisions/008-listen-metadata-forwarded-op.md) | Listen metadata + the forwarded op | Bind-first establisher (reply `bound` field = BND.ADDR fidelity; `listen` = the correlation token), `channels/tunnel/forwarded` accept-as-open (params carry `{listen, substrate, peer}` = reply#2 fidelity); this is the `forwarded-tcpip` shape; amended so that a producer-minted correlation token supersedes the channel-ID key |
## Open Questions ## Open Questions
@@ -179,13 +179,13 @@ Open questions are tracked in [open-questions.md](open-questions.md).
Key questions affecting this document: Key questions affecting this document:
- **OQ-TN-11**: resource naming collision domain + lifecycle (partially resolved) - **OQ-TN-11**: resource naming collision domain + lifecycle (partially resolved)
- **OQ-TN-12**: hub re-produce composition helper (deferred(scope) — - **OQ-TN-12**: hub re-produce composition helper (deferred(scope);
the policy sentence is pinned: the hub's ACL governs the final dial) the policy sentence is pinned: the hub's ACL governs the final dial)
- **OQ-TN-13**: UDP truncation semantics (resolved — fail-loud, ADR-003) - **OQ-TN-13**: UDP truncation semantics (resolved, fail-loud, ADR-003)
- **OQ-TN-14**: unix/stdio placement (resolved — unix in via - **OQ-TN-14**: unix/stdio placement (resolved; unix in via
`local`, stdio out: alktty's pipe mode owns process stdio) `local`, stdio out: alktty's pipe mode owns process stdio)
- **OQ-TN-15**: the direct op (resolved — ADR-007) - **OQ-TN-15**: the direct op (resolved; ADR-007)
- **OQ-TN-16**: listen metadata (resolved — ADR-008) - **OQ-TN-16**: listen metadata (resolved; ADR-008)
## References ## References
+66 -66
View File
@@ -1,14 +1,14 @@
--- ---
status: draft status: draft
last_updated: 2026-11-03 (the forwarded op's serving-side establishment last_updated: 2026-11-03 (the forwarded op's serving-side establishment
phase + the correlation-miss vocabulary — ADR-008 Amendment 2) phase + the correlation-miss vocabulary, ADR-008 Amendment 2)
--- ---
# alktunnels — Producer Half # alktunnels: Producer Half
The producer half: the `channels/tunnel/sub` open op — its spec, its The producer half: the `channels/tunnel/sub` open op, its spec, its
establishers (dial + listen shapes), its pump handler, and its establishers (dial + listen shapes), its pump handler, and its
registration — plus the sibling ops on the same ALPN: the direct op registration, plus the sibling ops on the same ALPN: the direct op
(`channels/tunnel/direct`, dynamic-target egress) and the forwarded op (`channels/tunnel/direct`, dynamic-target egress) and the forwarded op
(`channels/tunnel/forwarded`, accept-as-open). The producer is whoever (`channels/tunnel/forwarded`, accept-as-open). The producer is whoever
can reach the tunnel target; connection direction is irrelevant can reach the tunnel target; connection direction is irrelevant
@@ -44,16 +44,16 @@ ACL, allocates the channel (connection-owner rule, ADR-047 §5), runs
the establisher as the awaited bounded establishment phase, then the establisher as the awaited bounded establishment phase, then
spawns the pump handler. The generic channel ops spawns the pump handler. The generic channel ops
(`channel/close`, `channel/control`, `channel/resources/subscribe`) (`channel/close`, `channel/control`, `channel/resources/subscribe`)
register separately via `ChannelOperations::register_on` — the register separately via `ChannelOperations::register_on`; the
producer (or its assembly layer) MUST register them on the serving producer (or its assembly layer) MUST register them on the serving
registry so peers can manage channels out-of-band (the recipe alkcall registry so peers can manage channels out-of-band (the recipe alkcall
review 007 §Part B verified — `alkcall/docs/reviews/ review 007 §Part B verified; see `alkcall/docs/reviews/
007-establishment-follow-ups-review.md`; R-01 = the `Establishment` 007-establishment-follow-ups-review.md`; R-01 = the `Establishment`
plan payload, R-02 = the `OpenHandler` lifetime contract). plan payload, R-02 = the `OpenHandler` lifetime contract).
**Registration timing:** the openable may register AFTER **Registration timing:** the openable may register AFTER
`from_connection_with_serving` returns (the connect-side serving `from_connection_with_serving` returns (the connect-side serving
posture — the reverse POC's W2): the dispatcher reads through the posture; the reverse POC's W2): the dispatcher reads through the
shared `Arc<OperationRegistry>` per dispatch. Late registration is a shared `Arc<OperationRegistry>` per dispatch. Late registration is a
supported assembly shape, not a race. supported assembly shape, not a race.
@@ -61,7 +61,7 @@ supported assembly shape, not a race.
Dynamic-target egress (ADR-007): the same ALPN, a second `Sub`-typed Dynamic-target egress (ADR-007): the same ALPN, a second `Sub`-typed
open op whose establisher dials a per-call target instead of resolving open op whose establisher dials a per-call target instead of resolving
a resource. One ALPN, two ops, two scopes — dial-what-the-producer- a resource. One ALPN, two ops, two scopes: dial-what-the-producer-
registered (`tunnel:open`) and dial-anything (`tunnel:direct`) are registered (`tunnel:open`) and dial-anything (`tunnel:direct`) are
different capabilities. different capabilities.
@@ -77,11 +77,11 @@ access control: required_scopes: ["tunnel:direct"]
description: SHOULD carry a human-readable hint (dynamic-target egress) description: SHOULD carry a human-readable hint (dynamic-target egress)
``` ```
- **No registry lookup** — the establisher parses `target` against - **No registry lookup**: the establisher parses `target` against
`substrate` (malformed → `invalid_input`-class; `unknown_resource` `substrate` (malformed → `invalid_input`-class; `unknown_resource`
can never fire) and injects the parsed `TunnelTarget` into the same can never fire) and injects the parsed `TunnelTarget` into the same
assembly-provided `DialFn` the base op's backings resolve to. assembly-provided `DialFn` the base op's backings resolve to.
- **Dial-only** — no listen/direct-BIND form; the ingress capability - **Dial-only**: no listen/direct-BIND form; the ingress capability
is the listen + forwarded surface (ADR-008). is the listen + forwarded surface (ADR-008).
- Registration: `register_tunnel_direct_openable` (same wrapper, same - Registration: `register_tunnel_direct_openable` (same wrapper, same
plan-flow; the spec builder is public for assembly variants). plan-flow; the spec builder is public for assembly variants).
@@ -91,9 +91,9 @@ description: SHOULD carry a human-readable hint (dynamic-target egress)
The establisher is the open op's awaited, bounded establishment phase The establisher is the open op's awaited, bounded establishment phase
(alkcall ADR-049 §1/§2): validate params semantically, consult (alkcall ADR-049 §1/§2): validate params semantically, consult
ownership if wired, dial/accept the substrate, and return ownership if wired, dial/accept the substrate, and return
`Establishment::new(plan)` — the dialed handle rides the typed-opaque `Establishment::new(plan)`; the dialed handle rides the typed-opaque
plan (`ChannelPlan = Arc<dyn Any + Send + Sync>`; the `+ Sync` bound plan (`ChannelPlan = Arc<dyn Any + Send + Sync>`; the `+ Sync` bound
constrains plan payloads — socket-backed handles carry it naturally, constrains plan payloads; socket-backed handles carry it naturally,
reverse POC F-1, documented upstream). On failure: reverse POC F-1, documented upstream). On failure:
`EstablishmentError` (the typed reason codes) → the wrapper tears the `EstablishmentError` (the typed reason codes) → the wrapper tears the
channel down and replies `channel:open_failed`. channel down and replies `channel:open_failed`.
@@ -126,15 +126,15 @@ listener; a hub exposing its own port to a third party): the same
open op, the same params; the establisher pops the next accepted open op, the same params; the establisher pops the next accepted
connection from an assembly-owned listener queue and returns it as connection from an assembly-owned listener queue and returns it as
the plan payload. The listener lifecycle (bind, accept loop, cancel) the plan payload. The listener lifecycle (bind, accept loop, cancel)
is assembly code — the protocol never binds (OQ-TN-04). Error is assembly code; the protocol never binds (OQ-TN-04). Error
mapping: an empty queue during establishment is `resource_shortage`; mapping: an empty queue during establishment is `resource_shortage`;
a closed listener is `dial_failed`-class. No new wire surface — the a closed listener is `dial_failed`-class. No new wire surface: the
listen variant is plan-flow with a different halves source. listen variant is plan-flow with a different halves source.
**The accept-first shape stays** (ADR-008 §5): for producers whose **The accept-first shape stays** (ADR-008 §5): for producers whose
resource backing IS an accept queue (an externally-fed or pre-bound resource backing IS an accept queue (an externally-fed or pre-bound
listener shared across opens), this remains the correct shape — the listener shared across opens), this remains the correct shape: the
POC #5 composed-BIND shape. Its limits (no `bound` in the reply — the POC #5 composed-BIND shape. Its limits (no `bound` in the reply: the
accept hasn't happened at establishment; the POC's accept hasn't happened at establishment; the POC's
establishment-bound-vs-wait problem) are inherent to accepting-first. establishment-bound-vs-wait problem) are inherent to accepting-first.
@@ -142,32 +142,32 @@ establishment-bound-vs-wait problem) are inherent to accepting-first.
For a producer that OWNS the bind decision (the common hub case; For a producer that OWNS the bind decision (the common hub case;
alksocks' composed BIND): the establisher **binds the listener** (via alksocks' composed BIND): the establisher **binds the listener** (via
the assembly-provided listen function — the protocol never binds, the assembly-provided listen function; the protocol never binds,
OQ-TN-04) and returns the listener handle as the plan payload. OQ-TN-04) and returns the listener handle as the plan payload.
Establishment is fast — bounded by the bind, not by any app-server Establishment is fast (bounded by the bind, not by any app-server
dial: dial):
- **The establishment phase ends at bind time**, so the open-op reply - **The establishment phase ends at bind time**, so the open-op reply
can carry the REAL OS-chosen address: the establisher contributes can carry the REAL OS-chosen address: the establisher contributes
`"bound": {host, port}` (or `{path}`) to the reply — the `"bound": {host, port}` (or `{path}`) to the reply: the
establisher → reply projection (ADR-049 amendment 3, establisher → reply projection (ADR-049 amendment 3,
`Establishment::with_reply_field`; the `channel_id` key is `Establishment::with_reply_field`; the `channel_id` key is
wrapper-reserved). **Landed in alkcall 0.8.0** (review 008 U-2); wrapper-reserved). **Landed in alkcall 0.8.0** (review 008 U-2);
consumers read it via `ChannelClient::open_channel_with_reply`. consumers read it via `ChannelClient::open_channel_with_reply`.
- **The establisher mints the correlation token** (ADR-008 - **The establisher mints the correlation token** (ADR-008
Amendment 1): a fresh u32 per open, contributed as the `"listen"` Amendment 1): a fresh u32 per open, contributed as the `"listen"`
reply field (the same projection) and carried in the plan — the reply field (the same projection) and carried in the plan; the
accept dispatcher reads it from the plan and stamps it into each accept dispatcher reads it from the plan and stamps it into each
forwarded open's `listen` param. The plan carries forwarded open's `listen` param. The plan carries
`{listener, token, substrate}` (a bind-first plan type; the `{listener, token, substrate}` (a bind-first plan type; the
substrate is stamped at establisher time — the dispatcher reads it substrate is stamped at establisher time, and the dispatcher reads it
from the plan, never re-parses wire input); the token is from the plan, never re-parses wire input); the token is
relay-transparent by construction (reply fields and params both relay-transparent by construction (reply fields and params both
pass hubs untouched — alkcall ADR-051; ADR-008 Amendment 1 has the pass hubs untouched per alkcall ADR-051; ADR-008 Amendment 1 has the
why-superseded of the channel-ID key). why-superseded of the channel-ID key).
- **The pump handler for this shape is an accept dispatcher**, not - **The pump handler for this shape is an accept dispatcher**, not
`pump_bidi`: it accepts the channel's `BiStream` (the R-02 `pump_bidi`: it accepts the channel's `BiStream` (the R-02
contract), then runs the assembly-provided accept loop — each contract), then runs the assembly-provided accept loop; each
accepted connection produces a forwarded-channel open toward the accepted connection produces a forwarded-channel open toward the
listen opener (§The Forwarded Op below); the accepted halves pump listen opener (§The Forwarded Op below); the accepted halves pump
against the adopted channel. The listen channel's own `BiStream` against the adopted channel. The listen channel's own `BiStream`
@@ -179,22 +179,22 @@ dial:
- The address-first-vs-accept-first BIND deadlock (the alksocks POC's - The address-first-vs-accept-first BIND deadlock (the alksocks POC's
one real discovery) DISSOLVES in this shape: the open completes at one real discovery) DISSOLVES in this shape: the open completes at
bind time, reply#1-class metadata (the `bound` field) precedes any bind time, reply#1-class metadata (the `bound` field) precedes any
app-server dial, and the accept wait lives in the pump phase — app-server dial, and the accept wait lives in the pump phase:
teardown-governed, not establishment-bounded (the 10s bound problem teardown-governed, not establishment-bounded (the 10s bound problem
dissolves with it). dissolves with it).
**Identity:** the establisher receives the **per-call opener **Identity:** the establisher receives the **per-call opener
identity** (the dispatch-resolved identity overlaid onto the identity** (the dispatch-resolved identity overlaid onto the
install-time context — alkcall 0.7.0 CF-006). On hub-forwarded opens install-time context, alkcall 0.7.0 CF-006). On hub-forwarded opens
it sees the end client, not the hub. `auth` carries the opener's it sees the end client, not the hub. `auth` carries the opener's
identity plus the transport-truthful fields (`alpn`, `remote_addr`, identity plus the transport-truthful fields (`alpn`, `remote_addr`,
`tls_client_fingerprint`) — never rewritten. Consult it for `tls_client_fingerprint`); these are never rewritten. Consult it for
ownership-scoped resources if the assembly layer wants ownership-scoped resources if the assembly layer wants
`OwnershipProvider` checks here (OQ-TN-08's optional half). `OwnershipProvider` checks here (OQ-TN-08's optional half).
**Concurrency:** each open's establisher result flows to ITS pump **Concurrency:** each open's establisher result flows to ITS pump
handler — concurrent opens of the same resource are independent handler; concurrent opens of the same resource are independent
(the R-01 plan flow — alkcall review 007 R-01, the (the R-01 plan flow per alkcall review 007 R-01, the
`Establishment::new(plan)` payload; the forward POC's resource-keyed `Establishment::new(plan)` payload; the forward POC's resource-keyed
handoff race is structurally gone, confirmed under concurrency by the handoff race is structurally gone, confirmed under concurrency by the
reverse POC). reverse POC).
@@ -206,27 +206,27 @@ The `OpenHandler` (alkcall ADR-049/050 shape:
JoinHandle<()>`): JoinHandle<()>`):
1. Downcast the plan to the concrete handle type 1. Downcast the plan to the concrete handle type
(`Arc::downcast::<TargetHandle>` — establisher and handler agree (`Arc::downcast::<TargetHandle>`, so establisher and handler agree
on the type; alkcall never inspects it). on the type; alkcall never inspects it).
2. `conn.accept_bi()` — the channel's yield-once `BiStream`. 2. `conn.accept_bi()`: the channel's yield-once `BiStream`.
3. `pump_bidi(bidi, handle.read, handle.write)` — **awaited inline** 3. `pump_bidi(bidi, handle.read, handle.write)`: **awaited inline**
inside the handler's task. The returned `JoinHandle` MUST track inside the handler's task. The returned `JoinHandle` MUST track
the data-plane lifetime (R-02): the wrapper awaits it and its the data-plane lifetime (R-02): the wrapper awaits it and its
completion triggers channel teardown. **Early return = completion triggers channel teardown. **Early return =
teardown-at-birth** — both pumps see instant EOF with zero bytes; teardown-at-birth**: both pumps see instant EOF with zero bytes;
the "spawn-and-forget" shape is the one bug class the R-02 the "spawn-and-forget" shape is the one bug class the R-02
telemetry (yield-once flag + birth-teardown debug hint) diagnoses telemetry (yield-once flag + birth-teardown debug hint) diagnoses
at runtime. Hand-rolling the two-pump shape is out (ADR-050). at runtime. Hand-rolling the two-pump shape is out (ADR-050).
4. UDP: the plan payload is the FRAMED adapter (ADR-003) — the pump 4. UDP: the plan payload is the FRAMED adapter (ADR-003), so the pump
is unchanged; the codec lives at the substrate/pump boundary. is unchanged; the codec lives at the substrate/pump boundary.
5. **Bind-first listen handler (the accept dispatcher, ADR-008 §1):** 5. **Bind-first listen handler (the accept dispatcher, ADR-008 §1):**
the plan payload is the listener; after `accept_bi()`, the handler the plan payload is the listener; after `accept_bi()`, the handler
runs the accept loop (assembly-provided) concurrently with holding runs the accept loop (assembly-provided) concurrently with holding
the channel's `BiStream` — each accepted connection produces a the channel's `BiStream`; each accepted connection produces a
forwarded-channel open toward the listen opener. The listen forwarded-channel open toward the listen opener. The listen
channel's own stream stays idle (no pumps on it); its EOF cancels channel's own stream stays idle (no pumps on it); its EOF cancels
the loop (the listener tears down with the session). Per-accept the loop (the listener tears down with the session). Per-accept
data planes are still `pump_bidi` (convention 8 — per-channel, data planes are still `pump_bidi` (convention 8, per-channel,
never a hand-rolled loop over one stream). never a hand-rolled loop over one stream).
**The handler is substrate-agnostic by construction:** it cannot know **The handler is substrate-agnostic by construction:** it cannot know
@@ -237,18 +237,18 @@ in-process pipe, and must not (AGENTS.md convention 7).
Accept-as-open: when the bind-first listener accepts, the PRODUCER Accept-as-open: when the bind-first listener accepts, the PRODUCER
opens a channel back toward the listen opener. The opener SERVES the opens a channel back toward the listen opener. The opener SERVES the
op (`channels/tunnel/forwarded`, scope `tunnel:forwarded` — ingress; op (`channels/tunnel/forwarded`, scope `tunnel:forwarded`, ingress;
Amendment 2's two serving shapes: the recommended correlation-gate Amendment 2's two serving shapes: the recommended correlation-gate
establisher variant — a stale/missing token fails the open as a establisher variant, where a stale/missing token fails the open as a
typed `channel:open_failed`/`unknown_resource` before the channel typed `channel:open_failed`/`unknown_resource` before the channel
is born — or the base establisher-less shape, the data plane starts is born, or the base establisher-less shape, where the data plane starts
immediately and the assembly's post-open decline owns the miss): immediately and the assembly's post-open decline owns the miss):
1. The producer's per-accept open carries `{listen, substrate, peer}` 1. The producer's per-accept open carries `{listen, substrate, peer}`:
— the correlation token from the plan (the value the establisher the correlation token from the plan (the value the establisher
minted; the opener matches it against the `listen` field of its minted; the opener matches it against the `listen` field of its
open listener sessions — ADR-008 Amendment 1), the substrate open listener sessions per ADR-008 Amendment 1), the substrate
(also from the plan — stamped at establisher time from the one (also from the plan, stamped at establisher time from the one
successful params parse; the dispatcher never re-parses wire successful params parse; the dispatcher never re-parses wire
input), the accepted connection's remote address (reply#2-class input), the accepted connection's remote address (reply#2-class
fidelity's carrier). fidelity's carrier).
@@ -256,19 +256,19 @@ immediately and the assembly's post-open decline owns the miss):
adopts it (`TunnelSession::adopt`) and pumps the accepted halves adopts it (`TunnelSession::adopt`) and pumps the accepted halves
against it (`pump_against`). against it (`pump_against`).
3. The opener-side handler correlates the open (via `listen`) to the 3. The opener-side handler correlates the open (via `listen`) to the
waiting session (assembly-side bookkeeping — the protocol provides waiting session (assembly-side bookkeeping; the protocol provides
the op and params, not the wait state machine), accepts the the op and params, not the wait state machine), accepts the
`BiStream`, and pumps against the local session's halves. Under `BiStream`, and pumps against the local session's halves. Under
the establisher variant (Amendment 2) that correlation lookup the establisher variant (Amendment 2) that correlation lookup
rides the gate at establisher time instead — the handler-side rides the gate at establisher time instead; the handler-side
correlation remains only for the narrow residual race (a token correlation remains only for the narrow residual race (a token
vanishing between gate pass and handler run; the miss class is vanishing between gate pass and handler run; the miss class is
named `correlation-miss` at `debug`, assembly-side). named `correlation-miss` at `debug`, assembly-side).
4. Identity: the forwarded op's opener-side gate sees the PRODUCER's 4. Identity: the forwarded op's opener-side gate sees the PRODUCER's
per-call identity (CF-005 precedence — the producer may carry an per-call identity (CF-005 precedence; the producer may carry an
`auth_token` when it is itself forwarding); the peer address in `auth_token` when it is itself forwarding); the peer address in
params is producer-asserted data, not transport truth — the ACL params is producer-asserted data, not transport truth; the ACL
trusts the producer's grant, the application treats `peer` as trusts the producer's grant, and the application treats `peer` as
metadata. metadata.
## Registration API (the spec surface) ## Registration API (the spec surface)
@@ -279,7 +279,7 @@ pub fn register_tunnel_openable(
registry: &ResourceRegistry, // assembly-owned: (resource, substrate) -> backing registry: &ResourceRegistry, // assembly-owned: (resource, substrate) -> backing
on_registry: &Arc<OperationRegistry>, // the session's dispatch registry on_registry: &Arc<OperationRegistry>, // the session's dispatch registry
// (the per-session fork per alkcall ADR-047 §4 // (the per-session fork per alkcall ADR-047 §4
// amendment, 2026-09-03 — NOT the connection // amendment, 2026-09-03, NOT the connection
// overlay; Layer-2-registered open ops resolve // overlay; Layer-2-registered open ops resolve
// NOT_FOUND, review 004 F-01) // NOT_FOUND, review 004 F-01)
auth: AuthContext, // the install-time context (per-call overlay is upstream's) auth: AuthContext, // the install-time context (per-call overlay is upstream's)
@@ -310,7 +310,7 @@ pub fn register_tunnel_bindfirst_openable( // bind-first (shape 3, ADR-008)
- `ResourceRegistry` (assembly construct, OQ-TN-11): maps - `ResourceRegistry` (assembly construct, OQ-TN-11): maps
`(resource, substrate)` → backing. The POCs' `HashMap` shape is the `(resource, substrate)` → backing. The POCs' `HashMap` shape is the
candidate; the collision domain is the producer's registry candidate; the collision domain is the producer's registry
(ADR-001 — two producers on one connection may expose the same (per ADR-001, two producers on one connection may expose the same
resource name independently). resource name independently).
- The scope constants `TUNNEL_OPEN_SCOPE = "tunnel:open"`, - The scope constants `TUNNEL_OPEN_SCOPE = "tunnel:open"`,
`TUNNEL_DIRECT_SCOPE = "tunnel:direct"`, and `TUNNEL_DIRECT_SCOPE = "tunnel:direct"`, and
@@ -321,7 +321,7 @@ pub fn register_tunnel_bindfirst_openable( // bind-first (shape 3, ADR-008)
## Access control posture ## Access control posture
Tunnels reach local networks — the open gate is the security boundary Tunnels reach local networks; the open gate is the security boundary
(OQ-TN-08, ADR-006 + Amendment 1): (OQ-TN-08, ADR-006 + Amendment 1):
- The registry runs `AccessControl` before the establisher (the ACL - The registry runs `AccessControl` before the establisher (the ACL
@@ -331,26 +331,26 @@ Tunnels reach local networks — the open gate is the security boundary
`TUNNEL_FORWARDED_SCOPE` gates ingress (receiving forwarded `TUNNEL_FORWARDED_SCOPE` gates ingress (receiving forwarded
connections) on the opener side. Three scopes, three capabilities. connections) on the opener side. Three scopes, three capabilities.
- The assembly layer MAY add resource-scoped checks via the - The assembly layer MAY add resource-scoped checks via the
establisher (ownership on the resource name) — the protocol establisher (ownership on the resource name); the protocol
provides the seam, not the policy. For the direct op the seam is provides the seam, not the policy. For the direct op the seam is
the parsed `TunnelTarget` (a deployment MAY enforce target the parsed `TunnelTarget` (a deployment MAY enforce target
allowlists in its establisher — policy, not protocol). allowlists in its establisher; policy, not protocol).
- No target allowlists in the PROTOCOL, no tunnel-specific policy - No target allowlists in the PROTOCOL, no tunnel-specific policy
machinery in v1 — the far side owns the resource (OQ-TN-08 machinery in v1, as the far side owns the resource (OQ-TN-08
resolution); discovery is the ACL-filtered ops listing. resolution); discovery is the ACL-filtered ops listing.
- **Hub re-produce policy (the security model sentence, ADR-006 - **Hub re-produce policy (the security model sentence, ADR-006
Amendment 1):** a hub re-producing the direct op applies ITS OWN Amendment 1):** a hub re-producing the direct op applies ITS OWN
target policy at the re-produce hop — the hub's ACL governs the target policy at the re-produce hop; the hub's ACL governs the
final dial. The hub is the network boundary. final dial. The hub is the network boundary.
## Open Questions ## Open Questions
- **OQ-TN-11**: resource naming collision domain + lifecycle - **OQ-TN-11**: resource naming collision domain + lifecycle
(partially resolved — the per-producer registry is the leading (partially resolved; the per-producer registry is the leading
candidate) candidate)
- **OQ-TN-14**: resolved (split: unix in, stdio out — alktty's pipe - **OQ-TN-14**: resolved (split: unix in, stdio out, with alktty's pipe
mode owns process stdio; see open-questions.md) mode owning process stdio; see open-questions.md)
- **OQ-TN-15/16**: resolved (ADR-007/008 — the direct op, listen - **OQ-TN-15/16**: resolved (ADR-007/008: the direct op, listen
metadata, the forwarded op) metadata, the forwarded op)
## References ## References
@@ -364,7 +364,7 @@ Tunnels reach local networks — the open gate is the security boundary
- alkcall ADR-047/049 (open ops + establishment), ADR-050 (`pump_bidi`), - alkcall ADR-047/049 (open ops + establishment), ADR-050 (`pump_bidi`),
ledger CF-005/CF-006 (identity precedence + per-call opener) ledger CF-005/CF-006 (identity precedence + per-call opener)
- Upstream asks (review 008): `alkcall/docs/reviews/ - Upstream asks (review 008): `alkcall/docs/reviews/
008-tunnels-graduation-upstream-review.md` — both landed in 008-tunnels-graduation-upstream-review.md`; both landed in
alkcall 0.8.0 (flavor-form discovery derivation, ADR-047 alkcall 0.8.0 (flavor-form discovery derivation, ADR-047
amendment 3; the establisher → reply `bound` projection, ADR-049 amendment 3; the establisher → reply `bound` projection, ADR-049
amendment 3), plus the in-tree `ChannelRelay`/`HubLegTemplate` amendment 3), plus the in-tree `ChannelRelay`/`HubLegTemplate`
+58 -58
View File
@@ -3,12 +3,12 @@ status: draft
last_updated: 2026-09-20 (review 002 D-1/D-2 sync: the params schemas' port bounds, `additionalProperties: false` gates, `listen` `minimum: 1`, and the registry-side gate ordering recorded; ADR-007's oneOf wording corrected to the shipped allOf/if-then) last_updated: 2026-09-20 (review 002 D-1/D-2 sync: the params schemas' port bounds, `additionalProperties: false` gates, `listen` `minimum: 1`, and the registry-side gate ordering recorded; ADR-007's oneOf wording corrected to the shipped allOf/if-then)
--- ---
# alktunnels — Wire Format # alktunnels: Wire Format
The `alk/tunnel` wire surface. Two layers: the **open op** (JSON The `alk/tunnel` wire surface. Two layers: the **open op** (JSON
params on the channels call plane — structured, schema-validated, params on the channels call plane: structured, schema-validated,
typed errors) and the **data plane** (bytes/datagrams inside the typed errors) and the **data plane** (bytes/datagrams inside the
channel `BiStream` — the codec, ADR-003). The channels layer strips channel `BiStream`, the codec, ADR-003). The channels layer strips
its own 8-byte header transparently (alknet ADR-093); the tunnel its own 8-byte header transparently (alknet ADR-093); the tunnel
protocol owns everything inside the `BiStream`. protocol owns everything inside the `BiStream`.
@@ -21,7 +21,7 @@ this document is the normative WHAT.
`Sub`-typed operation (alkcall ADR-047): the reply carries `Sub`-typed operation (alkcall ADR-047): the reply carries
`{ "channel_id": <u32> }` once; the data plane flows on the channel's `{ "channel_id": <u32> }` once; the data plane flows on the channel's
`BiStream` — never in the call response stream. `BiStream`, never in the call response stream.
### Input (params) ### Input (params)
@@ -32,33 +32,33 @@ this document is the normative WHAT.
} }
``` ```
- `resource`: string, required — the producer's stable resource name - `resource`: string, required: the producer's stable resource name
(NOT an address; the producer's registry maps it to its backing). (NOT an address; the producer's registry maps it to its backing).
- `substrate`: string, required, enum `["tcp", "udp", "unix"]` — the - `substrate`: string, required, enum `["tcp", "udp", "unix"]`: the
extensible discriminator (ADR-001); selects the data-plane framing. extensible discriminator (ADR-001); selects the data-plane framing.
The schema ships with all three values in v1 (unix has an The schema ships with all three values in v1 (unix has an
implementation path — OQ-TN-14). An older producer rejecting a implementation path, OQ-TN-14). An older producer rejecting a
newer substrate value is the SSH "unknown channel type" posture — newer substrate value is the SSH "unknown channel type" posture:
loud, not silent. loud, not silent.
- `input_schema` pins both as required strings; unknown substrate - `input_schema` pins both as required strings; unknown substrate
values fail schema validation (typed error — loud, not silent). values fail schema validation (typed error, loud, not silent).
- Extensibility: a new substrate is a new enum value, not a format - Extensibility: a new substrate is a new enum value, not a format
change. An older producer rejecting a newer substrate is the SSH change. An older producer rejecting a newer substrate is the SSH
"unknown channel type" posture. "unknown channel type" posture.
### Reply / errors ### Reply / errors
- Success: `{ "channel_id": <u32> }` — the channel is open, the - Success: `{ "channel_id": <u32> }`: the channel is open, the
establishment phase (dial) succeeded, the pump handler is running. establishment phase (dial) succeeded, the pump handler is running.
Bind-first listen opens MAY add `"bound"` (§The Listen Open Op's Bind-first listen opens MAY add `"bound"` (§The Listen Open Op's
Reply below). Reply below).
- Failure: a typed `channel:open_failed` call error with - Failure: a typed `channel:open_failed` call error with
`details: { reason, message }` — `reason ∈ dial_failed` / `details: { reason, message }` with `reason ∈ dial_failed` /
`unknown_resource` / `resource_shortage` / `handler_error` / `unknown_resource` / `resource_shortage` / `handler_error` /
`timeout` (alkcall ADR-049 §3). The registry's ACL gate runs before `timeout` (alkcall ADR-049 §3). The registry's ACL gate runs before
the wrapper (`FORBIDDEN` for a scope-less identity); the 256-channel the wrapper (`FORBIDDEN` for a scope-less identity); the 256-channel
cap is `channel:too_many_channels`. **A failed open never returns a cap is `channel:too_many_channels`. **A failed open never returns a
`channel_id`** — no phantom channel (the SSH contract, POC-verified `channel_id`**; no phantom channel (the SSH contract, POC-verified
from the consumer side). from the consumer side).
- The scope gate: the open op's `AccessControl.required_scopes` is - The scope gate: the open op's `AccessControl.required_scopes` is
`["tunnel:open"]` (the `TUNNEL_OPEN_SCOPE` constant). Caller `["tunnel:open"]` (the `TUNNEL_OPEN_SCOPE` constant). Caller
@@ -69,10 +69,10 @@ this document is the normative WHAT.
## The Direct Op (`channels/tunnel/direct`) ## The Direct Op (`channels/tunnel/direct`)
Dynamic-target egress (ADR-007) — "dial what I name," not "dial what Dynamic-target egress (ADR-007): "dial what I name," not "dial what
the producer registered." `Sub`-typed, ALPN marker `alk/tunnel`; the the producer registered." `Sub`-typed, ALPN marker `alk/tunnel`; the
same establishment phase and wrapper as `…/sub`. Scope gate: same establishment phase and wrapper as `…/sub`. Scope gate:
`["tunnel:direct"]` (`TUNNEL_DIRECT_SCOPE`) — **never implied by `["tunnel:direct"]` (`TUNNEL_DIRECT_SCOPE`), **never implied by
`tunnel:open`** (ADR-006 Amendment 1). `tunnel:open`** (ADR-006 Amendment 1).
### Input (params) ### Input (params)
@@ -84,42 +84,42 @@ same establishment phase and wrapper as `…/sub`. Scope gate:
} }
``` ```
- `substrate`: string, required, enum `["tcp", "udp", "unix"]` — the - `substrate`: string, required, enum `["tcp", "udp", "unix"]`: the
same discriminator semantics as `…/sub` (ADR-001); selects the same discriminator semantics as `…/sub` (ADR-001); selects the
data-plane framing. data-plane framing.
- `target`: object, required, keyed by the sibling `substrate` — - `target`: object, required, keyed by the sibling `substrate`:
- `tcp`/`udp` → `{ "host": <string>, "port": <u16> }` (domain-form - `tcp`/`udp` → `{ "host": <string>, "port": <u16> }` (domain-form
hosts stay unresolved on the wire — producer-side resolution, the hosts stay unresolved on the wire; producer-side resolution, the
ssh `-D` semantic); ssh `-D` semantic);
- `unix` → `{ "path": <string> }`. - `unix` → `{ "path": <string> }`.
- `input_schema` pins both required; the target sub-schema is - `input_schema` pins both required; the target sub-schema is
per-substrate — an `allOf` of two `if`/`then` branches keyed on the per-substrate: an `allOf` of two `if`/`then` branches keyed on the
sibling `substrate` value, validated **registry-side** (the schema sibling `substrate` value, validated **registry-side** (the schema
gate runs before the establisher — the behavior gate runs before the establisher, as the behavior
`malformed_target_is_schema_class_and_leaves_no_channel` pins; the `malformed_target_is_schema_class_and_leaves_no_channel` pins; the
establisher's serde parse re-checks as the backstop). Malformed establisher's serde parse re-checks as the backstop). Malformed
targets are the `invalid_input`-class failure; **`unknown_resource` targets are the `invalid_input`-class failure; **`unknown_resource`
can never fire on the direct op** (no registry lookup). can never fire on the direct op** (no registry lookup).
- Schema bounds (wire-stable — the target sub-schema is part of the - Schema bounds (wire-stable; the target sub-schema is part of the
one-way door, ADR-007 §"Wire-stable"): each branch's target object one-way door, ADR-007 §"Wire-stable"): each branch's target object
carries `"type": "object"` and `"additionalProperties": false` — carries `"type": "object"` and `"additionalProperties": false`:
- `tcp`/`udp` → `{ "host": {"type": "string"}, "port": {"type": - `tcp`/`udp` → `{ "host": {"type": "string"}, "port": {"type":
"integer", "minimum": 0, "maximum": 65535} }`, both required; "integer", "minimum": 0, "maximum": 65535} }`, both required;
- `unix` → `{ "path": {"type": "string"} }`, required. - `unix` → `{ "path": {"type": "string"} }`, required.
The `additionalProperties: false` gate is what makes a cross-shape The `additionalProperties: false` gate is what makes a cross-shape
target (a `unix` open carrying `host`) fail the registry's target (a `unix` open carrying `host`) fail the registry's
`INVALID_INPUT` class *before* the establisher — not a serde-level `INVALID_INPUT` class *before* the establisher, not a serde-level
quirk. quirk.
### Reply / errors ### Reply / errors
- Success: `{ "channel_id": <u32> }` — identical semantics to - Success: `{ "channel_id": <u32> }`: identical semantics to
`…/sub`; no `bound` field (dial establishers do not bind). `…/sub`; no `bound` field (dial establishers do not bind).
- Failure: the same typed surface as `…/sub` minus `unknown_resource`, - Failure: the same typed surface as `…/sub` minus `unknown_resource`,
plus the schema/`invalid_input` class for malformed targets. plus the schema/`invalid_input` class for malformed targets.
## The Listen Open Op's Reply — the `bound` and `listen` Fields ## The Listen Open Op's Reply: the `bound` and `listen` Fields
A bind-first listen open (ADR-008 §1, served by `…/sub` with the A bind-first listen open (ADR-008 §1, served by `…/sub` with the
bind-first establisher) MAY add optional, additive reply fields: bind-first establisher) MAY add optional, additive reply fields:
@@ -133,16 +133,16 @@ bind-first establisher) MAY add optional, additive reply fields:
(`{host, port}` / `{path}`); the REAL OS-chosen address (port `0` (`{host, port}` / `{path}`); the REAL OS-chosen address (port `0`
resolved). resolved).
- `listen` is the correlation token (ADR-008 Amendment 1): a u32 the - `listen` is the correlation token (ADR-008 Amendment 1): a u32 the
establisher minted, unique per open on that producer — the key the establisher minted, unique per open on that producer; the key the
forwarded op's params carry (§The Forwarded Op below). Riding the forwarded op's params carry (§The Forwarded Op below). Riding the
reply is what makes it relay-transparent: reply fields pass through reply is what makes it relay-transparent: reply fields pass through
hub re-produce untouched (alkcall ADR-051), and the same minted hub re-produce untouched (alkcall ADR-051), and the same minted
token reappears in every hop's reply. token reappears in every hop's reply.
- Present only when the establisher supplies them; absent for dial - Present only when the establisher supplies them; absent for dial
establishers and accept-first listen opens. Old consumers ignore establishers and accept-first listen opens. Old consumers ignore
unknown reply fields (plain JSON) — wire-additive. unknown reply fields (plain JSON), which is wire-additive.
- `bound` is informational surface, not addressing: a consumer MUST - `bound` is informational surface, not addressing: a consumer MUST
NOT treat `bound` as a dial target (the producer owns the backing — NOT treat `bound` as a dial target (the producer owns the backing,
ADR-001). It exists for RFC-1928-reply-class fidelity (BND.ADDR) ADR-001). It exists for RFC-1928-reply-class fidelity (BND.ADDR)
and observability. and observability.
@@ -150,17 +150,17 @@ bind-first establisher) MAY add optional, additive reply fields:
Accept-as-open (ADR-008 §3): when a bind-first listener accepts, the Accept-as-open (ADR-008 §3): when a bind-first listener accepts, the
PRODUCER opens a channel back toward the listen-opener side. The PRODUCER opens a channel back toward the listen-opener side. The
producer *calls* the op (the opener SERVES it — the ADR-047 §5 producer *calls* the op (the opener SERVES it, per the ADR-047 §5
connection-owner rule: the serving side allocates the `channel_id`, connection-owner rule: the serving side allocates the `channel_id`,
the producer-side initiator adopts). `Sub`-typed, ALPN marker the producer-side initiator adopts). `Sub`-typed, ALPN marker
`alk/tunnel`; the serving side registers establisher-less by base `alk/tunnel`; the serving side registers establisher-less by base
shape (the data plane starts immediately) or — the recommended shape, shape (the data plane starts immediately) or (the recommended shape,
ADR-008 Amendment 2 — with the correlation-gate establisher variant ADR-008 Amendment 2) with the correlation-gate establisher variant
(a stale/missing token then fails the op as a typed (a stale/missing token then fails the op as a typed
`channel:open_failed` with `details.reason = "unknown_resource"`; the `channel:open_failed` with `details.reason = "unknown_resource"`; the
wire vocabulary is unchanged by the amendment). wire vocabulary is unchanged by the amendment).
Scope gate (opener side): `["tunnel:forwarded"]` Scope gate (opener side): `["tunnel:forwarded"]`
(`TUNNEL_FORWARDED_SCOPE`) — ingress, a separate grant from (`TUNNEL_FORWARDED_SCOPE`): ingress, a separate grant from
`tunnel:open`/`tunnel:direct` (ADR-006 Amendment 1). `tunnel:open`/`tunnel:direct` (ADR-006 Amendment 1).
### Input (params) ### Input (params)
@@ -173,23 +173,23 @@ Scope gate (opener side): `["tunnel:forwarded"]`
} }
``` ```
- `listen`: integer > 0, required — the correlation token from the - `listen`: integer > 0, required: the correlation token from the
listen open's `listen` reply field (ADR-008 Amendment 1; the listen open's `listen` reply field (ADR-008 Amendment 1; the
producer-minted token, relay-transparent). The opener correlates producer-minted token, relay-transparent). The opener correlates
the accept to its waiting listener session by matching it. The the accept to its waiting listener session by matching it. The
schema pins `"minimum": 1` and the serde type pins it as the schema pins `"minimum": 1` and the serde type pins it as the
backstop (a `listen: 0` on the wire is a malformed open, rejected backstop (a `listen: 0` on the wire is a malformed open, rejected
at parse time — never silently accepted). at parse time, never silently accepted).
- `substrate`: string, required, enum — the listen tunnel's - `substrate`: string, required, enum: the listen tunnel's
substrate; shapes the forwarded channel's data-plane framing. substrate; shapes the forwarded channel's data-plane framing.
- `peer`: object, required — the accepted connection's remote address, - `peer`: object, required: the accepted connection's remote address,
substrate-shaped (`{host, port}` / `{path}`). Reply#2-class substrate-shaped (`{host, port}` / `{path}`). Reply#2-class
fidelity's carrier (ask #2). fidelity's carrier (ask #2).
- Schema bounds (wire-stable, mirroring the direct op's target - Schema bounds (wire-stable, mirroring the direct op's target
sub-schema): the per-substrate `peer` sub-schema is an `allOf` of sub-schema): the per-substrate `peer` sub-schema is an `allOf` of
two `if`/`then` branches keyed on the sibling `substrate`, each two `if`/`then` branches keyed on the sibling `substrate`, each
branch's object carrying `"type": "object"` and branch's object carrying `"type": "object"` and
`"additionalProperties": false` — `"additionalProperties": false`:
- `tcp`/`udp` → `{ "host": {"type": "string"}, "port": {"type": - `tcp`/`udp` → `{ "host": {"type": "string"}, "port": {"type":
"integer", "minimum": 0, "maximum": 65535} }`, both required; "integer", "minimum": 0, "maximum": 65535} }`, both required;
- `unix` → `{ "path": {"type": "string"} }`, required. - `unix` → `{ "path": {"type": "string"} }`, required.
@@ -199,22 +199,22 @@ Scope gate (opener side): `["tunnel:forwarded"]`
### Reply / errors ### Reply / errors
- Success: `{ "channel_id": <u32> }` — the opener-allocated ID the - Success: `{ "channel_id": <u32> }`: the opener-allocated ID the
producer adopts, then pumps the accepted halves against. producer adopts, then pumps the accepted halves against.
- Failure: a stale/missing correlation token is the typed - Failure: a stale/missing correlation token is the typed
`channel:open_failed` with `details.reason = "unknown_resource"` `channel:open_failed` with `details.reason = "unknown_resource"`
(the establisher variant, ADR-008 Amendment 2 — the recommended (the establisher variant, ADR-008 Amendment 2, which is the
serving shape; the miss never births a channel). An opener without recommended serving shape; the miss never births a channel). An opener without
the scope is `FORBIDDEN`; an old opener that does not serve the op the scope is `FORBIDDEN`; an old opener that does not serve the op
rejects loudly (`NOT_FOUND` — the SSH posture). Other open failures rejects loudly (`NOT_FOUND`: the SSH posture). Other open failures
ride the existing typed vocabulary. Under the base (establisher- ride the existing typed vocabulary. Under the base (establisher-
less) serving shape an in-session miss produces no call error — it less) serving shape an in-session miss produces no call error; it
is the assembly-side post-open decline (the channel reaps at is the assembly-side post-open decline (the channel reaps at
birth, `correlation-miss` at `debug`). birth, `correlation-miss` at `debug`).
## The Data Plane ## The Data Plane
One data stream per direction on one channel — the channel ID is the One data stream per direction on one channel: the channel ID is the
flow key (no conn-id, no sub-demux). Two codecs by substrate flow key (no conn-id, no sub-demux). Two codecs by substrate
(ADR-003): (ADR-003):
@@ -225,7 +225,7 @@ overhead is the channels 8-byte chunk header. A zero-length read is
genuinely EOF (stream semantics). genuinely EOF (stream semantics).
Both pumps run `alkcall::channels::pump_bidi(channel, peer_read, Both pumps run `alkcall::channels::pump_bidi(channel, peer_read,
peer_write)` — the two-pump contract (alknet ADR-078, pinned upstream peer_write)`, the two-pump contract (alknet ADR-078, pinned upstream
ADR-050): when one direction's source EOFs, the pump shuts the ADR-050): when one direction's source EOFs, the pump shuts the
OPPOSITE sink down (the EOF sentinel crosses the mux; the peer sees a OPPOSITE sink down (the EOF sentinel crosses the mux; the peer sees a
clean half-close), and the helper completes when both pumps finish, clean half-close), and the helper completes when both pumps finish,
@@ -234,15 +234,15 @@ returning `(u64, u64)` copy counts. Copy errors are EOF-shaped
### Datagram substrate (`udp`): mandatory length framing ### Datagram substrate (`udp`): mandatory length framing
Every datagram rides `[len: u16 BE][datagram]` on BOTH directions — Every datagram rides `[len: u16 BE][datagram]` on BOTH directions;
mandatory for correctness (the F-2 rationale lives in ADR-003; the this is mandatory for correctness (the F-2 rationale lives in ADR-003; the
short form: raw pass-through cannot carry an empty datagram). Under short form: raw pass-through cannot carry an empty datagram). Under
the codec: the codec:
- **Framing (`frame_datagram`):** `len: u16 BE` + payload. Datagrams - **Framing (`frame_datagram`):** `len: u16 BE` + payload. Datagrams
> 65535 bytes are rejected at frame time (`Oversize`) — never a > 65535 bytes are rejected at frame time (`Oversize`), never a
wire overflow (the u16 length field would wrap). wire overflow (the u16 length field would wrap).
- **Decoding (`DatagramReader`):** incremental — datagrams split - **Decoding (`DatagramReader`):** incremental; datagrams split
across channel chunks, batch into single chunks, and survive across channel chunks, batch into single chunks, and survive
partial headers. The decoder buffers state across reads; it is the partial headers. The decoder buffers state across reads; it is the
only consumer-visible decode path. only consumer-visible decode path.
@@ -253,7 +253,7 @@ the codec:
returns `Some(bytes)` per datagram (possibly empty), `None` only on returns `Some(bytes)` per datagram (possibly empty), `None` only on
stream EOF. stream EOF.
- **Chunk-size independence:** datagram boundaries survive chunk - **Chunk-size independence:** datagram boundaries survive chunk
splitting, batching, and any channels-layer re-chunking — verified splitting, batching, and any channels-layer re-chunking, verified
end-to-end at 7-byte chunk splits (forward POC). end-to-end at 7-byte chunk splits (forward POC).
- **MTU discipline:** 1400-byte datagrams (a conservative MTU-safe - **MTU discipline:** 1400-byte datagrams (a conservative MTU-safe
payload, not the theoretical 1472 max) ride the bounded-buffer path; payload, not the theoretical 1472 max) ride the bounded-buffer path;
@@ -262,10 +262,10 @@ the codec:
- **Truncation:** silent truncation never happens (OQ-TN-13, resolved - **Truncation:** silent truncation never happens (OQ-TN-13, resolved
fail-loud per ADR-003): the `local` adapter recvs on a full fail-loud per ADR-003): the `local` adapter recvs on a full
65535-byte scratch (UDP `try_recv` truncates silently at the buffer 65535-byte scratch (UDP `try_recv` truncates silently at the buffer
size — a smaller scratch would corrupt the framing) and a size; a smaller scratch would corrupt the framing) and a
>caller-buffer frame is staged across reads, the length prefix >caller-buffer frame is staged across reads, the length prefix
intact, so the far-side de-framer reassembles the true length. A intact, so the far-side de-framer reassembles the true length. A
wire-level over-bound datagram (>65535 — impossible for a wire-level over-bound datagram (>65535, impossible for a
well-formed peer) is a loud error at the frame path. well-formed peer) is a loud error at the frame path.
### Sentinels and lifecycle (both codecs) ### Sentinels and lifecycle (both codecs)
@@ -282,10 +282,10 @@ the codec:
idle-expiry mapping is a producer-registry concern, not wire). idle-expiry mapping is a producer-registry concern, not wire).
- **No establishment frame.** The open op's reply IS the - **No establishment frame.** The open op's reply IS the
establishment result (ADR-049); the pre-ADR-049 establishment result (ADR-049); the pre-ADR-049
"JSON ack before data" hunch is superseded — the wire carries zero "JSON ack before data" hunch is superseded; the wire carries zero
tunnel-level control framing in v1. tunnel-level control framing in v1.
- **BAST:** the binary framing (the UDP codec) carries its BAST - **BAST:** the binary framing (the UDP codec) carries its BAST
document — [bast.md](bast.md) (AGENTS.md convention 12; the document in [bast.md](bast.md) (AGENTS.md convention 12; the
stream pass-through has no binary framing to describe). stream pass-through has no binary framing to describe).
## Normative byte diagrams ## Normative byte diagrams
@@ -307,11 +307,11 @@ UDP substrate, one direction's wire view:
## Open Questions ## Open Questions
- **OQ-TN-13**: resolved (fail-loud, ADR-003) — see the Truncation - **OQ-TN-13**: resolved (fail-loud, ADR-003); see the Truncation
bullet bullet
- **OQ-TN-02**: resolved — see the ADR-003 mandate - **OQ-TN-02**: resolved; see the ADR-003 mandate
- **OQ-TN-15**: resolved — see ADR-007 (the direct op) - **OQ-TN-15**: resolved; see ADR-007 (the direct op)
- **OQ-TN-16**: resolved — see ADR-008 (the `bound`/`listen` fields + - **OQ-TN-16**: resolved; see ADR-008 (the `bound`/`listen` fields +
the forwarded op) the forwarded op)
## References ## References
@@ -333,5 +333,5 @@ UDP substrate, one direction's wire view:
ADR-050 (`pump_bidi`); alknet ADR-071/093 (channels wire) ADR-050 (`pump_bidi`); alknet ADR-071/093 (channels wire)
- `docs/research/tunnels-graduation.md` (the graduation research the - `docs/research/tunnels-graduation.md` (the graduation research the
new ops implement); the upstream asks (review 008) landed in new ops implement); the upstream asks (review 008) landed in
alkcall 0.8.0 — see alkcall 0.8.0; see
`alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md` `alkcall/docs/reviews/008-tunnels-graduation-upstream-review.md`