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:
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user