docs: repo init — gitignore, AGENTS.md, Phase 0 draft
- .gitignore (target/, node_modules/, .worktrees/, Cargo.lock) - AGENTS.md: project conventions adapted from alktunnels for the SOCKS5 wrapper crate (no-forced-binding posture, fast-socks5 genericity, producer/consumer vocabulary, upstream-asks rule, verification commands with the wasm caveat for OQ-SK-04) - docs/research/phase-0.md: Phase 0 draft — vision, prior art (fast-socks5 surface, alktunnels -D conclusion, alknet ADR-090 client, noq trait-shape divergence), open questions OQ-SK-01..07, POC candidates, convergence checklist - sdd_process.md: package name fixed (alkcall -> alksocks)
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
# 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 <glm-5.3-flash@alk.dev>`). 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 — `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 — 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)** — 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` itself uses `tokio::net` and `socket2` unconditionally;
|
||||
whether it compiles wasm-clean, needs a fork, or the wrapper
|
||||
reimplements the SOCKS5 state machine over `tokio::io` generics is an
|
||||
open question (see `docs/research/phase-0.md` OQ-SK-04) — do not
|
||||
assume either way until it is resolved.
|
||||
|
||||
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 —
|
||||
`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** — 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<T, ...>`,
|
||||
`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)
|
||||
— use it; do not hand-roll the two-pump shape. The channels layer
|
||||
drops all per-channel senders on transport EOF — 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 —
|
||||
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). A local bind (a genuine `socks5://host:port`
|
||||
endpoint) is an optional assembly-layer/feature-gated capability.
|
||||
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 per-connection cap, 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.7.x — 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.7.1"` and bump
|
||||
deliberately. The establishment surface (alkcall ADR-049 +
|
||||
amendment 2) 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` — payloads
|
||||
must be `Send + Sync`). 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** — 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 (producer half — 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 is ours — make asks early.** We own `fast-socks5`
|
||||
(`/workspace/fast-socks5`, v1.0.0, MIT) and alkcall. If the wrapper
|
||||
needs a change upstream (a typestate hook, a generic split, a new
|
||||
event), file it and land it there rather than working around it
|
||||
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). Keep
|
||||
`/workspace/fast-socks5` as the reference checkout; if upstream
|
||||
diverges from what we publish, note the fork point in the research
|
||||
docs.
|
||||
|
||||
## 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 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
|
||||
```
|
||||
|
||||
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. Until OQ-SK-04 (fast-socks5 wasm posture) is
|
||||
resolved, the wasm check may fail on the dependency graph; that failure
|
||||
is expected and is exactly what the OQ tracks.
|
||||
|
||||
## Architecture Context
|
||||
|
||||
- `docs/research/phase-0.md` — 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.7.1, 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.
|
||||
- **fast-socks5** — `/workspace/fast-socks5` (v1.0.0, MIT, we own
|
||||
upstream). The SOCKS5 implementation to wrap: explicit typestate
|
||||
server API (`Socks5ServerProtocol<T, states::*>`), 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).
|
||||
- **alknet's SOCKS5 client** —
|
||||
`/workspace/@alkdev/alknet/crates/alknet-client/src/socks5.rs`
|
||||
(ADR-090): the client-side example need — `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` — `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 — 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 —
|
||||
the hub terminates and re-produces per-hop ACL (a SOCKS5 resource
|
||||
is the same shape, "further downstream" — 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.
|
||||
Reference in New Issue
Block a user