--- id: tunnels/params name: TunnelParams + open-op spec (schema, scope gate, constants) status: pending depends_on: [tunnels/crate-init] scope: narrow risk: low impact: component level: implementation tags: [wire, params, open-op] --- ## Description Implement `src/params.rs` per ADR-001 + wire.md §The Open Op: the `TunnelParams` wire type, the `Substrate` discriminator, and the open-op `OperationSpec` builder. This is the wire-stable surface (one-way door) — exact conformance to the ADR is the point. ### Types ```rust #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct TunnelParams { pub resource: String, pub substrate: Substrate, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum Substrate { Tcp, Udp, Unix } ``` - `deny_unknown_fields` is the loud-rejection posture (ADR-001: unknown fields fail schema validation, never silently ignored). - `Substrate::Unix` is included in the type AND in the open-op schema's enum list v1 (`["tcp", "udp", "unix"]` — OQ-TN-14 resolved: unix has an implementation path via the `local` feature). The schema list is the authoritative gate for newer-substrate negotiation: an older producer rejecting a newer substrate value is the SSH "unknown channel type" posture (loud, not silent). ### The open-op spec (in producer.rs or params.rs — put it where `tunnels/producer-open-op` can consume; the spec builder lives in params.rs per the module map, producer.rs re-exports) ```rust pub const OP_TUNNEL_OPEN: &str = "channels/tunnel/sub"; pub const TUNNEL_ALPN: &str = "alk/tunnel"; pub const TUNNEL_OPEN_SCOPE: &str = "tunnel:open"; pub fn tunnel_open_spec() -> OperationSpec ``` - `OperationType::Sub`; `Visibility::External`; channel-open marker `ChannelOpenSpec::new(TUNNEL_ALPN)`. - `input_schema`: `{resource: string (required), substrate: string enum ["tcp","udp","unix"] (required)}` — JSON Schema Draft shape as the POCs used (unix in the list per OQ-TN-14's resolution). - `output_schema`: `{channel_id: integer > 0}`. - `AccessControl.required_scopes: [TUNNEL_OPEN_SCOPE]` (ADR-006). - `description`: a one-line human hint (round-trips through discovery; the spec's SHOULD). ### Error surface `src/error.rs`: `TunnelError` (thiserror) covering the codec + session errors (filled incrementally by later tasks); plus the re-export of `alkcall::channels::client::ChannelOpenError` and an `establishment_reason(&CallError) -> Option<&str>` helper (ADR-049 §4 surface; the POC consumer's shape). ## Acceptance Criteria - [ ] `TunnelParams` round-trips serde; unknown fields rejected - [ ] `tunnel_open_spec()` matches wire.md §The Open Op exactly (op id, type, ALPN marker, schemas, ACL, description) - [ ] Schema validation test: valid params pass; missing fields, unknown fields, unknown substrate values all fail - [ ] `establishment_reason` maps `channel:open_failed` details.reason (POC-pinned shapes: `unknown_resource`, `dial_failed`, `resource_shortage`) - [ ] Unit tests cover the serde + schema shapes - [ ] wasm32 check passes ## References - docs/architecture/decisions/001-open-params-layout.md - docs/architecture/wire.md §The Open Op - docs/architecture/decisions/006-access-control-posture.md - POC reference: `/workspace/alktunnels-udp-poc/src/params.rs` + `producer.rs::tunnel_open_spec` (the shape, with `deny_unknown_fields` added) ## Notes > Agent fills during implementation. ## Summary > Agent fills this on completion.