# 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 / tunnel protocol crate) This is the tunnels protocol crate — arbitrary bidirectional tunnels in the `ssh -L` / `ssh -D` sense (TCP, UDP, unix sockets, and other stream or datagram substrates), riding alkcall channels the same way alktty does. It is the sibling of alktty (`alk/tty` — terminal sessions) and alkcall (call + channels — the substrate both build on). Where alktty multiplexes one service with a fixed five-stream channel structure, this crate generalizes the tunnel handler shape: the producer side dials or accepts on some local substrate and pumps bytes to/from a channels data channel; the consumer side bridges its own substrate onto that channel. 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 (`TunnelError`; `HandlerError`/`StreamError` come from `alkcall::core`). 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. The tunnel pumps, the channels integration, and the consumer session type are all async. Use `tokio::sync` primitives (`oneshot`, `mpsc`) for lifecycle correlation and per-direction data flow. Any non-wasm backend module (local TCP/UDP sockets, process-spawned listeners) lives behind a feature flag like alktty's `local`. 4. **WASM target is load-bearing (by default)** — the default crate (protocol-only) should compile to `wasm32-unknown-unknown`, following alktty's precedent. This is what keeps the downstream TS/Python adapter story viable (a wasm-compiled alktunnels is the protocol layer for a sandboxed adapter). 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. If a design decision ever forces the default crate off wasm (e.g. the wire format embeds a substrate-specific type), that is an ADR-worthy decision, not an accident. 5. **Wire format is stable** — the tunnel wire format is a one-way door once consumers exist. Follow the alknet ADR-093/alktty ADR-008 precedent: the tunnel payload rides inside channels data channels as raw bytes (channels strips its 8-byte header transparently; the tunnel protocol owns whatever framing it puts inside the `BiStream`). If a negotiation/setup frame is needed (like alktty's `NegotiateRequest`), make it self-contained per alktty ADR-006 — length-prefixed JSON, not alkcall's `EventEnvelope` framing. Any wire-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 tunnels (registers openable channels via `ChannelCore::register_openable`); a consumer opens tunnel channels (`ChannelClient`). Both sides can be both simultaneously — connection direction (who opened it) is independent of tunnel direction (who dials the target, who serves it). 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. See alkcall ADR-022, ADR-037. 7. **Substrate-agnostic by construction** — the protocol layer must not know whether the bytes come from TCP, UDP, a Unix socket, or a stdio pipe. The handler sees a `Connection`/`BiStream` from alkcall core and pumps bytes; the substrate-specific types (`TcpStream`, `UdpSocket`, etc.) are confined to backend modules behind feature flags, injected at the assembly layer — the same inversion-point pattern as alktty's `TtyBackend`. "Arbitrary tunnels" means the bookkeeping (target addressing, direction, lifecycle) must not hardcode a substrate. See alknet ADR-078 §Where the pattern lives and the channels-adapter spec. 8. **Two-pump shutdown-on-completion is a contract** — a tunnel is the canonical two-pump handler (one pump per direction). Each pump MUST shut down the opposite sink when it completes; `tokio::try_join!` alone deadlocks. This was POC-validated in the alknet-channels POC (Target 3), pinned as alknet ADR-078, and 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** — a tunnel must not require the producer (or consumer) to bind a local port. The POC's TCP-tunnel shape dialed a target from the handler; SSH `-R` style flows need the reverse (the remote side asks the local side to dial or listen). The API surface must support both directions and unbound/listen- optional flows; the binding decision belongs to the caller (assembly layer), not the protocol crate. This is a spec-level requirement for the architecture docs, not just an implementation preference. 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 tunnel crate consumes them via `ChannelCore`/`ChannelClient`; do not build a second demux/mux or re-derive limits. Datagram substrates (UDP) map onto the chunk stream with the same backpressure; datagram-boundary preservation, if required, is a wire-format decision for an ADR, not an accident. 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.5.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.6.0"` and bump deliberately. The 0.6.0 establishment surface (ADR-049 + amendment 2) is load-bearing for this crate: tunnel opens use `register_openable_with_establisher` so a refused target dial is a typed `channel:open_failed` call error, never a phantom channel, and the establisher returns the dialed handle via `Establishment::new(plan)` (typed-opaque `ChannelPlan`) — no side-channel handoff. 12. **BAST document for the wire format** — when the tunnel wire format gains binary framing (if any beyond pass-through), it carries a BAST (Binary Abstract Syntax Tree) document as its machine-readable spec under `docs/architecture/`, conforming to the BAST meta-schema at `https://alk.dev/bast/v1/schema`. BAST is plain JSON — no dependency required to author or consume it. alktunnels does not depend on alktype; hand-rolled codecs are fine for trivial formats, with the BAST doc as the contract. Do not roll your own offset map or validator for complex formats — use alktype (as an optional dep) when that surfaces. 13. **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 tunnel opens (e.g. `TUNNEL_OPEN_SCOPE`, shape following alktty's `TTY_OPEN_SCOPE`) and optionally consult `OwnershipProvider` for resource ownership (`provider.owns(id_ref, kind, &id, "tunnel")` — the 4-arg shape; `OwnershipStore::record` is the 3-arg shape). Tunnels reach local networks — treat the open gate as the security boundary. See alknet ADR-024 (registry layering, alkcall ADR-019), alknet ADR-050 (ownership, alkcall ADR-011). 14. **Feature flags** — substrate backends may be 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. 15. **Naming** — Rust standard: `snake_case` for functions/variables/ modules, `PascalCase` for types/traits, `SCREAMING_SNAKE_CASE` for constants. 16. **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 (to be finalized in the architecture docs): a producer half (adapter / open-handler for `alk/tunnel`-family ALPNs), a consumer half (a typed session/client that opens tunnel channels), and shared wire/target-addressing modules. Backend modules are feature-gated and never imported from the shared/producer/consumer modules. 17. **ALPN naming** — this crate owns the `alk/tunnel`-family ALPN(s). One ALPN per protocol per alkcall ADR-004; keep the `alk/` prefix convention. If multiple tunnel flavors need distinct ALPNs (e.g. stream vs datagram), decide via ADR before the first consumer — ALPN strings are wire-stable once published. ## 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) — run it whenever a non-backend module changes. ## Architecture Context - `docs/architecture/` — will hold the authoritative spec (Phase 1 of the SDD process). It does not exist yet; Phase 0 research lives in `docs/research/`. Read it before non-trivial changes once it exists. - The prior art for this crate: - **alknet-channels POC** — `/workspace/@alkdev/alknet/docs/research/alknet-channels/poc-summary.md` (§POC Target 3: tunnel handler). Validates the demux→Connection→ handler→mux path and the two-pump tunnel shape over a stand-in transport. POC code at `/workspace/alknet-channels-poc/`. - **alkcall** — `/workspace/@alkdev/alkcall` (v0.4.x, crates.io). The substrate: call protocol + channels multiplexing. The tunnel crate consumes `alkcall::core` types and the channels `ChannelCore`/`ChannelClient`/`register_openable` surface. - **alktty** — `/workspace/@alkdev/alktty` (v0.1, crates.io). The sibling pattern to follow: a producer/consumer protocol crate on alkcall channels, with the backend inversion point, wasm-clean default crate, and feature-gated local backend. - 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 tunnel payload is raw bytes inside the `BiStream` - alknet ADR-078 / alkcall ADR-050 — two-pump shutdown-on-completion (the tunnel handler pattern); 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 dialed handle via `Establishment::new` (typed-opaque `ChannelPlan`), replacing the POC's side-channel handoff - alknet ADR-074 / alkcall ADR-038 — `ChannelConnection` as a `BidiStreamSource`; every handler (TTY, tunnel, call) receives a `Connection` - alknet ADR-075 / alkcall ADR-039 — `ChannelsAdapter` / `ChannelManager` (substrate-agnostic demux; `params` is ALPN-specific — for tunnels, the target resource) - alknet ADR-071 / alkcall ADR-034 — channels wire format (one-way door) - alkcall ADR-037 — channel lifecycle operations (`channel/open` on channel 0; `params` carries the tunnel target) - alkcall ADR-042 — hub relay (byte-for-byte data-channel forwarding with ID rewrite — tunnels traverse relays transparently) - alknet ADR-085 — workspace scope: `alknet/tunnel` was flagged "POC-validated, minimal spec needed, not yet specced"; this crate is that spec - What the POC does NOT settle (open work for Phase 0/1): - UDP and Unix-socket substrates (the POC only exercised TCP) — the pump pattern is expected to generalize, but datagram boundary preservation and addressing bookkeeping are unspecced - The "no forced local binding" requirement (SSH `-R`/dynamic flows) is not covered by the POC at all - Target addressing format (what a tunnel `params` looks like) — alknet ADR-071 §ALPN table noted `alknet/tunnel` as `[0, 1]` data in/out only, but the addressing scheme was never decided - If a TODO references a design direction that an ADR has since decided against, the TODO is stale — remove it and align with the ADR. Do not implement the rejected design.