alkcall 0.7.0 resolved every open downstream-consumer finding, including this crate's reverse-flow POC W1 (filed as ledger CF-005, with the CF-006 corollary): - CF-005 (a): ServingConfig.identity — the explicit override - CF-005 (b): transport Connection::identity() propagates to channel 0 — set_identity before dialing is the mTLS/QUIC path - CF-006: the establisher/pump handler receive the per-call opener identity (dispatch-resolved), not the install-time context - CF-007: ADR-016 code list completed (doc-only) - F-1 (POC finding): the ChannelPlan type doc now carries the Send + Sync payload constraint alktunnels updates: - Cargo.toml: alkcall 0.6.0 -> 0.7.0 (lockfile resolves 0.7.0) - AGENTS.md convention 11: pin note updated; the 0.7.0 identity surface marked load-bearing (precedence order + per-call opener) - AGENTS.md architecture context: CF-005/CF-006 row added; the 'What the POC does NOT settle' list replaced with the accurate settled state (Phase 0 complete; Phase 1 = the spec) - reverse-poc-summary: W1 marked RESOLVED with the remediation validated from the POC's topology (16 tests over 0.7.0: transport identity alone authorizes, ServingConfig.identity overrides, identity-less fails closed, CF-006 witness); F-1 marked documented upstream; what-the-POC-does-not-validate and verification blocks updated - phase-0-findings OQ-TN-10: reverse POC entry notes the 0.7.0 re-validation and W1 resolution The reverse POC (/workspace/alktunnels-reverse-poc, 8b35818) re- validated: the default harness now authorizes on transport identity alone — the 0.6.0-era auth_token workaround is dead code. Verification: cargo test, clippy --all-targets -D warnings, fmt --check, wasm32 check, doc --no-deps — all clean
313 lines
17 KiB
Markdown
313 lines
17 KiB
Markdown
# 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 / 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.7.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` — its
|
|
`Send + Sync` bound constrains plan payloads; socket handles carry
|
|
`+ Sync`) — no side-channel handoff. The 0.7.0 identity surface
|
|
(ledger CF-005/CF-006) is load-bearing for the serving side: a
|
|
connect-side serving op resolves the caller identity in
|
|
precedence order — payload `auth_token` >
|
|
`ServingConfig.identity` > transport identity (set via
|
|
`Connection::set_identity` before dialing) — and the establisher/
|
|
pump handler receive the per-call opener identity, not the
|
|
install-time context.
|
|
|
|
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)
|
|
- 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 reverse-flow POC's W1, resolved)
|
|
- alknet ADR-085 — workspace scope: `alknet/tunnel` was flagged
|
|
"POC-validated, minimal spec needed, not yet specced"; this crate
|
|
is that spec
|
|
- What the POCs settled (Phase 0 complete — 2026-09-07): both POCs
|
|
ran clean (forward UDP POC + reverse-flow POC; see
|
|
`docs/research/poc-summary.md` and `reverse-poc-summary.md`). The
|
|
remaining Phase 1 work is the spec: OQ promotion, params JSON ADR,
|
|
ALPN ADR, codec ADR (UDP framing mandatory per F-2), and the
|
|
producer/consumer API surface (the consumer session type owns
|
|
teardown — reverse POC W3).
|
|
- 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. |