diff --git a/tasks/architecture/oq-promotion-sync.md b/tasks/architecture/oq-promotion-sync.md new file mode 100644 index 0000000..f21bf14 --- /dev/null +++ b/tasks/architecture/oq-promotion-sync.md @@ -0,0 +1,62 @@ +--- +id: architecture/oq-promotion-sync +name: Sync AGENTS.md + phase-0 ledger pointers to the promoted OQ ledger (planning) +status: pending +depends_on: [] +scope: single +risk: trivial +impact: project +level: planning +tags: [architecture, phase-1, oq] +--- + +## Description + +Phase 1 promoted the Phase 0 OQ ledger into `docs/architecture/open-questions.md` +(OQ-TN-01..14) and split OQ-TN-11/13 into resolved halves (ADR-001/ADR-003). +The older ledgers still describe pre-Phase-1 states in two places: + +1. `docs/research/phase-0-findings.md` — the OQ sections (OQ-TN-01..10) should + carry one-line "promoted to `docs/architecture/open-questions.md` as OQ-TN-NN" + pointers so a reader landing in the research doc finds the live tracker (the + convergence checklist's last unchecked item says "promoted ... with statuses" — + the promotion exists; the back-pointer is the missing half). +2. `AGENTS.md` — the "Architecture Context" section says `docs/architecture/` + "will hold the authoritative spec (Phase 1 of the SDD process)... It does not + exist yet". That is now stale: the spec exists (ADR-001..006 + 4 spec docs + + BAST + OQ tracker). Update the paragraph to point at the real directory and + adjust the "read it before non-trivial changes once it exists" line. + +## Work + +- phase-0-findings.md: add a promotion note at the top of the Open Questions + section (and one line under the convergence checklist's final item) pointing + at `docs/architecture/open-questions.md`; mark the checklist item done. +- AGENTS.md: rewrite the `docs/architecture/` bullet to the present tense + (spec exists; read it before non-trivial changes; ADRs 001..006 are + Accepted; the OQ tracker is authoritative for open questions). + +## Verification + +- [ ] phase-0-findings.md has the promotion pointer; the checklist's final + item is checked +- [ ] AGENTS.md architecture-context paragraph reflects the existing spec +- [ ] No other stale "does not exist yet" language anywhere in AGENTS.md or + docs/ + +## Out of scope + +- Any change to the OQ statuses themselves (that is the tracker's job now) + +## References + +- docs/architecture/open-questions.md (the promoted ledger) +- docs/sdd_process.md §Safe Exit (the two-halves rule for deferrals) + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills this on completion. \ No newline at end of file diff --git a/tasks/architecture/oq-tn-14-tracker.md b/tasks/architecture/oq-tn-14-tracker.md new file mode 100644 index 0000000..3e60d9d --- /dev/null +++ b/tasks/architecture/oq-tn-14-tracker.md @@ -0,0 +1,58 @@ +--- +id: architecture/oq-tn-14-tracker +name: "[external-trigger, deferred-oq] OQ-TN-14 unix/stdio placement tracker" +status: pending +depends_on: [] +scope: single +risk: trivial +impact: isolated +level: research +tags: [architecture, deferred-oq, external-trigger, unix, stdio] +--- + +## Description + +External-trigger tracker for OQ-TN-14 (`docs/architecture/open-questions.md` +— unix-socket and stdio substrate placement, currently `open` leaning +unix-yes-v1). This task is NOT actionable implementation work; it tracks +whether the unblocking condition has arrived. If the condition arrives, this +task flips the OQ (and the implementation task `tunnels/local-unix-halves` +below) from its deferred/open posture to actionable. + +Unblocking conditions (any one): + +1. A consumer needs unix-socket tunneling through alktunnels (a hub exposing + a docker container's unix socket, or a local daemon bridge). +2. The `tunnels/crate-init` task lands and the `local` feature module needs + its v1 surface finalized (the decision point baked into that task). + +If neither arrives by the end of the v1 implementation phase, the OQ stays +open and `local` ships TCP+UDP only (the lean posture ADR-004 endorses). + +## Work + +- Re-read OQ-TN-14 + `docs/architecture/overview.md` §Feature Gates when + triaging; confirm whether the unix helper ships with `local` v1. + +## Verification + +- [ ] Tracked (this file existing and being reviewed at triage time IS the + tracking mechanism — no code or doc change is expected until the + condition arrives) + +## Out of scope + +- Implementing the unix/stdio helpers (see `tunnels/local-unix-halves`) + +## References + +- docs/architecture/open-questions.md OQ-TN-14 +- docs/sdd_process.md §Deferred OQs and the blocker-task half + +## Notes + +> Agent fills during implementation. + +## Summary + +> Agent fills this on completion. \ No newline at end of file diff --git a/tasks/tunnels/consumer-session.md b/tasks/tunnels/consumer-session.md new file mode 100644 index 0000000..e5c704d --- /dev/null +++ b/tasks/tunnels/consumer-session.md @@ -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` — `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) -> Result` — 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` 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, 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. \ No newline at end of file diff --git a/tasks/tunnels/crate-init.md b/tasks/tunnels/crate-init.md new file mode 100644 index 0000000..559fb6d --- /dev/null +++ b/tasks/tunnels/crate-init.md @@ -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. \ No newline at end of file diff --git a/tasks/tunnels/end-to-end-suite.md b/tasks/tunnels/end-to-end-suite.md new file mode 100644 index 0000000..71e813b --- /dev/null +++ b/tasks/tunnels/end-to-end-suite.md @@ -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. \ No newline at end of file diff --git a/tasks/tunnels/local-socket-halves.md b/tasks/tunnels/local-socket-halves.md new file mode 100644 index 0000000..3794e37 --- /dev/null +++ b/tasks/tunnels/local-socket-halves.md @@ -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; +// TcpStream::connect (5s timeout per the POCs) → into_split → boxed halves + +pub async fn connect_udp(target: &str) -> Result; +// 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; +// 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; + pub fn accept_fn(&self, queue: &AcceptQueue) -> impl Future; // the accept loop feeding the queue +} + +pub struct UdpAssociateHalves { sock: Arc } // 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` 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. \ No newline at end of file diff --git a/tasks/tunnels/params.md b/tasks/tunnels/params.md new file mode 100644 index 0000000..000fd05 --- /dev/null +++ b/tasks/tunnels/params.md @@ -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. \ No newline at end of file diff --git a/tasks/tunnels/producer-listen.md b/tasks/tunnels/producer-listen.md new file mode 100644 index 0000000..ed7fbb0 --- /dev/null +++ b/tasks/tunnels/producer-listen.md @@ -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> + + 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> + notify */ } +impl AcceptQueue { + pub fn push(&self, handle: TargetHandle); // the accept loop feeds + pub async fn pop(&self) -> Option; // 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. \ No newline at end of file diff --git a/tasks/tunnels/producer-open-op.md b/tasks/tunnels/producer-open-op.md new file mode 100644 index 0000000..43693ba --- /dev/null +++ b/tasks/tunnels/producer-open-op.md @@ -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> + + Send + Sync, +>; +// TargetHandle: boxed halves (read: Box, 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, // 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. \ No newline at end of file diff --git a/tasks/tunnels/review-core-crates.md b/tasks/tunnels/review-core-crates.md new file mode 100644 index 0000000..92bc94f --- /dev/null +++ b/tasks/tunnels/review-core-crates.md @@ -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. \ No newline at end of file diff --git a/tasks/tunnels/review-impl.md b/tasks/tunnels/review-impl.md new file mode 100644 index 0000000..814003d --- /dev/null +++ b/tasks/tunnels/review-impl.md @@ -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` + + 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. \ No newline at end of file diff --git a/tasks/tunnels/wire-codec.md b/tasks/tunnels/wire-codec.md new file mode 100644 index 0000000..74ad62e --- /dev/null +++ b/tasks/tunnels/wire-codec.md @@ -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 +// [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, 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. \ No newline at end of file