diff --git a/Cargo.lock b/Cargo.lock index 909ad12..cea7cdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,7 +51,6 @@ name = "alktunnels" version = "0.1.0" dependencies = [ "alkcall", - "async-trait", "bytes", "futures", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8c004bc..e56b60d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ bytes = "1" futures = "0.3" serde = { version = "1", features = ["derive"] } serde_json = "1" -async-trait = "0.1" tracing = "0.1" thiserror = "2" diff --git a/src/consumer.rs b/src/consumer.rs new file mode 100644 index 0000000..ceaa068 --- /dev/null +++ b/src/consumer.rs @@ -0,0 +1,9 @@ +//! The consumer half: `TunnelSession` — the typed client for tunnel +//! channels (ADR-005). Forward path (`open`), reverse path (`adopt`), +//! substrate-shaped data planes (`stream_halves` / `take_halves` / +//! `send_datagram` / `recv_datagram`), and teardown ownership +//! (`close` / `join` / `Drop`). +//! +//! Skeleton module — filled by `tunnels/consumer-session`. + +pub use crate::producer::{OP_TUNNEL_OPEN, TUNNEL_ALPN}; diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..a62b372 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,23 @@ +//! Tunnel error surfaces: the crate's `TunnelError` plus the typed +//! open-error helpers consumers branch on (ADR-049 §4 surface). +//! +//! Skeleton module — filled by `tunnels/params` (codec + establishment +//! error mapping) and `tunnels/consumer-session` (session errors). + +/// The crate-level error type (thiserror; AGENTS.md convention 2). +/// +/// Skeleton: the codec and session variants land with +/// `tunnels/wire-codec` and `tunnels/consumer-session`. +#[derive(Debug, thiserror::Error)] +pub enum TunnelError { + /// The data-plane codec rejected an operation (UDP framing). + #[error("codec: {0}")] + Codec(String), + /// A substrate-shaped operation was attempted on the wrong + /// data plane (e.g. `send_datagram` on a stream tunnel). + #[error("wrong substrate for this operation")] + WrongSubstrate, + /// An I/O failure on the session's data plane. + #[error("io: {0}")] + Io(#[from] std::io::Error), +} diff --git a/src/lib.rs b/src/lib.rs index 9c0e924..8b37457 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,29 @@ //! alktunnels — arbitrary bidirectional tunnels over alkcall channels. //! -//! Protocol-only scaffold. The producer half (tunnel open-handler), -//! consumer half (typed session), and shared wire/target-addressing -//! modules land with the Phase 1 architecture spec. +//! TCP, UDP, unix-socket, and other stream/datagram substrates, in the +//! `ssh -L` / `ssh -D` / `ssh -R` sense. A producer/consumer protocol +//! crate riding alkcall channels the same way alktty does (`alk/tty` +//! is the sibling precedent): the producer half registers the +//! `alk/tunnel` open op and pumps bytes between the channel and the +//! substrate; the consumer half is the typed session +//! ([`TunnelSession`]) that opens tunnel channels and owns teardown. +//! +//! A tunnel is a resource, not an address: the open-op params identify +//! a produced resource + substrate ([`TunnelParams`], ADR-001); the +//! producer owns the backing. The protocol layer is substrate-agnostic +//! by construction — socket/platform I/O is confined to the +//! feature-gated `local` backend (the default crate is wasm-clean). +//! +//! - Wire format: `docs/architecture/wire.md` (+ BAST at `bast.md`) +//! - Open-op params: ADR-001; codec: ADR-003; ALPN: ADR-002 +//! - Producer shapes: ADR-004; consumer session: ADR-005; ACL: ADR-006 + +pub mod consumer; +pub mod error; +pub mod params; +pub mod producer; +pub mod wire; + +pub use error::TunnelError; +pub use params::{establishment_reason, ChannelOpenError, TUNNEL_ALPN, TUNNEL_OPEN_SCOPE}; +pub use wire::MAX_DATAGRAM_LEN; diff --git a/src/params.rs b/src/params.rs new file mode 100644 index 0000000..71f43fa --- /dev/null +++ b/src/params.rs @@ -0,0 +1,28 @@ +//! Open-op params: `TunnelParams {resource, substrate}` — the +//! wire-stable carrier of "what to open" (ADR-001), plus the open-op +//! `OperationSpec` builder, the scope/ALPN/op-id constants, and the +//! typed establishment-reason helper (ADR-049 §4 surface). +//! +//! Skeleton module — the schema-validation tests land with +//! `tunnels/params`; the spec builder shape is already final here. + +pub use alkcall::channels::client::ChannelOpenError; + +/// The `channels/tunnel/sub` operation id (the open op). +pub const OP_TUNNEL_OPEN: &str = "channels/tunnel/sub"; + +/// The data-plane ALPN the tunnel channel carries (ADR-002: single +/// `alk/tunnel` ALPN; the substrate discriminator rides params). +pub const TUNNEL_ALPN: &str = "alk/tunnel"; + +/// The scope gating tunnel opens (ADR-006). Stable once published. +pub const TUNNEL_OPEN_SCOPE: &str = "tunnel:open"; + +/// The establishment-failure reason code of a +/// `channel:open_failed` error (`details.reason`): `dial_failed`, +/// `unknown_resource`, `resource_shortage`, `handler_error`, +/// `timeout` (alkcall ADR-049 §3). `None` for any other failure +/// shape (`FORBIDDEN`, `channel:too_many_channels`, …). +pub fn establishment_reason(err: &ChannelOpenError) -> Option<&str> { + err.establishment_reason() +} diff --git a/src/producer.rs b/src/producer.rs new file mode 100644 index 0000000..0ba0e3d --- /dev/null +++ b/src/producer.rs @@ -0,0 +1,10 @@ +//! The producer half: the `channels/tunnel/sub` open op — spec +//! builder, establisher shapes (dial + listen), the pump handler, and +//! registration (`register_tunnel_openable`) per `producer.md` / +//! ADR-004. +//! +//! Skeleton module — filled by `tunnels/producer-open-op` (establisher +//! + pump handler + registration) and `tunnels/producer-listen` (the +//! listen establisher variant). + +pub use crate::params::{establishment_reason, OP_TUNNEL_OPEN, TUNNEL_ALPN, TUNNEL_OPEN_SCOPE}; diff --git a/src/wire.rs b/src/wire.rs new file mode 100644 index 0000000..ab9e504 --- /dev/null +++ b/src/wire.rs @@ -0,0 +1,11 @@ +//! The data-plane codec (ADR-003): raw pass-through for stream +//! substrates (`tcp`, `unix`), mandatory `[len: u16 BE]` per-datagram +//! framing for `udp`. Normative WHAT in `wire.md`; the binary framing +//! carries its BAST contract at `docs/architecture/bast.md`. +//! +//! Skeleton module — `frame_datagram`, `DatagramReader`, and +//! `DatagramCodecError` land with `tunnels/wire-codec`. + +/// The u16 length-field bound: datagrams larger than this are +/// rejected at frame time (ADR-003 — never a wire overflow). +pub const MAX_DATAGRAM_LEN: usize = 65535; diff --git a/tasks/tunnels/crate-init.md b/tasks/tunnels/crate-init.md index 559fb6d..320171e 100644 --- a/tasks/tunnels/crate-init.md +++ b/tasks/tunnels/crate-init.md @@ -1,7 +1,7 @@ --- id: tunnels/crate-init name: Initialize the alktunnels module skeleton (params, wire, producer, consumer, error) -status: pending +status: completed depends_on: [architecture/oq-promotion-sync] scope: narrow risk: low @@ -67,14 +67,14 @@ pub mod consumer; ## Acceptance Criteria -- [ ] `src/lib.rs` declares `error`, `params`, `wire`, `producer`, +- [x] `src/lib.rs` declares `error`, `params`, `wire`, `producer`, `consumer` with doc comments; public API re-exports listed (empty bodies fine) -- [ ] Every skeleton module compiles (`cargo check` clean) -- [ ] `cargo clippy --all-targets -- -D warnings` clean -- [ ] `cargo fmt --check` clean -- [ ] `cargo check --target wasm32-unknown-unknown` passes -- [ ] No comments in code beyond doc comments (convention 1) +- [x] Every skeleton module compiles (`cargo check` clean) +- [x] `cargo clippy --all-targets -- -D warnings` clean +- [x] `cargo fmt --check` clean +- [x] `cargo check --target wasm32-unknown-unknown` passes +- [x] No comments in code beyond doc comments (convention 1) ## References @@ -84,8 +84,29 @@ pub mod consumer; ## Notes -> Agent fills during implementation. +- All five skeleton modules created per the task's shapes, with module + docs pointing at the spec docs + ADRs and `tunnels/*` tasks that fill + them in. +- Constants placed early (`OP_TUNNEL_OPEN`, `TUNNEL_ALPN`, + `TUNNEL_OPEN_SCOPE` in params.rs — they are part of the wire surface, + not producer-only, so they live in the shared module per the module + map's "params.rs … + open-op input schema"). +- `establishment_reason` is a thin delegation to + `ChannelOpenError::establishment_reason` (alkcall 0.7.0 has the + method upstream); the crate-level helper keeps the POC's free-fn + shape for consumers and re-exports the typed error. +- `MAX_DATAGRAM_LEN` (65535) placed in wire.rs — the one codec constant + ADR-003 pins regardless of the codec functions landing later. +- **`async-trait` dropped** per the task's IF: the skeleton confirms no + trait is needed (ADR-004 — halves functions, not traits); + `cargo check` passes without it. ## Summary -> Agent fills this on completion. \ No newline at end of file +Module skeleton landed (error/params/wire/producer/consumer + lib.rs +docs + re-exports); Cargo.toml unchanged except `async-trait` dropped +(ADR-004 confirmation). Verified: `cargo check`, `cargo clippy +--all-targets -- -D warnings`, `cargo fmt --check`, `cargo test`, +`cargo check --target wasm32-unknown-unknown`, +`cargo clippy --target wasm32-unknown-unknown -- -D warnings` — all +clean. \ No newline at end of file