tasks: Phase 2 decomposition — 12-task dependency graph for v1

tasks/architecture/:
- oq-promotion-sync (planning): back-pointers from the phase-0 ledger
  + AGENTS.md to the promoted OQ tracker (the convergence checklist's
  final half)
- oq-tn-14-tracker: the Safe-Exit external-trigger tracker task for
  OQ-TN-14 (unix/stdio placement; [external-trigger, deferred-oq],
  risk trivial, level research per the two-halves rule)

tasks/tunnels/ (the implementation graph, 8 generations):
- crate-init: module skeleton per overview.md's module map
- params: TunnelParams + open-op spec (ADR-001 wire-stable surface)
- wire-codec: frame_datagram/DatagramReader + the 8 POC-pinned test
  families (ADR-003)
- producer-open-op: establisher (dial, plan flow R-01) + pump handler
  (pump_bidi inline R-02) + registration; POC-ported integration tests
- consumer-session: TunnelSession (open/adopt, data planes, teardown
  matrix — ADR-005); generalizes the reverse POC's ReverseTunnel
- producer-listen: the listen establisher + AcceptQueue contract
  (ADR-004 shape 2)
- local-socket-halves: the local feature (TCP/UDP/unix halves
  functions; truncation fail-loud per OQ-TN-13; unix ships per
  OQ-TN-14's lean-yes, stdio deferred)
- review-core-crates: review-injection point before the downstream
  tasks build on the high-risk producer/consumer shapes
- end-to-end-suite: 6 suites / >=20 tests consolidating both POC
  suites against the public API (the spec's executable form)
- review-impl: the phase-gate review (wire/API/conventions/docs sync;
  findings doc per the alkhttp/alkcall house pattern)

Graph verified with taskgraph: 12 tasks valid, no cycles, 8
generations; critical path = oq-promotion-sync -> crate-init ->
params -> wire-codec -> producer-open-op -> consumer-session ->
review-core-crates -> review-impl; risk concentrated in the two
session tasks (both POC-validated); parallel groups available at
generations 1 and 6
This commit is contained in:
2026-09-07 19:07:20 +00:00
parent bd7d1ad8ec
commit 69498b79cc
12 changed files with 1051 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
---
id: tunnels/consumer-session
name: Consumer half — TunnelSession (open/adopt, data planes, teardown)
status: pending
depends_on: [tunnels/params, tunnels/wire-codec, tunnels/producer-open-op]
scope: broad
risk: high
impact: phase
level: implementation
tags: [consumer, session, teardown]
---
## Description
Implement `src/consumer.rs` per consumer.md + ADR-005: `TunnelSession`
the typed client, both construction paths, the substrate-shaped data
plane, and teardown ownership. The reverse POC's consumer
(`/workspace/alktunnels-reverse-poc/src/consumer.rs``ReverseTunnel`)
is the seed; this task generalizes it to the spec's session type.
### Construction
- **Forward path:** `TunnelSession::open(client:
&ChannelClient, params: TunnelParams) -> Result<Self,
TunnelOpenError>` — `client.open_channel(OP_TUNNEL_OPEN, params,
TUNNEL_ALPN)` → adopt → split the channel `BiStream` → data plane by
substrate. Typed errors (ADR-049 §4; never a phantom session).
- **Reverse path:** `TunnelSession::adopt(manager: &ChannelManager,
channel_id: u32, alpn: impl Into<String>) -> Result<Self,
TunnelOpenError>` — adopt a worker-allocated ID (early arrivals
parked), install the data plane from the adopted halves.
`open_reverse_channel(hub_call, params, auth_token) -> Result<u32,
ReverseOpenError>` as the free function the assembly layer calls
before `adopt` (the POC's shape — the hub's call surface is a
`CallConnection`, not a `ChannelClient`, so the two-step is the
honest API).
### Data plane
- **Stream variant:** `stream_halves()` →
`(&mut dyn AsyncRead, &mut dyn AsyncWrite)` (borrowed access);
`take_halves(self)` → owned boxed halves (the session's halves ARE
the tunnel — raw pass-through).
- **Datagram variant:** `send_datagram(&[u8]) -> Result<(),
TunnelIoError>` (frame → write → flush; `Oversize` >65535);
`recv_datagram() -> Result<Option<Bytes>, TunnelIoError>` —
`Some(bytes)` per datagram (possibly empty, `len=0` legal), `None`
only on stream EOF (the F-2 layering; the POC's
`read_one_datagram` incremental loop).
- Wrong-substrate operations are `WrongSubstrate` errors (the POC's
shape).
### Pump ownership + teardown (ADR-005 — the point)
- `pump_against(self, accepted: impl AsyncRead + AsyncWrite + ...)` —
for the reverse path: spawn `pump_bidi(channel_bistream,
accepted_read, accepted_write)`, hold the returned handle. Returns
the session (builder-style) or takes self and returns the handle —
pick the shape that makes holding easy; document it.
- `close(self) -> bool` — abort the pump (if session-owned) +
`teardown_channel` (ungraceful path).
- `join(self) -> (u64, u64, bool)` — await pump completion, then reap;
copy counts for observability. **Pump-less sessions** (after
`take_halves`): completes immediately, reaps only, `(0, 0, reaped)`
(consumer.md's pinned semantics).
- `Drop` — abort + sync `teardown_channel` (best-effort; never leak
the entry). No `Clone`.
### Tests
Extend the producer task's integration suite: forward open (session
halves drive a duplex), reverse open+adopt+pump_against (the POC's
`ReverseTunnel` tests: round-trip, half-close W4, join_and_reap copy
counts, out-of-band close + self-reaping, pump-less join), datagram
variant (round-trip incl. empty datagram via the codec), teardown
matrix (close/join/Drop paths — no leaked channel entries asserted via
`channel_ids()`).
## Acceptance Criteria
- [ ] `open` + `adopt` construction both present; typed error surfaces
exact
- [ ] `stream_halves`/`take_halves`/`send_datagram`/`recv_datagram`
per consumer.md
- [ ] Teardown matrix: close (ungraceful), join (graceful + copy
counts), pump-less join `(0, 0, reaped)`, Drop (no leak)
- [ ] Half-close semantics test (W4's shape) passes
- [ ] No `Clone` on `TunnelSession` (compile-asserted)
- [ ] Clippy/fmt clean; wasm32 check passes; `cargo test` green
## References
- docs/architecture/consumer.md (the normative API)
- docs/architecture/decisions/005-consumer-session-owns-teardown.md
- POC reference: `/workspace/alktunnels-reverse-poc/src/consumer.rs`
(the seed shape to generalize)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+91
View File
@@ -0,0 +1,91 @@
---
id: tunnels/crate-init
name: Initialize the alktunnels module skeleton (params, wire, producer, consumer, error)
status: pending
depends_on: [architecture/oq-promotion-sync]
scope: narrow
risk: low
impact: project
level: implementation
tags: [scaffold, crate-init]
---
## Description
Convert the alktunnels scaffold crate into the real module skeleton per
`docs/architecture/overview.md` §Module Map. The crate already exists
(Cargo.toml with alkcall 0.7.0, the wasm-clean tokio subset, licenses,
`lib.rs` stub); this task adds the module structure the subsequent tasks
fill in.
### Module skeleton
```rust
// src/lib.rs
//! alktunnels — arbitrary bidirectional tunnels over alkcall channels.
//! (crate docs per overview.md §What — the two-paragraph shape)
pub mod error;
pub mod params;
pub mod wire;
pub mod producer;
pub mod consumer;
// Public API surface: re-exports filled in by subsequent tasks (convention 16).
```
- `src/error.rs``TunnelError` (thiserror; convention 2) + the typed
open-error surface re-exported from alkcall (`ChannelOpenError`,
`establishment_reason`-shaped helper). Skeleton only; filled by
`tunnels/params` and `tunnels/consumer-session`.
- `src/params.rs``TunnelParams {resource, substrate}` +
`Substrate` enum (`tcp | udp | unix`) + `tunnel_open_spec()` builder +
`TUNNEL_OPEN_SCOPE`/`OP_TUNNEL_OPEN`/`TUNNEL_ALPN` constants. Skeleton
with type definitions; the schema builder + tests land in
`tunnels/params`.
- `src/wire.rs` — the codec module doc (ADR-003 summary + bast.md
pointer); skeleton for `frame_datagram`/`DatagramReader`/
`DatagramCodecError`, filled by `tunnels/wire-codec`.
- `src/producer.rs` — module doc (producer.md summary); skeleton for
`tunnel_open_spec` wiring, `register_tunnel_openable`, the establisher
shape, the pump handler — filled by `tunnels/producer-open-op` +
`tunnels/producer-listen`.
- `src/consumer.rs` — module doc (consumer.md summary); skeleton for
`TunnelSession` — filled by `tunnels/consumer-session`.
### Keep in place / verify
- Cargo.toml: alkcall 0.7.0 pin, the wasm-clean tokio subset, the empty
`[features]` table (the `local` feature is added by `tunnels/local-socket-halves`).
- `bytes`, `serde`, `serde_json`, `thiserror`, `tracing`, `futures` stay;
drop `async-trait` IF the skeleton confirms no trait is needed (ADR-004 —
it likely is not; note the decision in Summary if dropped).
- The default crate stays wasm-clean: `cargo check --target
wasm32-unknown-unknown` must pass at every task in this graph
(convention 4; the structural guard).
## Acceptance Criteria
- [ ] `src/lib.rs` declares `error`, `params`, `wire`, `producer`,
`consumer` with doc comments; public API re-exports listed (empty
bodies fine)
- [ ] Every skeleton module compiles (`cargo check` clean)
- [ ] `cargo clippy --all-targets -- -D warnings` clean
- [ ] `cargo fmt --check` clean
- [ ] `cargo check --target wasm32-unknown-unknown` passes
- [ ] No comments in code beyond doc comments (convention 1)
## References
- docs/architecture/overview.md (module map, feature gates)
- docs/architecture/decisions/004-no-backend-trait.md (module placement)
- AGENTS.md conventions 1/2/4/15/16
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+80
View File
@@ -0,0 +1,80 @@
---
id: tunnels/end-to-end-suite
name: End-to-end suite — forward, reverse, listen, datagram, teardown matrix
status: pending
depends_on: [tunnels/consumer-session, tunnels/producer-listen, tunnels/local-socket-halves]
scope: broad
risk: medium
impact: phase
level: implementation
tags: [tests, integration, phase-gate]
---
## Description
Consolidate the integration coverage into `tests/` as the crate's
end-to-end suite: every validated POC behavior, re-expressed against the
crate's public API (not the POCs' internals). This is the quality gate
before the implementation review — the suite is the spec's executable
form.
### Suites
1. **Forward (`-L`)**: consumer `open` → producer establisher dials →
`pump_bidi` → round-trip; 1 MiB backpressure; typed errors
(unknown_resource / dial_failed / FORBIDDEN / timeout-adjacent);
per-call opener identity witness (CF-006).
2. **Reverse (`-R`)**: hub initiator — `open_reverse_channel` +
`adopt` + `pump_against`; odd-ID allocation asserted (ADR-047 §5);
identity precedence chain (transport alone / `ServingConfig.
identity` override / token override / identity-less fail-closed);
half-close (W4); out-of-band `channel/close` + self-reaping (W3);
concurrent same-resource opens (R-01, no handoff race).
3. **Listen producer**: the accept-queue flow end-to-end + its typed
errors (from `tunnels/producer-listen`).
4. **Datagram (`udp`)**: forward + reverse datagram sessions via the
codec — round-trip, empty datagram (the F-2 layering), chunk-split
survival, TCP+UDP concurrent channels on one connection.
5. **Teardown matrix** (ADR-005): close / join (with + without pump) /
Drop — `channel_ids()` asserts no leaks on either side after each.
6. **`local` feature suite**: real sockets end-to-end (from
`tunnels/local-socket-halves` — runs after that task lands; the
duplex-transport suites above do not need it).
Transport stand-in: `tokio::io::duplex` (both POCs' harness; the
alkcall layer owns real transports — unchanged scope). The suite may
share one harness module (`tests/common/`); the POC harnesses
(`/workspace/alktunnels-udp-poc/src/harness.rs`,
`/workspace/alktunnels-reverse-poc/src/harness.rs`) are the two
topologies to unify.
### The spec-conformance assertions to keep visible
- A failed open never returns a `channel_id` (no phantom channel —
both sides' `channel_ids()` hold only 0 afterward).
- A failed adopt never leaks the session (Drop reaps).
- The pump handler's `JoinHandle` tracks the data plane (a test that
simulates early-return would hang/EOF-instantly — the R-02 shape;
assert the correct shape survives).
## Acceptance Criteria
- [ ] All 6 suites present; ≥20 integration tests total
- [ ] `cargo test` green; `cargo test --features local` green
- [ ] Repeat-run stable (3× clean — the POCs' flakiness bar)
- [ ] Clippy/fmt clean; wasm32 check passes
## References
- docs/architecture/wire.md + consumer.md + producer.md (the normative
behaviors under test)
- POC test suites: `/workspace/alktunnels-udp-poc/tests/tunnel_poc.rs`
(10), `/workspace/alktunnels-reverse-poc/tests/tunnel_poc.rs` (16)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+105
View File
@@ -0,0 +1,105 @@
---
id: tunnels/local-socket-halves
name: "local feature — real socket halves functions (TCP dial, UDP connect, unix)"
status: pending
depends_on: [tunnels/producer-open-op, tunnels/wire-codec]
scope: moderate
risk: medium
impact: component
level: implementation
tags: [local, backend, sockets, feature-gate]
---
## Description
Add the `local` feature module (`src/local/`) per overview.md §Feature
Gates + ADR-004: the real-socket halves functions the assembly layer
injects as `DialFn`/`AcceptFn` closures. The POCs' substrate adapters
are the reference (they ran all of this); the task is packaging them
into the feature-gated module with the crate's conventions.
### Surface
```rust
// src/local/mod.rs (feature = "local"; non-wasm by design)
pub async fn dial_tcp(target: &str) -> Result<TargetHandle, TunnelEstablishError>;
// TcpStream::connect (5s timeout per the POCs) → into_split → boxed halves
pub async fn connect_udp(target: &str) -> Result<TargetHandle, TunnelEstablishError>;
// UdpSocket::bind(("0.0.0.0", 0)) → connect(target) → the FRAMED adapter
// (UdpHalf + codec — ADR-003; truncation fail-loud per OQ-TN-13)
pub async fn dial_unix(path: &str) -> Result<TargetHandle, TunnelEstablishError>;
// UnixStream::connect → into_split (OQ-TN-14: ships with local v1 — cheap,
// same halves shape as TCP)
pub struct TcpListenerHalves { listener: tokio::net::TcpListener }
impl TcpListenerHalves {
pub async fn bind(addr: &str) -> Result<Self, ...>;
pub fn accept_fn(&self, queue: &AcceptQueue) -> impl Future; // the accept loop feeding the queue
}
pub struct UdpAssociateHalves { sock: Arc<UdpSocket> } // the POC's associate shape
```
- **UDP adapter (`UdpHalf`)**: port from the forward POC
(`params.rs`'s `UdpHalfRead`/`UdpHalfWrite`) with the truncation fix:
`poll_recv` into a too-small caller buffer must fail loud
(OQ-TN-13's resolved posture — ADR-003). The codec wraps at this
boundary; the pump never sees UDP specifics.
- **Error mapping** to `TunnelEstablishError` variants (the POC's
`Into<EstablishmentError>` path).
- `TargetHandle` re-used from producer.rs (the halves type lives in
producer.rs; local/ depends on it, never vice versa).
### Feature wiring
```toml
[features]
default = []
local = ["tokio/net"]
```
- `tokio/net` is the only dep addition (no new external crates; the
POCs proved `rt+sync+io-util+macros+time+net` covers everything).
- The default crate stays wasm-clean: `local`-gated code must not be
importable from `producer.rs`/`consumer.rs` (convention 16 — backend
modules are never imported from the shared/producer/consumer
modules).
- Unix is IN v1 (OQ-TN-14's lean-yes posture; same halves shape as
TCP); stdio bridging is NOT (different lifecycle — deferred).
### Tests
Behind the feature: TCP dial round-trip through the real establisher
path, UDP associate + datagram round-trip with the framed adapter,
unix dial round-trip, truncation fail-loud (a short recv surfaces as
an error), the 1400-byte MTU datagram through bounded buffers (the
POC's sizing test).
## Acceptance Criteria
- [ ] `cargo test --features local` green; `cargo test` (default)
unaffected
- [ ] `cargo test --all-features` green (AGENTS.md convention 14)
- [ ] wasm32 check on the DEFAULT crate passes; `local` is non-wasm by
design (documented)
- [ ] Truncation fails loud (the OQ-TN-13 test)
- [ ] No socket type appears outside `src/local/`
- [ ] Clippy/fmt clean
## References
- docs/architecture/overview.md §Feature Gates, §Dependencies
- docs/architecture/decisions/004-no-backend-trait.md (the injection
point), 003 (the framed adapter)
- POC reference: `/workspace/alktunnels-udp-poc/src/producer.rs`
(`UdpHalf`, the dial paths), `params.rs` (the UDP adapter)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+101
View File
@@ -0,0 +1,101 @@
---
id: tunnels/params
name: TunnelParams + open-op spec (schema, scope gate, constants)
status: pending
depends_on: [tunnels/crate-init]
scope: narrow
risk: low
impact: component
level: implementation
tags: [wire, params, open-op]
---
## Description
Implement `src/params.rs` per ADR-001 + wire.md §The Open Op: the
`TunnelParams` wire type, the `Substrate` discriminator, and the open-op
`OperationSpec` builder. This is the wire-stable surface (one-way door) —
exact conformance to the ADR is the point.
### Types
```rust
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TunnelParams {
pub resource: String,
pub substrate: Substrate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Substrate { Tcp, Udp, Unix }
```
- `deny_unknown_fields` is the loud-rejection posture (ADR-001: unknown
fields fail schema validation, never silently ignored).
- `Substrate::Unix` is included in the type (the wire enum per ADR-001)
but NOT in the open-op schema's enum list v1 (`["tcp", "udp"]`) until
OQ-TN-14 resolves — an older producer rejecting a newer substrate is
the SSH "unknown channel type" posture; the schema list is the
authoritative gate. Document this asymmetry in the type docs (type
extensible, schema conservative).
### The open-op spec (in producer.rs or params.rs — put it where
`tunnels/producer-open-op` can consume; the spec builder lives in
params.rs per the module map, producer.rs re-exports)
```rust
pub const OP_TUNNEL_OPEN: &str = "channels/tunnel/sub";
pub const TUNNEL_ALPN: &str = "alk/tunnel";
pub const TUNNEL_OPEN_SCOPE: &str = "tunnel:open";
pub fn tunnel_open_spec() -> OperationSpec
```
- `OperationType::Sub`; `Visibility::External`; channel-open marker
`ChannelOpenSpec::new(TUNNEL_ALPN)`.
- `input_schema`: `{resource: string (required), substrate: string enum
["tcp","udp"] (required)}` — JSON Schema Draft shape as the POCs used.
- `output_schema`: `{channel_id: integer > 0}`.
- `AccessControl.required_scopes: [TUNNEL_OPEN_SCOPE]` (ADR-006).
- `description`: a one-line human hint (round-trips through discovery;
the spec's SHOULD).
### Error surface
`src/error.rs`: `TunnelError` (thiserror) covering the codec + session
errors (filled incrementally by later tasks); plus the re-export of
`alkcall::channels::client::ChannelOpenError` and an
`establishment_reason(&CallError) -> Option<&str>` helper
(ADR-049 §4 surface; the POC consumer's shape).
## Acceptance Criteria
- [ ] `TunnelParams` round-trips serde; unknown fields rejected
- [ ] `tunnel_open_spec()` matches wire.md §The Open Op exactly (op id,
type, ALPN marker, schemas, ACL, description)
- [ ] Schema validation test: valid params pass; missing fields,
unknown fields, unknown substrate values all fail
- [ ] `establishment_reason` maps `channel:open_failed` details.reason
(POC-pinned shapes: `unknown_resource`, `dial_failed`,
`resource_shortage`)
- [ ] Unit tests cover the serde + schema shapes
- [ ] wasm32 check passes
## References
- docs/architecture/decisions/001-open-params-layout.md
- docs/architecture/wire.md §The Open Op
- docs/architecture/decisions/006-access-control-posture.md
- POC reference: `/workspace/alktunnels-udp-poc/src/params.rs` +
`producer.rs::tunnel_open_spec` (the shape, with `deny_unknown_fields`
added)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+92
View File
@@ -0,0 +1,92 @@
---
id: tunnels/producer-listen
name: Listen establisher — the producer-side listener variant
status: pending
depends_on: [tunnels/producer-open-op]
scope: narrow
risk: medium
impact: component
level: implementation
tags: [producer, establisher, listen, reverse]
---
## Description
Implement the listen establisher shape per producer.md §The Establisher
(shape 2): a producer whose resource is a LISTENER (SSH `-R` far-side
listener; a hub exposing its own port to a third party). Same open op,
same params, same typed errors — the establisher pops the next accepted
connection from an assembly-owned listener queue instead of dialing.
No new wire surface (ADR-004).
### API
```rust
pub type AcceptFn = Arc<
dyn Fn() -> BoxFuture<'static, Result<TargetHandle, TunnelEstablishError>>
+ Send + Sync,
>;
pub fn listen_establisher(registry: ResourceRegistry, accept: AcceptFn) -> OpenEstablisher
```
- The assembly layer owns the listener + its accept loop and injects an
`AcceptFn` that pops one accepted connection (a queue front). The
protocol crate never binds (OQ-TN-04).
- Error mapping (producer.md's table): empty queue during
establishment → `resource_shortage`; closed listener →
`dial_failed`; registry miss → `unknown_resource` (same as dial).
- The pump handler is UNCHANGED (the same `make_tunnel_pump_handler`) —
the listen variant is plan-flow with a different halves source.
### Assembly-side listener helper (the `local`-gated half lands in
`tunnels/local-socket-halves`; here, define the protocol-side queue
contract)
```rust
pub struct AcceptQueue { /* Mutex<VecDeque<TargetHandle>> + notify */ }
impl AcceptQueue {
pub fn push(&self, handle: TargetHandle); // the accept loop feeds
pub async fn pop(&self) -> Option<TargetHandle>; // the establisher pops (bounded wait)
}
```
- `pop` during establishment: bounded (the establisher's own deadline
applies — ADR-049 §2); `None``resource_shortage`.
### Tests
Extend the integration suite with a listen-producer topology: an
in-process listener queue fed by a test accept loop; a consumer opens
toward it; the accepted handle is the plan payload; the two-pump
round-trip completes. Cover: pop-during-establishment ordering (the
always-before-take guarantee the wrapper's await order gives), empty
queue → `resource_shortage`, closed listener → `dial_failed`.
## Acceptance Criteria
- [ ] `listen_establisher` + `AcceptFn` + the queue contract compile
and wire through `register_tunnel_openable`'s shape (a listen
variant of the registration or a dial/accept enum — pick the
honest shape, document it)
- [ ] The pump handler is untouched (same function registered for
listen producers)
- [ ] Integration tests: the listen flow end-to-end + the two typed
error paths
- [ ] Clippy/fmt clean; wasm32 check passes
## References
- docs/architecture/producer.md §The Establisher (shape 2)
- docs/architecture/decisions/004-no-backend-trait.md (the listen
variant rationale)
- Reverse POC (the hub's accept loop stands in for the assembly
listener — `docs/research/reverse-poc-summary.md` §The -R template)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+125
View File
@@ -0,0 +1,125 @@
---
id: tunnels/producer-open-op
name: Producer half — establisher (dial shape) + pump handler + register_tunnel_openable
status: pending
depends_on: [tunnels/params, tunnels/wire-codec]
scope: broad
risk: high
impact: phase
level: implementation
tags: [producer, establisher, open-op, pump]
---
## Description
Implement `src/producer.rs` per producer.md: the dial-shape establisher,
the substrate-agnostic pump handler, and the registration surface. This
is the crate's core — the open op that both POCs validated (the forward
POC's producer on the accept side; the reverse POC's worker on the
connect side). Port from the REVERSE POC first
(`/workspace/alktunnels-reverse-poc/src/producer.rs`) — it is the 0.6/0.7
idiomatic shape (plan flow, no handoff) and its 16 tests are the
reference suite.
### The establisher (dial shape — the listen shape is a separate task)
```rust
pub type DialFn = Arc<
dyn Fn(Substrate, &str) -> BoxFuture<'static, Result<TargetHandle, TunnelEstablishError>>
+ Send + Sync,
>;
// TargetHandle: boxed halves (read: Box<dyn AsyncRead + Send + Sync + Unpin>, write: ...),
// or the framed UDP adapter (the establisher wraps BEFORE returning the plan —
// ADR-003's placement; the pump stays substrate-agnostic)
```
- `tunnel_establisher(registry: ResourceRegistry, dial: DialFn,
identity_witness: Option<...>) -> OpenEstablisher` — the assembly layer
injects the substrate dial function (ADR-004); the establisher:
1. Parses params (`TunnelParams` — schema-validated upstream, semantic
parse here).
2. Registry lookup `(resource, substrate)` → backing address —
`unknown_resource` on miss (typed reason mapping per ADR-001's
table).
3. `dial(substrate, backing)` → the handle (the closure owns real
socket code — behind `local` or assembly-constructed).
4. UDP: wrap in the framed adapter (ADR-003) — `UdpHalf` + codec,
truncation fail-loud (OQ-TN-13).
5. Return `Ok(Establishment::new(Arc::new(handle)))` (R-01 plan flow).
- `ResourceRegistry`: the assembly-owned
`HashMap<(String, Substrate), String>` (POC shape; OQ-TN-11's
collision domain) with `register`/`lookup` async methods.
### The pump handler
```rust
pub fn make_tunnel_pump_handler() -> OpenHandler
```
1. Downcast the plan to `TargetHandle` (`Arc::downcast` — error path:
log + return; the birth-teardown telemetry catches it).
2. `conn.accept_bi()` (yield-once).
3. `pump_bidi(bidi, t_read, t_write)` — **awaited inline** (R-02: the
returned `JoinHandle` tracks the data-plane lifetime; early return =
teardown-at-birth). The handler is substrate-agnostic by
construction — it must not know what the halves came from
(ADR-004).
### Registration
```rust
pub fn register_tunnel_openable(
core: &ChannelCore,
registry: &ResourceRegistry,
on_registry: &Arc<OperationRegistry>, // the session's dispatch registry (ADR-047 §4 fork)
auth: AuthContext,
dial: DialFn,
) -> Result<(), String>
```
- Wires `tunnel_open_spec()` + the establisher + the pump via
`register_openable_with_establisher` (timeout `None` = the 10s
default).
- Post-hoc registration is supported (W2): the dispatcher reads through
the shared `Arc` per dispatch.
### Integration tests (the POC suite, ported)
Port the reverse POC's test topology (`harness.rs`: worker =
`from_connection_with_serving` + `ChannelOperations::register_on` +
post-hoc openable; hub = adapter + capturing install hook) into
`tests/` with the `dial` closure standing in for real sockets
(in-process duplex halves — no `local` feature needed). Cover:
establishment success + typed errors (unknown_resource / dial_failed /
FORBIDDEN / timeout-adjacent), the plan flow under same-resource
concurrency, per-call opener identity (CF-006 witness),
late-registration visibility, pump round-trip through `pump_bidi`.
## Acceptance Criteria
- [ ] Establisher: typed reason mapping exact (ADR-001's table)
- [ ] Pump handler: `pump_bidi` inline; no spawn-and-forget; no
substrate types leak into the handler
- [ ] `register_tunnel_openable` wires spec + establisher + pump +
dial injection
- [ ] Integration tests: forward AND reverse topology (the harness
shapes from both POCs), ≥8 tests covering the above
- [ ] Clippy/fmt clean; wasm32 check passes (dial closures are
runtime-injected — the crate compiles without `local`)
- [ ] `cargo test` green
## References
- docs/architecture/producer.md (the normative shapes)
- docs/architecture/decisions/001/003/004 (params, codec placement, no
trait)
- POC reference: `/workspace/alktunnels-reverse-poc/src/producer.rs` +
`harness.rs` + `tests/tunnel_poc.rs` (the 0.7.0-idiomatic shapes)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+56
View File
@@ -0,0 +1,56 @@
---
id: tunnels/review-core-crates
name: Mid-phase review — producer + consumer halves before the local feature
status: pending
depends_on: [tunnels/consumer-session]
scope: moderate
risk: low
impact: phase
level: review
tags: [review, injection-point]
---
## Description
Review injection point (SDD §Review Injection — "high-risk tasks: review
before proceeding"): `tunnels/producer-open-op` and
`tunnels/consumer-session` are the two `risk: high, impact: phase` tasks
and the crate's wire/API surface. Review them BEFORE
`tunnels/local-socket-halves` and `tunnels/end-to-end-suite` build
against the shapes — a wire or API mistake found here costs one task's
rework; found later it costs three.
### Checklist (focused — the full gate is `tunnels/review-impl`)
1. Wire conformance of what landed (params/codec/open-op vs wire.md +
ADR-001/003) — the one-way-door check.
2. The pump handler shape: `pump_bidi` inline, JoinHandle tracks the
data plane (R-02) — the POC's hang-bug class must be structurally
absent.
3. Teardown matrix of `TunnelSession` — close/join/Drop soundness, no
leaks (the W3 class).
4. No substrate types outside `src/local/`-to-be; no hand-rolled
two-pump loops; no side-channel handoff.
5. Integration tests green + repeat-run stable (3×).
Deliverable: findings inline here (Notes/Summary); criticals block the
downstream tasks (Safe Exit); majors create remediation notes for
`tunnels/review-impl` to re-check.
## Acceptance Criteria
- [ ] The 5 checklist items each with a verdict
- [ ] Criticals (if any) resolved before proceeding; majors logged
## References
- docs/architecture/ (the spec set under review)
- docs/sdd_process.md §Review Injection
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+86
View File
@@ -0,0 +1,86 @@
---
id: tunnels/review-impl
name: Review alktunnels v1 implementation for spec conformance (pre-release gate)
status: pending
depends_on: [tunnels/end-to-end-suite, tunnels/review-core-crates]
scope: moderate
risk: low
impact: project
level: review
tags: [review, phase-gate]
---
## Description
Review the v1 implementation against the architecture spec before any
publish consideration. This is the phase-gate review (the SDD process's
review-injection point at the end of the critical path): the wire
surface (ADR-001/002/003) is a one-way door, and the API surface
(ADR-005/006) becomes ABI-stable once a consumer exists.
### Review checklist
1. **Wire conformance** (wire.md + ADR-001/002/003):
- params shape exact (`{resource, substrate}`, `deny_unknown_fields`,
schema enum without `unix` until OQ-TN-14)
- op id `channels/tunnel/sub`, ALPN marker `alk/tunnel`, scope
`tunnel:open`
- codec: `[len: u16 BE]`, `len=0` = legal empty datagram, `Oversize`
at frame time, no sentinel in the codec
- typed errors: the five establishment reasons + `FORBIDDEN` +
`channel:too_many_channels` surfaces
2. **Producer conformance** (producer.md):
- establisher = the awaited bounded phase; plan flow (R-01) — no
side-channel handoff anywhere (grep for `Mutex<HashMap>` +
poll-loop take patterns — the POC's dead shape)
- pump handler: `pump_bidi` inline, no substrate types, JoinHandle
tracks the data plane (R-02)
- registration: post-hoc supported (W2); `ChannelOperations::
register_on` guidance correct
- listen variant: same op, no new wire surface, typed error mapping
3. **Consumer conformance** (consumer.md + ADR-005):
- `TunnelSession`: open/adopt/stream_halves/take_halves/
send_datagram/recv_datagram/pump_against/close/join/Drop
- teardown matrix sound (no leaks; pump-less join `(0, 0, reaped)`)
- no `Clone`
4. **Conventions sweep** (AGENTS.md):
- no comments in code (doc comments fine); no unwrap/expect outside
tests; thiserror everywhere; poisoned-lock `unwrap_or_else(into_inner)`
- substrate types confined to `src/local/` (grep-verify: no
`tokio::net` outside the feature gate + tests)
- wasm-clean default crate (the structural guard passed at every
step, but re-verify)
- `pump_bidi` consumed, never hand-rolled (grep for hand-rolled
two-pump loops)
5. **Docs ↔ implementation sync**: lib.rs doc comments match
overview.md's module map; public API = lib.rs re-exports (convention
16); ADR statuses updated from Draft → Accepted where the
implementation confirms them (or findings filed back to the OQ
tracker).
### Deliverables
Findings as a review doc (`docs/reviews/001-implementation-review.md`,
the alkhttp/alkcall house pattern) with severity levels + concrete
remediation tasks. Criticals block the phase; majors get remediation
tasks; minors get a follow-up batch.
## Acceptance Criteria
- [ ] Review doc filed with severity-graded findings
- [ ] Remediation tasks created for anything above trivial
- [ ] The 5 checklist sections each covered with a verdict
## References
- docs/architecture/ (the full spec set)
- House pattern: `/workspace/@alkdev/alkcall/docs/reviews/` (the review
numbering + severity legend)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.
+91
View File
@@ -0,0 +1,91 @@
---
id: tunnels/wire-codec
name: Data-plane codec (frame_datagram / DatagramReader) + sentinel layering tests
status: pending
depends_on: [tunnels/crate-init, tunnels/params]
scope: narrow
risk: medium
impact: component
level: implementation
tags: [wire, codec, udp]
---
## Description
Implement `src/wire.rs` per ADR-003 + wire.md §The Data Plane + bast.md:
the mandatory UDP length-framing codec. This is a direct port of the
forward POC's `/workspace/alktunnels-udp-poc/src/wire.rs` (17 tests rode
it; the shape is settled) with the POC-to-crate generalizations the ADRs
pin.
### API
```rust
pub const MAX_DATAGRAM_LEN: usize = u16::MAX as usize; // 65535
pub struct DatagramCodecError { .. } // thiserror: Oversize(usize), InvalidLength(...)
pub fn frame_datagram(payload: &[u8]) -> Result<Bytes, DatagramCodecError>
// [len: u16 BE][payload]; len=0 is a legal empty datagram; >65535 = Oversize at frame time
pub struct DatagramReader { /* incremental decoder state */ }
impl DatagramReader {
pub fn new() -> Self
pub fn feed(&mut self, chunk: &[u8]) -> Result<Vec<Bytes>, DatagramCodecError>
// zero or more complete datagrams per chunk; buffers partial frames
// across chunk boundaries
}
```
### The invariants the tests MUST pin (all POC-validated; they are the
spec's executable form)
1. Single datagram round-trip (frame → feed → exact bytes out).
2. Empty datagram (`len=0`): survives as a real datagram — NEVER
confused with EOF (the F-2 invariant; the codec layer never emits a
zero-length read).
3. Split across chunks (feed a frame in awkward 7-byte chunks; exact
reassembly).
4. Two datagrams batched in one chunk (feed once, two out, in order).
5. Partial header at a chunk boundary (1-byte `len` prefix split).
6. Mid-datagram state observable (incremental decode correctness).
7. Oversize rejected at frame time (never a wire overflow).
8. Truncation fail-loud (OQ-TN-13): the adapter-level receive that
would truncate surfaces as an error — test at the codec boundary by
asserting `frame_datagram` + reader round-trip with a buffer
smaller than a full datagram is not silently accepted (the concrete
API shape lands with `tunnels/local-socket-halves`; here, pin the
codec-side invariant: a truncated stream yields an error, not a
partial datagram).
No `stream_type` byte, no 5-byte header — a tunnel has one data stream
per direction (ADR-003). No EOF sentinel in the codec — EOF is the
channels-level `length=0` chunk, a different layer (the two coexist;
the POC proved no collision).
## Acceptance Criteria
- [ ] All 8 test families above pass (the POC's 7 + the truncation pin)
- [ ] `frame_datagram` returns `Bytes` (zero-copy handoff to the mux)
- [ ] `DatagramReader` state is incremental across arbitrary chunk
boundaries
- [ ] `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`
clean
- [ ] wasm32 check passes (the codec is pure byte work — it must be
wasm-clean)
## References
- docs/architecture/decisions/003-codec-and-udp-framing.md
- docs/architecture/bast.md (the binary contract)
- docs/architecture/wire.md §The Data Plane (normative byte diagrams)
- POC reference: `/workspace/alktunnels-udp-poc/src/wire.rs` (port with
the crate's doc-comment style)
## Notes
> Agent fills during implementation.
## Summary
> Agent fills this on completion.