- 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
7.8 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | |||
|---|---|---|---|---|---|---|---|---|---|---|
| implementation/core-producer-connect | Core: producer half, establisher + dispatch + registration + CONNECT | completed |
|
moderate | medium | phase | implementation |
Description
Implement the producer half of src/producer.rs per producer.md:
the session-gate establisher, the command-dispatch pump handler, the
registration surface, and the CONNECT data plane (POC #1's validated
scope). This is the crate's core task: the open op all three
commands ride.
Deliverables (port from /workspace/alksocks-connect-poc/src/ producer.rs + /workspace/alksocks-udp-poc/src/producer.rs, adapted
to the crate's seam types and alkcall 0.8.0):
- The session-gate establisher (
socks5_session_establisher): validates/records the opener identity (the CF-005/CF-006 seam in ADR-005) and applies session-level policy. RunsEstablishment::default()-shaped (no plan payload; the dial happens post-establishment, so there is nothing to thread establisher→handler; POC #1's wire note). Optionalidentity_witnessprobe seam (the alktunnels posture) for tests. - The pump handler (
make_socks5_pump_handler(dial, egress, accept)):accept_bi()→ drive the forked typestate over theBiStream(accept_no_auth → read_command) → dispatch:- CONNECT → invoke
DialFn(target)at the command-read interception point; success:reply_successthenpump_bidiawaited inline (R-02: the returnedJoinHandletracks the data-plane lifetime; early return = teardown-at-birth). Refusal:reply_errorwith the mapped RFC code (dial-failure →ConnectionRefused; scope-class →ConnectionNotAllowed), no phantom data plane. - UDP ASSOCIATE / TCP BIND → this task lands the dispatch
arms as RFC-faithful refusals (
CommandNotSupported); the associate/bind producer tasks replace them. (The dispatch skeleton exists from day one so the regression guard (one registration, all three commands) is testable early.) - The handler is substrate-agnostic by construction: it sees boxed halves, never a socket type.
- CONNECT → invoke
register_socks5_openable: spec + establisher + pump viaregister_openable_with_establisher(timeout: None= the 10s default), scope-gatedsocks5:open(the registry runs the ACL before the establisher). Missing callbacks = those commands refuse RFC-faithfully (CommandNotSupported-class), never a panic (ADR-008).serve_socks5_over<T>: the substrate-free protocol entry point (the POC shape): generic overT: AsyncRead + AsyncWrite + Unpin, driving the typestate and dispatching. This is what makes the producer testable without channels (the differential-suite pattern) and reusable by the batch-2 local backend.- Tests (via the harness + the raw RFC client helper): CONNECT
echo round-trip through
pump_bidi; dial refusal → 0x05 in the reply, no phantom data plane; FORBIDDEN for a scope-less identity (fails closed, no phantom channel); per-call opener identity witnessed (CF-006); ASSOCIATE/BIND refuseCommandNotSupported(the pre-vertical posture); half-close teardown (client EOF ends the session cleanly).
Acceptance Criteria
- Establisher: session-gate shape (
Establishment::default(), identity recorded; witness seam optional) - Pump handler: typestate-driven dispatch;
pump_bidiinline for CONNECT; no substrate types leak register_socks5_openablewires spec + establisher + pump + injected seams; missing-callback commands refuse RFC-faithfullyserve_socks5_over<T>is substrate-free and directly testable- Integration tests: round-trip, refusal mapping (0x05), FORBIDDEN fails closed, identity witness, refusal arms for ASSOCIATE/BIND, half-close teardown
cargo testgreen; clippy/fmt clean; both wasm checks clean
References
- docs/architecture/producer.md (the normative shapes; the wiring requirement)
- docs/architecture/decisions/001/004/005/006 (op, seams, auth, gates)
- POC sources:
/workspace/alksocks-connect-poc/src/producer.rs(CONNECT + establisher),/workspace/alksocks-udp-poc/src/producer.rs(the dispatch skeleton + registration) - alkcall ADR-049 (establishment; R-02 inline-await contract),
/workspace/@alkdev/alktunnels/src/producer.rs(the 0.8.0 registration template)
Notes
Implementation notes (2026-09-23):
Socks5ProducerBuilderwas added beyond the deliverable list: it is the spec'd assembly shape (producer.md §Wiring, theSocks5Producer::builder()sketch);register_socks5_openablestays as the explicit per-registration form. Both ride the sameregister_openable_with_establishercall; the builder'sregister/serve_socks5_overdelegate to them verbatim.- The
establisheracceptsOption<IdentityWitness>(the alktunnels*_with_witnessposture) rather than a required witness, because the witness is a test seam, not a protocol surface;register_socks5_openablethreads it through.- The ASSOCIATE arm carries the
EgressFactoryparameter (and the BIND arm theBindAcceptFactory) in the registration signature from day one, so the associate/bind tasks replace the refusal arms without touching the registration shape (no churn for the callers). Until those tasks land, the seams are accepted and their commands refuseCommandNotSupported(the pre-vertical posture, ADR-008's missing-callback class), tested.- CONNECT's refusal error shape: dial-seam refusal returns
SocksServerError::Bug("dial refused in-stream")afterreply_error(the refusal was already RFC-reported; the error return is the session-end signal, not a second wire message). A missing dial callback refusesCommandNotSupportedviaSocksServerError::UnknownCommand(cmd).
Summary
Completed 2026-09-23 (commit on
main).src/producer.rsfilled: the session-gate establisher ([socks5_session_establisher]; CF-005/CF-006 witness seam,Establishment::default()per POC #1's no-plan wire note), the command-dispatch pump handler ([make_socks5_pump_handler]:accept_bi→ typestate (accept_no_auth → read_command) → dispatch), the CONNECT data plane (DialFnat the command-read interception point →reply_success(sentinel)→pump_bidiawaited inline, R-02),register_socks5_openable(spec + establisher + pump viaregister_openable_with_establisher,timeout: None= the 10s default,socks5:open-gated),serve_socks5_over<T>(the substrate-free entry), and the builder assembly shape.Dispatch arms: CONNECT serves; ASSOCIATE/BIND refuse
CommandNotSupported(the pre-vertical posture; the skeleton exists so the regression guard is testable early; the associate/ bind tasks replace the arms).Tests: 11 in-module (substrate-free, duplex-driven: round-trip + half-close teardown, refusal mapping per variant 0x05/0x03/0x04/ 0x02/0x01, missing-dial refusal, ASSOCIATE/BIND arms, dispatch guard, establisher witness + no-plan, builder shape, sentinel shape) + 5 harness integration (end-to-end CONNECT echo through channels with the identity witness, dial refusal 0x05 with no phantom data plane, scope-class refusal 0x02, ASSOCIATE/BIND refusals with CONNECT still serving (the one-registration regression guard, repeated CONNECTs on one registration). The harness's
RegisterModegained the real-producer mode; the minimal CONNECT stand-in is retained only as a harness-internal helper.Verification:
cargo test87+19+37 green; clippy--all-targetsclean; fmt clean; both wasm checks clean;cargo docclean;--all-featuresgreen.