- Rewrote em dash prose across the whole tree outside docs/reviews/ (the historical reviews stay verbatim, matching the alktunnels and alktty precedent): src doc comments and inline comments (587), docs/architecture (319), docs/research + sdd_process (484), tests (361), tasks (899), fuzz (28), README/CHANGELOG/Cargo.toml (77), AGENTS.md (85), and the .opencode/agents prompts (109) - Fenced content follows the precedent: comments, ASCII-diagram label separators, and template placeholders rewritten; wire data, JSON/BAST documents, byte diagrams, and transcripts survive verbatim; assert, expect, panic, error-display, and format message literals rewritten as prose; no code, fixture, or test data change - src/rfc1928/LICENSE: the two dashes in alksocks' own provenance header rewritten; the MIT legal text untouched - AGENTS.md adds Project Convention 18: no em dashes in prose; varied restructure, no spaced-hyphen substitutes, fenced prose rule, en dash numeric ranges exempt, docs/reviews/ exempt from the greps - tasks/*.md frontmatter: the name values rewritten by the sweep are now quoted (a raw colon inside an unquoted YAML scalar parsed as null); taskgraph validate reports 37 tasks, no issues Verification: cargo test; cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; fuzz corpus replay (cargo test --manifest-path fuzz/shared/Cargo.toml); wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/ and the bast.md BAST JSON data fence
8.6 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level |
|---|---|---|---|---|---|---|---|
| implementation/composed-egress | Composed: the direct-op egress (lazy destination→tunnel table) | completed | moderate | medium | component | implementation |
Description
The composed feature's egress provider (ADR-008's composed
variants): the ASSOCIATE egress that dials each destination through
an alktunnels 0.2.0 direct-op channel (TunnelSession::open_direct,
Substrate::Udp): per-target ACL for free (each destination rides
its own tunnel:direct-governed channel), the lazy
destination→tunnel table LRU-evicted at a cap (POC #2's
ComposedEgress shape).
Deliverables:
Cargo.toml:composed = ["dep:alktunnels"](the only feature that adds the alktunnels dependency, ADR-009); alktunnels pinned"0.2.0"(the sibling pin posture; convention 11's deliberate-bump rule applies). Verify the composed graph stays wasm-clean (alktunnels' default crate is wasm-clean; the composed providers use only its ungated channels surface, and the wasm check with--features composedis a hard gate).src/composed/mod.rs(featurecomposed): the composed providers, named per producer.md §Wiring:direct_egress(...)→EgressFactory: the POC #2ComposedEgressshape ported to the landed seam types: a lazy destination→tunnel table: each new destination opens one alktunnels UDP-tunnel channel (open_direct,Substrate::Udp), a relay task per tunnel drains inbound datagrams into a shared queue, the write half stays in the table (LRU-capped; dropping a write half EOFs the tunnel producer-side; the session's Drop reaps the channel entry (alktunnels ADR-005).- The Sync-bound wrinkle (the batch-1 review's seam contract vs
alktunnels' surface): the landed
TargetHandle/BindTarget Handlehalves areBox<dyn AsyncRead + Send + Sync + Unpin>, but alktunnels'TakenHalvesyieldsSend + Unpinonly. The composed providers need an owner-wrapper inside the handle payloads (egress.rs's documented posture: "keep-alive state a substrate needs rides inside the halves; store a provider-side owner wrapper"). For the egress the wrapper is the table entry's session (the egress owns it, not a handle payload); for the composed accept (the sibling task) the wrapper is load-bearing, so design it here and share it. - The readiness contract (load-bearing):
next_replyparks on the shared inbound queue (recv().await, Pending until a relay task delivers); a closed queue (all relays ended) returnsErr, never a hotOk(None)(the POC's closed-queue arm; the stage's zero-yield defense, review 002 R-1, is the backstop, not the design). Replies carry inner bytes (addr + data; the reply address is the entry's recorded destination; framing-once). - Per-datagram
sendis awaited inline (the stage interleaves sends with reply drains), so a slow egress applies backpressure to the association, not to other channels (wire.md's note: "the batch-2 composed-egress author inherits this bound knowingly"). The table's open cost rides the send path (lazy open on first datagram; the POC's measured ~0.8 ms). - The resource/target mapping:
open_directtakes aSubstrateAddr: the destination'sTargetAddrrenders to it (IP forms direct; domain form passes through unresolved, producer-side resolution, the ssh-Dsemantic; ADR-012). Constructor config carries the LRU cap (the 256-channel default cap bounds fan-out, ADR-010's posture; the cap is config, not wire). - Module doc: names the ADRs (008/009), the per-target-ACL benefit, the readiness contract, the framing-once invariant, the backpressure bound.
- Tests (
tests/composed-egress.rs): the POC #2 shape end to end over channels: a tunnel-producing peer (the alktunnels registry shape,register_tunnel_direct_openable), the composed egress against it, datagram round-trips through the tunnel (multiple destinations → multiple tunnels; the LRU eviction at cap pinned by test); the readiness contract re-pinned (park-then- datagram, no spin); the closed-queue → association-end shape; framing-once (no double prefix, the POC #2 bug class).
Acceptance Criteria
cargo testpasses;cargo test --all-featurespassescargo check --features composed --target wasm32-unknown- unknownclean; the composed graph is wasm-clean (hard gate)cargo clippy --all-targets --all-features -- -D warningsclean;cargo fmt --checkcleanalktunnelspinned"0.2.0"; the dep ridescomposedalone (default graph unchanged, per thecargo treespot-check)- The lazy table + LRU eviction land per the POC shape (pinned by test)
- The readiness contract honored (real async park; closed queue
→
Err, never a hotOk(None)), pinned by test - Framing-once (replies are inner bytes; no double prefix), pinned by test
- Per-datagram send backpressure documented on the constructor
References
- docs/architecture/decisions/008-provider-defaults.md (the composed variants), 009-feature-gates.md, 003 (the handle swap), 010 (the cap), 012 (resolution)
- docs/architecture/producer.md §Wiring (
composed::direct_egress), §The Three Seams - POC reference:
/workspace/alksocks-udp-poc/src/egress.rs(ComposedEgress: the table, the relay tasks, the closed-queue posture) - Landed surfaces:
src/egress.rs(the seam contracts + the owner-wrapper posture), alktunnels 0.2.0 (TunnelSession::open_direct,TakenHalves,register_tunnel_direct_openable)
Notes
Landed as
src/composed/mod.rs(featurecomposed) +tests/composed-egress.rs, with awire_tunnel_directtopology added totests/harness.rs(the consumer-side topology with alktunnels' direct op on the producer's serving registry; the composed egress'sChannelClientis the direct-op caller).Two deviations from the POC shape, both deliberate:
- The closed-queue condition is a live-relay count, not a queue close. The egress holds its own factory
Sender, so the sharedmpscqueue can never fully disconnect:recv()would never returnNoneand the POC's closed-queue arm was unreachable through this surface. Relays instead emit anEndedmarker on their tunnel's EOF;next_replycounts them (live_relays) and returns the association-endErrwhen the count reaches zero and the queue is drained. The readiness contract is unchanged (the wait is a real async park; a drained-but-live relay set parks).- The tunnel codec is alktunnels'
wire::DatagramReader([len]+ payload), not this crate's RFC-framed one; the POC fed the tunnel read half into the SOCKS wire reader (a bug class the round-trip test caught during bring-up); the composed egress frames the tunnel wire once with the tunnel codec and the RFC wire once downstream (framing-once holds at both boundaries).The Sync-bound wrinkle resolves as designed: the shared
SessionOwnerwrapper (half + session in one owner) is exported for the composed accept (the sibling task); the egress itself owns the session in the table entry (the POC's shape) and the relay task owns the bare read half.
cargo tree --edges normalspot-check:alktunnelsridescomposedalone (absent from the default normal graph; the dev-dep copy is dev-only).
Summary
The composed egress landed:
composed = ["dep:alktunnels"](alktunnels pinned "0.2.0"; dep ridescomposedalone),src/composed/mod.rs(direct_egress, theEgressFactory), the lazy destination→tunnel table with LRU eviction at cap (DEFAULT_ MAX_TUNNELS= 256), per-tunnel relay tasks draining into a shared inbound queue,SessionOwner(the owner wrapper shared with the composed accept), the readiness contract (real async park; ended relays → association-endErr, never a hotOk(None)), framing- once, per-target ACL via the direct op, domain pass-through unresolved (ADR-012), and the backpressure bound documented on the constructor.tests/composed-egress.rs(6 tests over thewire_tunnel_directharness topology): round-trips per destination → one tunnel each, table reuse, LRU eviction/re-dial, the readiness park, the closed-relay-setErr, framing-once through the wire layer, and the domain pass-through. Verification:cargo test(default) +--all-featurespass; the wasm check with--features composedclean; clippy (--all-targets --all-features -- -D warnings) clean;cargo fmt --checkclean;cargo doc --no-depsclean.