docs: OQ-SK-04 restructured as the conditional decision tree it is

The root question is 'does wasm make sense for alksocks?', not 'how
do we get wasm': Case 1 (wasm wanted) -> fork-first, PR as courtesy,
carry regardless; Case 2 (wasm unwanted) -> plain dependency on the
router.rs native path, no fork (the alkhttp precedent, strictly
better when its premise holds). Decision inputs written: (a) is
there a planned sandboxed (wasm) consumer of the SOCKS5 service,
(b) is the fork genuinely minimal (POC #4 checks). Fork-first is a
conditional preference, not a general one — do not carry a fork for
an invariant's sake. AGENTS.md conventions 4/17 aligned.
This commit is contained in:
2026-09-12 11:00:56 +00:00
parent 766e489f99
commit a628715106
2 changed files with 86 additions and 74 deletions
+15 -12
View File
@@ -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
+72 -63
View File
@@ -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).
**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.
**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
- **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). 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.)
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.
**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:
- **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,