Files
glm-5.3-flash e196ed43e3 docs: remove em dashes from repo prose and forbid them going forward
- 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
2026-11-03 19:31:53 +00:00

173 lines
8.6 KiB
Markdown

---
id: implementation/composed-egress
name: "Composed: the direct-op egress (lazy destination→tunnel table)"
status: completed
depends_on: []
scope: moderate
risk: medium
impact: component
level: 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:
1. **`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 composed` is a hard gate).
2. **`src/composed/mod.rs`** (feature `composed`): the composed
providers, named per producer.md §Wiring:
- `direct_egress(...)` → `EgressFactory`: the POC #2
`ComposedEgress` shape 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
Handle` halves are `Box<dyn AsyncRead + Send + Sync + Unpin>`,
but alktunnels' `TakenHalves` yields `Send + Unpin` only. 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_reply` parks on
the shared inbound queue (`recv().await`, Pending until a
relay task delivers); a closed queue (all relays ended) returns
`Err`, never a hot `Ok(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 `send` is 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_direct` takes a
`SubstrateAddr`: the destination's `TargetAddr` renders to it
(IP forms direct; domain form passes through unresolved,
producer-side resolution, the ssh `-D` semantic; 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.
3. **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
- [x] `cargo test` passes; `cargo test --all-features` passes
- [x] `cargo check --features composed --target wasm32-unknown-
unknown` clean; the composed graph is wasm-clean (hard gate)
- [x] `cargo clippy --all-targets --all-features -- -D warnings`
clean; `cargo fmt --check` clean
- [x] `alktunnels` pinned `"0.2.0"`; the dep rides `composed` alone
(default graph unchanged, per the `cargo tree` spot-check)
- [x] The lazy table + LRU eviction land per the POC shape (pinned
by test)
- [x] The readiness contract honored (real async park; closed queue
→ `Err`, never a hot `Ok(None)`), pinned by test
- [x] Framing-once (replies are inner bytes; no double prefix),
pinned by test
- [x] 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` (feature `composed`) +
> `tests/composed-egress.rs`, with a `wire_tunnel_direct` topology
> added to `tests/harness.rs` (the consumer-side topology with
> alktunnels' direct op on the producer's serving registry; the
> composed egress's `ChannelClient` is 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 shared
> `mpsc` queue can never fully disconnect: `recv()` would never
> return `None` and the POC's closed-queue arm was unreachable
> through this surface. Relays instead emit an `Ended` marker on
> their tunnel's EOF; `next_reply` counts them (`live_relays`) and
> returns the association-end `Err` when 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
> `SessionOwner` wrapper (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 normal` spot-check: `alktunnels` rides
> `composed` alone (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 rides `composed` alone),
> `src/composed/mod.rs` (`direct_egress`, the `EgressFactory`), 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-end `Err`, never a hot `Ok(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 the
> `wire_tunnel_direct` harness topology): round-trips per
> destination → one tunnel each, table reuse, LRU eviction/re-dial,
> the readiness park, the closed-relay-set `Err`, framing-once
> through the wire layer, and the domain pass-through. Verification:
> `cargo test` (default) + `--all-features` pass; the wasm check with
> `--features composed` clean; clippy (`--all-targets --all-features
-- -D warnings`) clean; `cargo fmt --check` clean; `cargo doc
> --no-deps` clean.