# AGENTS.md Operating instructions for opencode agents working in this repo. opencode auto-loads this file as instructions, overriding the built-in defaults for this project. Custom agents in `.opencode/agents/` inherit these rules unless their own prompts say otherwise. ## Git Workflow **Commit and push when reasonable.** When a change is complete and verified (build + lint + tests pass), commit and push to `origin/main` without asking. This overrides the built-in default of "only commit when explicitly asked." The workflow: 1. Make the change 2. Verify: `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo fmt --check`, `cargo doc --no-deps` if docs changed 3. Inspect `git status` and `git diff` before staging; stage only the intended files, never secrets 4. Write a concise commit message matching the repo style (see `git log --oneline -10`). For multi-point changes, use a summary line plus a body with bullet points and a verification block. 5. `git push origin main` 6. Report the commit hash and the verification summary Exceptions (**do not** commit or push without asking): - The change is exploratory / speculative (you're not sure the user wants it kept) - The user is actively reviewing the diff and may ask for changes - The change touches the wire format or a trait shape that backends or consumers implement (one-way doors; see "Wire formats are stable" and "Backend trait shapes are one-way doors" below; once consumers exist, those signatures are wire-stable contracts) - You'd be force-pushing, amending a published commit, creating an empty commit, or skipping hooks Never commit secrets, keys, or credentials. If a commit fails or hooks reject it, fix the issue and create a new commit; do not amend the failed one. Git identity is preconfigured (`glm-5.3-flash `). Do not change `git config`, skip hooks, or use `git commit -i`. ## Project Conventions (Rust / SOCKS5 protocol crate) This is the SOCKS5 protocol crate, a wrapper around `fast-socks5` that offers the SSH `-D` (dynamic / SOCKS5 proxy) capability alongside alktunnels' `-R` and `-L`, composed with alkcall channels ("ALPN as a service"). It sits in the alk* family: alkcall (call + channels, the substrate), alktty (terminal sessions), alktunnels (arbitrary `-L`/`-R`/ tunnels). The distinguishing posture of this crate: **the SOCKS5 server never binds a port unless explicitly configured to**: the protocol is served inside alkcall channels data channels (a produced, ACL-scoped resource), not on a kernel socket. A local bind is one optional assembly-layer shape, not the protocol's home. The conventions below apply to all work in `src/` and `tests/`. They mirror `.opencode/agents/implementation-specialist.md` §Project Conventions and are repeated here so they apply to every session, not just spawned implementation agents. 1. **No comments in code** unless the user explicitly asks. This is a project-wide convention. Doc comments (`///`, `//!`) are fine and expected on public API. Inline `//` comments only when the user asks or when a non-obvious safety/correctness constraint would otherwise be missed (e.g., "a two-pump tunnel must shut down the opposite sink on pump completion, since `try_join!` alone deadlocks; see alkcall ADR-050's `pump_bidi` / alknet ADR-078"). 2. **Error handling**: `thiserror` for library error types; map SOCKS5 reply codes (`fast_socks5::ReplyError`) faithfully per RFC 1928. No panics in library code. No `unwrap()` or `expect()` outside tests. If you reach for `unwrap`, the error path wasn't specified, so stop and decide what should actually happen. For poisoned `RwLock`/`Mutex`, use `unwrap_or_else(|e| e.into_inner())` so a panic in one operation does not cascade to other operations. 3. **`tokio` is the async runtime**: all I/O is async. `fast-socks5` is tokio-native; the wrapper's protocol layer, pumps, and client session types are all async. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for lifecycle correlation. Any non-wasm backend module (local TCP listener for a "real" `socks5://` endpoint) lives behind a feature flag like alktty/alktunnels' `local`. 4. **WASM target is load-bearing (by default)**, meaning the default crate (protocol-only) should compile to `wasm32-unknown-unknown`, following the alktty/alktunnels precedent. Use the wasm-clean tokio subset (`rt`, `sync`, `io-util`, `macros`, `time`); **do NOT use `features = ["full"]`**; and keep socket/platform I/O feature-gated. Note: `fast-socks5` uses `tokio::net` and `socket2` unconditionally as published; the posture is the **targeted fork** (OQ-SK-04, resolved 2026-09-20, convention 17; extraction pinned 2026-09-21, ADR-011 §Amendment; fork 2026-09-22, ADR-013): the `T`-generic subset was extracted in-tree (the kernel-net handlers are not copied), so the RFC 1928 core (`src/rfc1928/`) is wasm-clean by construction with no `net`-shaped feature; socket I/O rides `local` alone. Run the wasm check whenever a non-backend module changes; it is a hard gate (no dependency-graph failure is expected, since the core contains no `tokio::net`). 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 params that carry it. Those params are a one-way door once consumers exist. Follow the alktunnels precedent: params are a self-contained JSON object identifying the produced resource (alkcall ADR-039, whose `params` is ALPN-specific, interpreted by the open handler). Any params-format ADR must be written before the first consumer exists; after that, changes are additive-only. 6. **Producer/consumer, not server/client**, because both sides of a channels connection can initiate. A producer exposes the SOCKS5 service (registers openable channels via `ChannelCore::register_openable`); a consumer opens SOCKS5 channels and speaks RFC 1928 inside them. Both sides can be both simultaneously; connection direction (who opened it) is independent of service direction (who dials targets, who serves). Avoid "server" and "client" framing in docs and API names; use "producer" and "consumer," or "accept side" / "connect side" for the connection-establishment half specifically. (SOCKS5's own client/server roles are RFC vocabulary and keep their RFC names inside the protocol layer.) See alkcall ADR-022, ADR-037. 7. **Substrate-agnostic by construction**: the protocol layer must not know whether the far side of a SOCKS5 connection is a kernel TCP socket, a channels `BiStream`, a unix socket, or an in-process pipe. `fast-socks5`'s explicit server API is generic over `T: AsyncRead + AsyncWrite + Unpin` (`Socks5ServerProtocol`, `transfer(inbound, outbound)`); the wrapper must preserve that genericity: the channels adapter feeds the SOCKS5 state machine a `BiStream` (which is `AsyncRead + AsyncWrite`), the local backend feeds it a `TcpStream`. Substrate-specific types are confined to feature-gated backend modules, injected at the assembly layer, the same inversion-point pattern as alktty's `TtyBackend` and alktunnels' pump halves. 8. **Two-pump shutdown-on-completion is a contract**: the SOCKS5 CONNECT data plane is the canonical two-pump shape (one pump per direction). Each pump MUST shut down the opposite sink when it completes; `tokio::try_join!` alone deadlocks. The helper is pinned upstream as alkcall `channels::pump_bidi` (ADR-050, review 007 R-03), so use it; do not hand-roll the two-pump shape. The channels layer drops all per-channel senders on transport EOF, so rely on that for teardown, and emit EOF sentinels (zero-length chunks) on clean sink shutdown. Await `pump_bidi` inline inside the `OpenHandler`'s task, because the returned `JoinHandle` must track the data-plane lifetime (R-02; early return = teardown-at-birth). 9. **No forced local binding**: the defining requirement of this crate. The SOCKS5 server must run without binding any port: the RFC 1928 conversation is carried inside a channels data channel, and target dials happen on the producing side (or hop further through alktunnels/alksocks). Local binds exist only as explicit, optional assembly-layer shapes on either side: the producer may serve from a real listener (a feature-gated local backend), and the consumer may expose the resource on a local port (the ssh `-D` front door that vanilla clients (curl, browsers, tun2proxy) point at). The protocol itself never binds. UDP ASSOCIATE is the hard case: RFC 1928 replies with a UDP relay address and the client sends datagrams to it, which seems to demand a bind; how the relay address is virtualized (or the associate shape adapted) is a Phase 0 question (OQ-SK-03). Binding decisions belong to the caller (assembly layer), never the protocol crate. 10. **Backpressure and limits are inherited, not redefined**: bounded per-channel buffers, the 256-channel default caps (per-connection memory bound, ADR-040; per-identity DoS defense, ADR-041, i.e. policy knobs, configurable without a wire change; the `u32` wire space itself is ~4 billion), monotonic channel IDs, and the zero-length-sentinel EOF convention are alkcall channels invariants (see alkcall ADR-040/041/034). The crate consumes them via `ChannelCore`/`ChannelClient`; do not build a second demux/mux or re-derive limits. SOCKS5 CONNECT is a single stream per open (no sub-demux inside the channel); one channel per SOCKS5 session, no protocol-level flow key. 11. **Vendored core types come from alkcall**: `Connection`, `ProtocolHandler`, `BiStream`, `BidiStreamSource`, `AuthContext`, `Identity`, `IdentityProvider`, `AccessControl`, `OwnershipProvider`, `HandlerError`, `StreamError` come from `alkcall::core`. Do not vendor copies into this crate. alkcall is v0.10.x, where breaking changes are expected at this major-zero stage; this is an early consumer, so we find and fix issues upstream rather than working around them. Pin `alkcall = "0.10.1"` and bump deliberately. The establishment surface (alkcall ADR-049 + amendments) is load-bearing for this crate: SOCKS5 session opens use `register_openable_with_establisher` so a refused/auth-failed session is a typed `channel:open_failed` call error, never a phantom channel; the establisher returns the negotiated handle via `Establishment::new(plan)` (typed-opaque `ChannelPlan`, whose payloads must be `Send + Sync`; 0.8.0 adds the optional reply projection, `with_reply_field(s)` from ADR-049 amendment 3). The 0.7.0 identity surface (ledger CF-005/CF-006) is load-bearing for auth mapping: the per-call opener identity arrives on the open-op hooks; map it to SOCKS5 credentials/ACL there, not via any in-band SOCKS auth unless clients need RFC-facing username/password. 12. **Access control**: the producer's openable channels get authorization for free via `ChannelCore::register_openable`, which wires `AccessControl` into the operation spec (the registry runs the ACL before the open handler). Scope-gate SOCKS5 opens (e.g. `SOCKS5_OPEN_SCOPE`, shape following alktty's `TTY_OPEN_SCOPE`) and optionally consult `OwnershipProvider` for resource ownership. SOCKS5 is arbitrary-egress by nature; treat the open gate and the target policy as the security boundary; per-target allowlists (if any) are a producer-side policy question for Phase 1 (OQ-SK-05). See alknet ADR-024 (registry layering, alkcall ADR-019), alknet ADR-050 (ownership, alkcall ADR-011), and alktunnels' resolved posture (target selection lives in the SOCKS5 protocol at the producing side; whatever ACL governs the socks5 resource governs everything reachable through it). 13. **Feature flags**: substrate backends are feature-gated if the need arises. The base crate should compile lean (no socket/platform deps unless the feature is on). Verify both `cargo test` (default) and `cargo test --all-features` pass if features are added. 14. **Naming** follows the Rust standard: `snake_case` for functions/variables/ modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. 15. **Module structure**: one module per file under `src/`, re-exported from `src/lib.rs`. Public API surface is `lib.rs` re-exports. The expected shape (pending Phase 1 pinning): the SOCKS5 protocol wrapper (server state machine + client session), the channels adapter (the producer half, the open handler for this crate's ALPN), the consumer half (a typed client that opens SOCKS5 channels and speaks RFC 1928 through them), and feature-gated backend modules (the optional local-bind listener). Backend modules are feature-gated and never imported from the protocol/adapter/client modules. 16. **ALPN naming**: this crate owns a `alk/`-prefixed ALPN (provisional `alk/socks5`; final naming per alkcall ADR-004/ADR-006 convention). One ALPN per protocol. The ALPN string is wire-stable once published; decide via ADR before the first consumer. 17. **Upstream: alkcall is ours to shape; fast-socks5 is forked in-tree.** alkcall is ours (the actively co-developed substrate): 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 0.5.0 within a day; the graduation sweep landed in alkcall 0.8.0 / alktunnels 0.2.0). `fast-socks5` is NOT ours and never will be; it is third-party upstream (`/workspace/fast-socks5` is a read-only reference checkout, v1.0.0, MIT), but the `T`-generic subset we depend on is now **ours, as a targeted fork** (OQ-SK-04 resolved 2026-09-20 as the inline-vendor extraction; extraction pinned 2026-09-21, ADR-011 §Amendment; fork posture pinned 2026-09-22, ADR-013): the subset lives in `src/rfc1928/` as fully owned code, and project conventions apply to it unconditionally (no vendor carve-outs, no lint-allow posture, no verbatim preservation). The extraction cut (ADR-011's table) is the hand-port map: if a future need surfaces (e.g. the kernel-net region for a standalone vanilla-server example), port the piece from the reference checkout, adapt it to the conventions, and let the differential suite guard it. A future upstream fix that matters is a hand-port too; nothing is absorbed silently, and nothing waits on upstream. Keep `/workspace/fast-socks5` for differential testing of RFC edge cases. Provenance is permanent: the module docs and the bundled license notice (`src/rfc1928/LICENSE`) always name the origin (v1.0.0, the extraction range, the fork point): extend, never replace, the notice. 18. **No em dashes in prose.** Do not use the em dash (U+2014) in source doc comments, markdown docs (including all of `docs/` and `tasks/`), agent prompts under `.opencode/agents/`, or commit messages. Restructure instead: split the sentence, use parentheses, commas, a colon, or a semicolon, whichever reads naturally, and vary the choice rather than converting every instance the same way. A spaced hyphen (` - `) is not an acceptable substitute; it reads as a different but equally recognizable writing tell. Inside fenced code blocks the rule applies to prose-y content only: line comments, ASCII-diagram label separators, and template placeholders are rewritten too, while wire data, JSON/BAST examples, and tool transcripts survive verbatim. The same rule covers en dashes used as clause separators, though en dashes inside numeric ranges keep their normal meaning. Hyphens in compound words (`wasm-clean`, `fast-socks5`, `alk/socks5`) are untouched. The one exemption is `docs/reviews/`: reviews are archived historical records, so their prose is preserved verbatim and is excluded from the em-dash greps. ## Verification Commands Run these before committing. All must pass. ```bash cargo test # full suite cargo clippy --all-targets -- -D warnings cargo fmt --check cargo doc --no-deps # if docs changed cargo test --all-features # if features are added cargo test --manifest-path fuzz/shared/Cargo.toml # corpus replay (the fuzz gate) cargo check --target wasm32-unknown-unknown # default crate stays wasm-clean cargo clippy --target wasm32-unknown-unknown -- -D warnings cargo publish --dry-run --allow-dirty # before a release ``` ### Release steps (after the version bump + verification) 1. Commit the release (bump + CHANGELOG/README/AGENTS pins). 2. Tag it: `git tag -a vX.Y.Z -m "release: vX.Y.Z ()"` **annotated**, always (v0.1.0/v0.2.0 are lightweight; annotated since the v0.3.0 release, 2026-10-19). 3. `git push origin main && git push origin vX.Y.Z`. 4. `cargo publish` (no `--allow-dirty` once the tree is clean). 5. Confirm with `cargo search` / `cargo info`. The corpus replay is the fuzz gate (the alkcall §7.9 posture): it replays every committed seed through the same invariant functions the fuzz targets run, on stable, without nightly. Campaigns (nightly, cargo-fuzz) run manually via `fuzz/run-detached.sh` when `src/wire.rs`, `src/rfc1928/`, or `src/params.rs` change, never as a foreground child of an agent session (docs/research/fuzzing.md §5). The wasm check is the structural guard for the "default crate is wasm-clean" invariant (same as alktty/alktunnels); run it whenever a non-backend module changes. It is a hard gate: the RFC 1928 core is an extraction with no kernel-net code (ADR-011 §Amendment), so no dependency-graph failure is expected. ## Architecture Context - `docs/research/phase-0.md` holds the Phase 0 (Exploration) document and the current state of this repo: vision, prior art, open questions (OQ-SK-01..NN), and POC candidates. Read it before non-trivial work. The SDD process lives in `docs/sdd_process.md` (Phase 0 in progress; `docs/architecture/` does not exist yet). - The prior art for this crate: - **alktunnels** (`/workspace/@alkdev/alktunnels`, v0.1, the closest sibling): the producer/consumer tunnel crate on alkcall channels. Its Phase 0 findings (`docs/research/phase-0-findings.md`) settled the hub-owns-the-connection model, the two-pump shape, the establishment story (alkcall ADR-049), and the `-D`-composes-at-the- assembly-layer conclusion; this crate is that conclusion's realization. Its Phase 1 ADRs (params shape, ALPN strategy, access-control posture) are the template for this crate's Phase 1. - **alktty** (`/workspace/@alkdev/alktty`, v0.4): the first producer/consumer protocol crate on alkcall channels; the backend inversion point, wasm-clean default crate, and feature-gated local backend precedents. - **alkcall** (`/workspace/@alkdev/alkcall`, v0.9.x, crates.io). The substrate: call protocol + channels multiplexing. This crate consumes `alkcall::core` types and the channels `ChannelCore`/`ChannelClient`/`register_openable_with_establisher` surface, same as alktty/alktunnels. 0.8.0 adds the establisher reply projection (`with_reply_field`, ADR-049 amendment 3), flavor-form open-op ids in discovery (ADR-047 amendment 3), and the in-tree `ChannelRelay`/`HubLegTemplate` (ADR-051), the hub re-produce machinery the SOCKS5 hub story composes on. - **alktunnels** (v0.2.0, ADRs 007/008): the direct op (`channels/tunnel/direct`, dynamic-target egress; OQ-SK-01 Option A's vehicle), the bind-first establisher + forwarded op (the composed BIND substrate; OQ-SK-08 variant 2), and `TunnelListener`. - **fast-socks5** (`/workspace/fast-socks5`, v1.0.0, MIT, third-party upstream, NOT ours and never will be). The SOCKS5 implementation the RFC 1928 core forked: explicit typestate server API (`Socks5ServerProtocol`), generic over `T: AsyncRead + AsyncWrite + Unpin`; `run_tcp_proxy` / `run_udp_proxy_custom` / `transfer` are the interception points; client side has `Socks5Stream` (generic over the backing socket) and `Socks5Datagram` (UDP associate). The posture is the **targeted fork** (OQ-SK-04 resolved 2026-09-20 as the inline-vendor extraction; extraction pinned 2026-09-21, ADR-011 §Amendment; fork 2026-09-22, ADR-013, convention 17): the `T`-generic subset lives in `src/rfc1928/` as owned code, no kernel-net code and no `net` feature; BIND and the `new_reply` publication are the two carried changes; the reference checkout stays for differential testing of RFC edge cases. - **alknet's SOCKS5 client** at `/workspace/@alkdev/alknet/crates/alknet-client/src/socks5.rs` (ADR-090), the client-side example: `Socks5UdpSocket` implements `quinn::AsyncUdpSocket` so QUIC rides SOCKS5 UDP ASSOCIATE. Validated by the quinn-proxy POC (`/workspace/@alkdev/alknet/docs/research/quinn-quic-proxy/ findings.md`). The open question for this crate's client story: the same trait impl against **noq** (iroh's quinn fork, `/workspace/noq`, which has `noq::AsyncUdpSocket`, `Endpoint:: new_with_abstract_socket` exist but with a different surface shape; see OQ-SK-06). - Key upstream ADRs that inform this crate's design (alknet numbers; alkcall's ports live in `/workspace/@alkdev/alkcall/docs/architecture/ decisions/`): - alknet ADR-093 / alkcall ADR-035: channels pure channel multiplexing (8-byte header, no `stream_type`); the SOCKS5 session rides inside the channel's `BiStream` - alknet ADR-078 / alkcall ADR-050: two-pump shutdown-on-completion; the helper is pinned upstream as `alkcall::channels::pump_bidi` (use it, do not hand-roll) - alkcall ADR-049 (amendment 2): the establishment phase; the establisher returns the negotiated handle via `Establishment::new` (typed-opaque `ChannelPlan`, `Send + Sync` payload constraint) - alknet ADR-074 / alkcall ADR-038: `ChannelConnection` as a `BidiStreamSource`; every handler receives a `Connection` - alknet ADR-075 / alkcall ADR-039: `ChannelsAdapter` / `ChannelManager` (substrate-agnostic demux; `params` is ALPN-specific, i.e. for this crate the SOCKS5 service identity/policy) - alknet ADR-071 / alkcall ADR-034: channels wire format (one-way door); alkcall ADR-036: channel 0 is pre-negotiated `alk/call`; alkcall ADR-037: channel lifecycle operations (`channel/open` on channel 0) - alkcall ADR-042: hub relay; alktunnels' hub-proxy refinement, in which the hub terminates and re-produces per-hop ACL (a SOCKS5 resource is the same shape, "further downstream", per alktunnels phase-0-findings §Prior art) - alkcall ledger CF-005/CF-006 (0.7.0): the connect-side serving identity seam: caller identity precedence (token > `ServingConfig.identity` > transport), per-call opener identity on the open-op hooks - The suite decomposition context: alknet was the massive POC ("vpn-like without being a vpn" + transport agnosticism) being rewritten; this crate extracts and improves its SOCKS5 story (client side: ADR-090's proxy support; server side: the `-D` capability alktunnels deferred to composition). `alknet-client/src/socks5.rs` and the quinn-proxy POC findings are the direct ancestors of this crate's client work. - If a TODO references a design direction that a later ADR has decided against, the TODO is stale; remove it and align with the ADR. Do not implement the rejected design.