docs: Phase 1 architecture spec — ADRs 001..006, spec docs, OQ promotion
The Phase 0 OQ ledger (OQ-TN-01..10) promoted into
docs/architecture/open-questions.md (now OQ-TN-01..14, with four
new Phase 1 residues). Six ADRs and four spec docs, all Draft, all
decision-shaped per the SDD process (ADRs carry the WHY; specs carry
the WHAT and reference by number):
ADRs:
- 001 open-params-layout: params = {resource, substrate} — the
producer's stable name, not an address; wire-stable from the first
consumer; unknown substrates fail loudly at schema time
- 002 alpn-strategy: single alk/tunnel ALPN (option A); substrate in
params selects the framing; prior art (SSH/SOCKS5/udpgw) gives no
reason for the split
- 003 codec-and-udp-framing: raw pass-through (stream) / mandatory
[len: u16 BE] (UDP) — F-2 mandate recorded (the codec is mandatory
for correctness, not cosmetics); len=0 = legal empty datagram;
OQ-TN-13 resolved fail-loud (truncation)
- 004 no-backend-trait: halves functions at the assembly layer;
listen is an establisher shape; hub re-produce deferred(OQ-TN-12);
trait re-evaluated at the alknet ADR-078 convergence threshold
- 005 consumer-session-owns-teardown: TunnelSession with
open/adopt/stream_halves/take_halves/pump_against/close/join/Drop —
the W3 adopter gap closes structurally; Drop is the best-effort
fallback
- 006 access-control-posture: the open gate is the boundary;
TUNNEL_OPEN_SCOPE = tunnel:open (stable once published); op-level
ACL; ownership seam; no allowlists in v1; identity = 0.7.0's
precedence chain
Spec docs:
- overview.md: purpose, resource model, deps (alkcall 0.7.0,
wasm-clean default, local feature), module map
- wire.md: the open op (params/reply/typed errors), the data plane
by substrate, sentinels + half-close, byte diagrams
- producer.md: spec, establisher (dial + listen shapes), pump
handler (pump_bidi inline, R-02), registration API, ACL posture
- consumer.md: TunnelSession (forward open + reverse adopt
construction), data plane, teardown API (close/join/Drop incl.
pump-less join semantics)
- bast.md: the BAST doc for the UDP codec (convention 12's trigger
fired — the framing IS binary beyond pass-through)
- open-questions.md: OQ-TN-01..14; 01..10 promoted (faithful to the
phase-0 ledger's final states), 11 partially resolved (collision
domain = per-producer registry per ADR-001; lifecycle open),
12 deferred(scope), 13 resolved fail-loud (ADR-003), 14 open
Verified by an architecture-reviewer pass (2 criticals, 6 majors,
10 minors — all fixed: ADR-001/003-vs-OQ decision-state contradictions
resolved; the Layer-2 mislabel corrected to the ADR-047 §4 per-session
fork; alktty/alknet ADR misattributions fixed; codec placement pinned
to the establisher (pump stays substrate-agnostic); reverse-path
construction named (TunnelSession::adopt + pump_against); security
posture promoted to ADR-006; BAST doc written; F-2 rationale deduped;
README tables completed; channel_id > 0; MTU wording fixed; ADR-047 §5
attributions corrected; impacts lines added to unresolved OQs; review/
ledger pointer paths added to README references).
Verification: doc set internal cross-refs all resolve; cargo test,
fmt --check, doc --no-deps clean
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-09-07
|
||||
---
|
||||
|
||||
# alktunnels — Consumer Half
|
||||
|
||||
The consumer half: `TunnelSession` — the typed client for tunnel
|
||||
channels. The consumer is whoever wants the bytes; it opens tunnel
|
||||
channels toward the producer (forward path), or per local accept
|
||||
toward a connect-side serving producer (the reverse flow). Both are
|
||||
the same session type — role follows the resource, not the connection
|
||||
(OQ-TN-03). Design decisions: [ADR-005](decisions/005-consumer-session-owns-teardown.md)
|
||||
(session owns teardown), [ADR-003](decisions/003-codec-and-udp-framing.md)
|
||||
(codec); this document is the normative WHAT.
|
||||
|
||||
## Opening a Tunnel
|
||||
|
||||
### Forward path (`-L`/direct — the consumer dials the producer's transport)
|
||||
|
||||
```rust
|
||||
let session = TunnelSession::open(&channel_client, params).await?;
|
||||
```
|
||||
|
||||
- `params`: `{resource, substrate}` (ADR-001) — the produced resource
|
||||
+ substrate discriminator.
|
||||
- Flow: `ChannelClient::open_channel("channels/tunnel/sub", params,
|
||||
"alk/tunnel")` → adopt the returned channel ID → split the channel
|
||||
`BiStream` → present the substrate-shaped data plane.
|
||||
- Errors: a failed open resolves a typed error —
|
||||
`ChannelOpenError::CallFailed` carrying the wire `CallError`;
|
||||
branch on `establishment_reason()` for `channel:open_failed`'s
|
||||
reason (dial_failed / unknown_resource / resource_shortage /
|
||||
handler_error / timeout), or the pre-establishment codes
|
||||
(`FORBIDDEN`, `channel:too_many_channels`). **A failed open never
|
||||
yields a session** — no phantom session, mirroring the
|
||||
no-phantom-channel property (POC-verified).
|
||||
|
||||
### Reverse path (`-R` — the consumer initiates toward a connect-side serving producer)
|
||||
|
||||
The hub/accept-side shape (the reverse POC's `ReverseTunnel`, the
|
||||
seed of this path). Construction differs from the forward path only
|
||||
in how the channel is obtained — the session type and teardown API
|
||||
are identical:
|
||||
|
||||
```rust
|
||||
// Per local accept (assembly-owned listener):
|
||||
let channel_id = open_reverse_channel(&hub_call, ¶ms, auth_token).await?;
|
||||
let session = TunnelSession::adopt(hub_manager, channel_id, TUNNEL_ALPN).await?;
|
||||
let pump_handle = session.pump_against(accepted_halves).await; // or drive the halves directly
|
||||
```
|
||||
|
||||
- `open_reverse_channel` — call the worker's open op on channel 0
|
||||
(`call_with_payload`; optional `auth_token`), extract the
|
||||
worker-allocated `channel_id`. Caller identity rides the transport
|
||||
by default (alkcall 0.7.0 CF-005 (b): `Connection::set_identity`
|
||||
before dialing); the payload `auth_token` is the optional
|
||||
hub-forwarding path (precedence: token > `ServingConfig.identity` >
|
||||
transport).
|
||||
- `TunnelSession::adopt(manager, channel_id, alpn)` — adopt the
|
||||
worker-allocated ID (connection-owner rule, ADR-047 §5 — the
|
||||
SERVING side allocates; the initiator adopts; early-arrival parking
|
||||
covers the adoption race), install the session's data plane from
|
||||
the adopted halves. `adopt` takes the same typed-error surface as
|
||||
`open` (an adopt failure is `AdoptFailed`-class, not an
|
||||
establishment error).
|
||||
- `pump_against(accepted_halves)` — spawn `pump_bidi` against the
|
||||
accepted local connection and hold the pump handle (ADR-005).
|
||||
|
||||
The session type and its teardown API are identical on both paths —
|
||||
one API, `-L` and `-R` alike.
|
||||
|
||||
## The Data Plane (substrate-shaped)
|
||||
|
||||
- **Stream variant (`tcp`, `unix`):** `stream_halves()` — raw
|
||||
`AsyncRead`/`AsyncWrite` halves; the halves ARE the tunnel (raw
|
||||
pass-through, ADR-003). For owned access (spawning local pumps),
|
||||
`take_halves()` consumes the session's halves.
|
||||
- **Datagram variant (`udp`):** `send_datagram(&[u8])` /
|
||||
`recv_datagram() -> Option<Bytes>` over the mandatory
|
||||
`[len: u16 BE]` codec (ADR-003). `recv_datagram` returns
|
||||
`Some(bytes)` per datagram — possibly empty (`len=0` is a legal
|
||||
empty datagram) — and `None` only on stream EOF (the channels-level
|
||||
sentinel; the codec never collides with it, F-2). A >65535-byte
|
||||
send is rejected at frame time (`Oversize`).
|
||||
|
||||
## Lifecycle and Teardown (the point of ADR-005)
|
||||
|
||||
One session = one channel = one tunnel. The session owns:
|
||||
|
||||
- **The adopted channel entry** — nothing upstream awaits the
|
||||
adopter's pump (the W3 gap); the session closes it structurally.
|
||||
- **The session-owned pump handle** (when the session spawned one —
|
||||
the reverse path's `pump_against`).
|
||||
|
||||
Teardown API:
|
||||
|
||||
- `close(self)` — abort the pump (if session-owned), tear down the
|
||||
adopted channel (`ChannelManager::teardown_channel`), consume the
|
||||
session. The ungraceful path.
|
||||
- `join(self)` — await pump completion (both directions finished;
|
||||
half-close semantics fall out of `pump_bidi` — one direction EOFs,
|
||||
the opposite sink shuts down, the other keeps pumping until its own
|
||||
EOF; W4-validated), THEN reap the adopted channel. Returns the
|
||||
`(u64, u64)` copy counts for observability. The graceful path.
|
||||
**Pump-less sessions** (after `take_halves`, or a datagram session
|
||||
the caller drives directly): there is no session-owned pump —
|
||||
`join` completes immediately (nothing to await) and reaps only;
|
||||
the copy counts are `(0, 0)`.
|
||||
- `Drop` — aborts the pump and sync-reaps the channel (teardown
|
||||
semantics of `close`, best-effort — `Drop` cannot await). Dropping
|
||||
without close/join never leaks the channel entry.
|
||||
- **`TunnelSession` does not implement `Clone`.** Aliasing a session
|
||||
would alias its teardown; multi-channel consumers hold a
|
||||
`Vec<TunnelSession>` (or the assembly layer does).
|
||||
|
||||
## Out-of-band close
|
||||
|
||||
A peer-initiated `channel/close` (the generic channel op on the
|
||||
producer's serving registry) tears the SERVING side down; the
|
||||
consumer's session still owns its adopted entry — `close`/`join` reaps
|
||||
it. The reverse POC pinned this split (the close ran on the worker;
|
||||
the hub reaped its own entry, W3). EOF propagation is symmetric:
|
||||
either side's teardown reaches the other as the channels-level
|
||||
sentinel.
|
||||
|
||||
## Both-Sides Sanity
|
||||
|
||||
The consumer and producer halves compose on one connection in both
|
||||
roles (alkcall ADR-022 §2): a worker dials the hub (connect side),
|
||||
serves its open ops (serving side), and its own outbound calls still
|
||||
resolve while serving (reverse POC: worker_outbound_calls_still_
|
||||
resolve_while_serving). The consumer half adds nothing to the
|
||||
connection's serving posture — a pure consumer (`from_connection`)
|
||||
never dispatches inbound requests; a reverse-flow initiator needs no
|
||||
serving registry of its own.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **OQ-TN-12**: hub re-produce composition (deferred — a hub proxy
|
||||
re-exporting a consumed resource would add a
|
||||
`register_openable_with_establisher` whose establisher opens a
|
||||
consumer channel; no concrete consumer yet)
|
||||
- **OQ-TN-13**: UDP truncation (resolved — fail-loud, ADR-003)
|
||||
|
||||
## References
|
||||
|
||||
- [producer.md](producer.md) (the serving side's shapes),
|
||||
[wire.md](wire.md) (the data plane)
|
||||
- Reverse POC `docs/research/reverse-poc-summary.md` (the seed shape:
|
||||
`ReverseTunnel` open_and_pump / close / join_and_reap; W3, W4)
|
||||
- alkcall ADR-047 §5 (allocation — the initiator adopts), ADR-050
|
||||
(`pump_bidi`), ledger
|
||||
CF-005/CF-006 (identity on the reverse path)
|
||||
Reference in New Issue
Block a user