- Rewrote the remaining em dashes outside docs/reviews/ (preserved as historical record, matching the alktty precedent): tests/ (313), fuzz/ (34 plus one shell comment), tasks/ (671 plus 12 kept in fences), docs/research + sdd_process + plans (525 plus 25 in fences), docs/architecture/overview.md fence labels (13), README, CHANGELOG, AGENTS.md, fuzz/README.md (176), and the .opencode/agents prompts (110) - Rewrote fenced content too: ASCII tree and module-map label separators, Rust snippet comments, and template placeholders; wire data, JSON/BAST examples, and transcripts survive verbatim - Rewrote 6 assert/expect message literals in tests/ and 2 format strings in fuzz/shared/src; no test data changed - Added Project Convention 18: no em dashes in prose (doc comments, markdown including docs/ and tasks/, agent prompts, commit messages); fence content rule, en-dash clause rule, and the no-spaced-hyphen prohibition follow alktty's convention 15 Follows6b20924(src doc comments) andcd0afe0(docs/architecture). Verification: cargo test (141 pass); cargo clippy --all-targets -D warnings; cargo fmt --check; cargo doc --no-deps; wasm check and clippy for wasm32-unknown-unknown with -D warnings; em dash grep over the repo returns matches only in docs/reviews/
6.6 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| tunnels/local-socket-halves | local feature: real socket halves functions (TCP dial, UDP connect, unix) | completed |
|
moderate | medium | component | implementation |
|
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
// 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 per ADR-003; truncation fails 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; it is 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'sUdpHalfRead/UdpHalfWrite) with the truncation fix:poll_recvinto a too-small caller buffer must fail loud (OQ-TN-13's resolved posture, i.e. ADR-003). The codec wraps at this boundary; the pump never sees UDP specifics. - Unix:
dial_unixships (OQ-TN-14 resolved, with the same halves shape as TCP). Stdio does NOT, because a spawned process's stdio is alktty's pipe mode (exit codes + signals are tty semantics, not tunnel semantics); remote command execution composes via alktty, not here. - Error mapping to
TunnelEstablishErrorvariants (the POC'sInto<EstablishmentError>path). TargetHandlere-used from producer.rs (the halves type lives in producer.rs; local/ depends on it, never vice versa).
Feature wiring
[features]
default = []
local = ["tokio/net"]
tokio/netis the only dep addition (no new external crates; the POCs provedrt+sync+io-util+macros+time+netcovers everything).- The default crate stays wasm-clean:
local-gated code must not be importable fromproducer.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 (a 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 localgreen;cargo test(default) unaffectedcargo test --all-featuresgreen (AGENTS.md convention 14)- wasm32 check on the DEFAULT crate passes;
localis 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.
bind_tcp+TcpListenerHalves::accept_loop(queue)complete the listen side (the task surface sketchedbind/accept_fn): the accept loop is a spawned task feeding an [AcceptQueue], the protocol-side queue contract from producer-listen. The assembly layer aborts the task on teardown; accept errors end the loop.UdpHalftruncation probe (OQ-TN-13):poll_readrecvs into a 64KiB scratch buffer first, then size-checks against the caller's buffer, so a >buffer datagram isio::ErrorKind::InvalidData, never a silent truncation. (tokio'spoll_recv-into-ReadBuf truncates silently, which is exactly the invariant F-2/OQ-TN-13 forbids.) WouldBlock pollspoll_recv_readyfor waker registration.- The task sketch's
TcpListenerHalves::accept_fn(&self, queue)becameaccept_loop(&self, queue)(an owned async loop to spawn) ; theaccept_fnborrow-shape fought the spawn model, while the loop returning on listener failure is the assembly-abort contract. - Test plumbing note (tests): the listen-over-real-socket test
drives the client socket via std
try_cloneBEFOREfrom_std(O_NONBLOCK is shared across dup, so both ends go async); the session uses borrowed halves (the producer's wrapper-managed pump already pumps channel <-> accepted handle;pump_againstthere would loop the same socket back into itself).
Summary
Agent fills this on completion.
Implemented the local feature (src/local/mod.rs, feature =
["tokio/net"]): dial_tcp (5s timeout), connect_udp (ephemeral
bind + connect + the FRAMED UdpHalf adapter (truncation fails
loud), dial_unix (OQ-TN-14 in v1), bind_tcp/TcpListenerHalves:: accept_loop (the listen shape's assembly half), and local_dial()
(the one DialFn covering all three substrates). No socket type
appears outside src/local/ (convention 16: the module is never
imported from producer/consumer/shared; TargetHandle re-used from
producer.rs). Stdio bridging is NOT here (alktty's pipe mode owns
process stdio).
Tests: tests/local_halves.rs (7, #![cfg(feature = "local")]) with
the TCP dial round-trip through the real establisher path, UDP framed
round-trip, the 1400-byte MTU datagram, unix dial round-trip, the
OQ-TN-13 truncation fail-loud probe (60k datagram vs 16KiB read →
InvalidData), dial-refusal → dial_failed, and the listen producer
over a real TCP listener end-to-end.
Verified: cargo test (default) and cargo test --all-features
green (44 + 7), clippy -D warnings clean (default + all-features +
wasm32), fmt clean, wasm32 check passes (default crate stays
wasm-clean; local is non-wasm by design).