diff --git a/AGENTS.md b/AGENTS.md index 859b93d..6d6d9d9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,11 +90,13 @@ implementation agents. (`rt`, `sync`, `io-util`, `macros`, `time`) — **do NOT use `features = ["full"]`** — and keep socket/platform I/O feature-gated. Note: `fast-socks5` itself uses `tokio::net` and `socket2` unconditionally; - the resolved posture (OQ-SK-04) is fork-first: vendor the needed - subset and feature-gate native-net pieces (the alkhttp crate shows a - native-only crate is possible, but wasm-clean remains the preferred - posture here). Run the wasm check whenever a non-backend module - changes. + the posture is conditional (OQ-SK-04): if wasm matters for this + crate, the path is fork-first (vendor the `T`-generic subset, gate + native-net pieces — see OQ-SK-04 Case 1); if wasm does not matter, + ride the native `router.rs` path with a plain dependency (Case 2 — + the alkhttp precedent). The root question — does wasm make sense + here — is the OQ to settle; do not carry a fork for an invariant's + sake. Run the wasm check whenever a non-backend module changes. 5. **Wire format is stable** — the SOCKS5 protocol itself is RFC 1928 (fixed); this crate's wire surface is the ALPN + the channel open-op @@ -227,13 +229,14 @@ implementation agents. One ALPN per protocol. The ALPN string is wire-stable once published — decide via ADR before the first consumer. -17. **Upstream is ours — fork-first, PR as courtesy.** We own - `fast-socks5` (`/workspace/fast-socks5`, v1.0.0, MIT) and alkcall. - For changes this crate needs upstream, the preferred posture is: - make the change as a minimal fork and use it — offer it upstream - as a PR and merge upstream if they want it, but never wait on - approval (see `docs/research/phase-0.md` OQ-SK-04's resolved - posture). For alkcall (the actively co-developed substrate) the +17. **Upstream is ours — fork-first where it pays, asks for alkcall.** + We own `fast-socks5` (`/workspace/fast-socks5`, v1.0.0, MIT) and + alkcall. For fast-socks5, the posture is conditional (OQ-SK-04): + if wasm matters for this crate (Case 1), make the change as a + minimal fork and use it — offer it upstream as a PR and merge + upstream if they want it, but never wait on approval; if wasm does + not matter (Case 2), ride the native path with a plain dependency, + no fork. For alkcall (the actively co-developed substrate) the alk* precedent still applies: file asks early and land them there rather than working around them locally — the alktunnels precedent (the E-01/E-02 sweep, filed from its Phase 0 and landed in alkcall diff --git a/docs/research/phase-0.md b/docs/research/phase-0.md index c08d3b3..7795121 100644 --- a/docs/research/phase-0.md +++ b/docs/research/phase-0.md @@ -445,68 +445,76 @@ published). unknown-unknown` has no `tokio::net`. The wrapper's own protocol layer can stay wasm-clean (the typestate API is generic over `T`), but depending on the crate at all may break `cargo check --target -wasm32-unknown-unknown` at the dependency-graph level. Options: +wasm32-unknown-unknown` at the dependency-graph level. -- **Option A: upstream feature-gating ask** — add a feature to - fast-socks5 that gates `tokio::net`/`socket2` behind a default-on - `net` feature (like alktty's `local`), leaving the codecs, typestate - machinery, and client `use_stream` path wasm-clean. We own upstream - (AGENTS.md convention 17); the alk* precedent is to make asks early. -- **Option B: protocol reimplementation** — the wrapper reimplements - the SOCKS5 state machine over `tokio::io` generics (RFC 1928 is - small); fast-socks5 remains the native-path implementation. Duplicates - protocol logic; against the "wrap, don't fork" vision unless A fails. -- **Option C: wasm drops off the invariant for this crate** — accept a - native-only default. Against the alktty/alktunnels precedent; - document as an ADR-worthy exception if forced. +**The decision tree (2026-09-12 clarification — two main forks, and the +preference between them is conditional):** -Action: verify the breakage empirically (a minimal crate depending on -fast-socks5, `cargo check --target wasm32-unknown-unknown`), then take -Option A upstream if confirmed. This OQ gates the wasm verification -command in AGENTS.md (the expected-failure note is already written -there). +- **Case 1: wasm matters for this crate → fork-first.** Make the + minimal fork and use it — offer it upstream as a PR (merge if they + want it; we carry the fork regardless; never wait on approval). + The gating change is genuinely small (feature-gate + `tokio::net`/`socket2` behind a default-on `net` feature, per the + alktty `local` pattern); the long-term cost of maintaining a + minimal fork of a well-written lib trends toward zero. Concretely + "fork" likely means **vendoring the relevant subset into this + crate** (the codecs, typestate machinery, client `use_stream` path — + the parts that are already `T`-generic) and gating the native-net + pieces behind the crate's own `local` feature, rather than + publishing a divergent crate. fast-socks5 remains the reference + checkout for differential testing of RFC edge cases (reply-code + mapping, domain addressing, fragmentation) and the PR source; if + upstream accepts the PR, the fork shrinks to a plain dependency. +- **Case 2: wasm doesn't matter for this crate → plain dependency, no + fork.** Ride the `router.rs` native path exactly as-is (the + alkhttp precedent — a crate in this suite can ship native-only). + This is strictly better *if its premise holds*: no fork burden, no + vendoring, upstream stays upstream. The alkhttp contrast is + instructive — alkhttp's substance (axum/hyper/reqwest) is + socket-native, so nobody misses wasm there. -**Fork vs reimplement vs wrap — resolved as fork-first (2026-09-12 -discussion, supersedes the earlier upstream-ask-first weighting).** The -wrap surface was re-checked and **no structural fork is needed** for the -channels path: CONNECT rides the interception points as-is — -`read_command` → own dial → `reply_success` → `transfer` (the -`examples/router.rs` shape), with the producer's `BiStream` as `T` on -both sides of `transfer`. UDP is ours either way: `run_udp_proxy_custom` -takes a custom `transfer` closure for the relay half, `reply_success` -accepts any `SocketAddr` (the sentinel reply), and -`new_udp_header`/`parse_udp_request` are public, pure, and reusable. -The one hardwired piece — `run_udp_proxy`'s peer relay socket -(`udp_bind_random_port`, socket2-based, called inside the default -handler before `run_udp_proxy_custom` even runs) — is in the *default* -handler, not the protocol: the wrapper's custom closure never needs it. +**The root question is therefore: does wasm make sense for alksocks?** +Not "how do we get wasm" — that is Case 1's solved problem. Analysis +for the decision: -**Decision: fork-first, "fork" = inline in this crate (vendor-and-vet).** -Do the feature gating as a minimal, minimal-diff fork and use it — -offering it upstream as a PR (merge if they want it; we carry the fork -regardless). Rationale: depending on someone else's approval cadence is -the worst part of the upstream-ask path; the long-term cost of -maintaining a minimal fork of a well-written lib trends toward zero -(modern tooling/AI assistance makes small-diff rebase-and-review cheap); -and the changes needed here are genuinely small (feature-gate -`tokio::net`/`socket2` behind a default-on `net` feature, per the -alktty `local` pattern). Concretely this likely means **vendoring the -relevant subset into this crate** (the codecs, typestate machinery, -client `use_stream` path — the parts that are already `T`-generic) and -gating the native-net pieces behind the crate's own `local` feature, -rather than publishing a divergent crate. fast-socks5 remains the -reference checkout for differential testing of RFC edge cases -(reply-code mapping, domain addressing, fragmentation) and the PR -source. If upstream accepts the feature-gating PR, the fork shrinks to -a plain dependency; if not, we carry it — either way this crate's -timeline is not hostage to approval. What is *not* off the table: -dropping wasm outright is still available (Option C) as the -alkhttp precedent shows a crate can ship native-only — but it is not -preferred; the fork makes the wasm-clean default achievable without -anyone's approval. (The user also flagged the alternative of simply -not worrying about wasm for this crate and riding the `router.rs` -native path; that remains viable if the fork's gating turns out more -invasive than expected — an escape hatch, not the plan.) +- **What wasm would serve:** the protocol layer (components 1+2 of the + three-component split, §Vision) in a sandboxed adapter — the + alktty/alktunnels posture that a wasm-compiled protocol crate is the + protocol layer for downstream TS/Python adapters. The natural wasm + shape is a **consumer**: speak RFC 1928 client-side over a channels + `BiStream` (pure byte framing; the producer dials targets, so no + local sockets are needed on the client side). A wasm **producer** is + odd standalone (no kernel egress) but composes — its dialer can open + alktunnels channels further downstream, which is the hub story. +- **What wasm would not serve:** component 3 (the quinn/noq client + wrapper) is native-only in practice — it wraps kernel UDP sockets for + UDP ASSOCIATE; no wasm story is expected there regardless. +- **The alkhttp contrast, honestly weighed:** for alkhttp, + "an HTTP client accessed over a channel" was judged weird, and the + crate ships native-only. alksocks differs structurally: its protocol + substance *is* byte-framing over a generic `T` — the part that costs + nothing to keep wasm-clean. But "costs nothing under the fork" is + only an argument once Case 1 is chosen; it is not itself the use + case. The use case is a planned sandboxed SOCKS consumer. + +**Decision inputs (what actually settles Case 1 vs Case 2):** + +1. **Is there a planned sandboxed (wasm) consumer of the SOCKS5 + service?** If the downstream TS/Python adapter story includes a + channels-connected sandbox that wants proxied egress, wasm matters + → Case 1. If not → Case 2. +2. **Is the fork genuinely minimal?** POC #4 (OQ-SK-07) verifies + empirically how invasive the gating is. If the gating turns out + structural (the `T`-generic surface can't be separated from + `tokio::net` without a rewrite), the fork cost rises and Case 2 + gains weight — reimplementing the protocol to serve a wasm story + nobody has yet defeats the purpose. + +Both inputs point the same way when aligned: fork-first in Case 1 is +the preferred branch *because* wasm is wanted there — not as a general +preference for forks over upstream asks. If the adapter story never +materializes, Case 2 (plain dep, router path) is the obvious choice, +and no one should carry a fork for an invariant's sake. ### OQ-SK-05: Target policy and egress scoping @@ -670,10 +678,11 @@ Candidate reading for the research specialist (to be expanded): - [ ] OQ-SK-03 (UDP ASSOCIATE) — the shape space written (including the phase-model-vs-alktty-demux analysis); resolve via research + POC #2, ADR before the first consumer -- [ ] OQ-SK-04 (fast-socks5 wasm) — verify empirically (POC #4); - resolved posture: fork-first (vendor-and-vet, PR as courtesy) — - carry the minimal fork regardless of upstream's decision; - wasm-drop (alkhttp precedent) is the escape hatch, not the plan +- [ ] OQ-SK-04 (fast-socks5 wasm) — the decision tree is written + (Case 1: wasm wanted → fork-first; Case 2: wasm unwanted → + plain dep, router path); the root question ("does wasm make + sense for alksocks?") settles on the adapter-story input + + POC #4's minimality check - [ ] OQ-SK-05 (target policy) — folded into the OQ-SK-01 decision; scope-gate convention pinned in Phase 1 - [ ] OQ-SK-06 (noq client) — research pass (trait-shape comparison,