--- id: implementation/core-params-errors name: Core — params schema + scope constants + typed error surface status: completed depends_on: [implementation/core-crate-deps] scope: narrow risk: low impact: component level: implementation --- ## Description Implement `src/params.rs` and `src/error.rs` per wire.md §The Open Op, ADR-001, and ADR-006 — the wire-stable surface (the one-way door) and the typed open-error vocabulary. Template: `/workspace/@alkdev/alktunnels/src/params.rs` (the sibling's spec builder + scope-constant posture). Deliverables: 1. **`src/params.rs`**: - Constants: `OP_SOCKS5_OPEN = "channels/socks5/sub"`, `SOCKS5_ALPN = "alk/socks5"`, `SOCKS5_OPEN_SCOPE = "socks5:open"`, `SOCKS5_BIND_SCOPE = "socks5:bind"` (ADR-001/ ADR-006; the scope-string convention `: `; stable once published). - `socks5_open_spec() -> OperationSpec` — `Sub`-typed, external, channel-open marker on `SOCKS5_ALPN`, **trivial params**: input schema `{type: object}`, `additionalProperties: false` (empty params stay valid forever — ADR-001's additive-only extension rule); reply schema `{"channel_id": integer, minimum 1}`; `required_scopes = ["socks5:open"]`. Follow the alktunnels spec-builder shape exactly (the registry validates against these schemas). - The trivial params struct (`Socks5Params {}` — serde `deny_unknown_fields`, empty) for the consumer's open call. 2. **`src/error.rs`** — the typed open-error surface (alkcall ADR-049 §4): - `SocksOpenError` — the consumer-side open-failure vocabulary mapping the `channel:open_failed` reasons (`dial_failed` / `unknown_resource` / `resource_shortage` / `handler_error` / `timeout`) plus the registry's `FORBIDDEN`/`INVALID_INPUT` classes, shaped on alktunnels' `TunnelOpenError` + `establishment_reason` helper. - The session-level error type for the consumer sessions (`SocksSessionError`-shaped: open errors vs in-stream `SocksError` vs I/O) — the exact variant set is the task's judgment; keep it faithful to what the sessions actually surface (a refused session never yields a phantom channel; a refused target is an RFC reply code surfaced through the session type — consumer.md §Error surface). 3. **lib.rs re-exports** for the new public surface (convention 15). Tests: spec-builder shape assertions (op id, ALPN marker, scopes, schemas — the trivial-params schema rejects unknown fields), params serde round-trip (empty object valid; unknown field rejected), the error-reason mapping table. ## Acceptance Criteria - [ ] Constants + spec builder per ADR-001/ADR-006 exactly (op id, ALPN, both scopes, trivial schemas) - [ ] Input schema rejects unknown fields (the loud-rejection posture) - [ ] Typed open-error surface maps the ADR-049 §4 reasons - [ ] lib.rs re-exports the public surface - [ ] `cargo test` green; clippy/fmt clean; both wasm checks clean ## References - docs/architecture/wire.md §The Open Op (the normative op shape) - docs/architecture/decisions/001-alpn-and-open-op.md (trivial params; the one-way door) - docs/architecture/decisions/006-access-control.md (the two scopes) - docs/architecture/decisions/011-inline-vendor.md §4 (the typed open-error surface) - Template: `/workspace/@alkdev/alktunnels/src/params.rs` (`tunnel_open_spec`, the scope constants, `establishment_reason`) - POC sketch: `/workspace/alksocks-connect-poc/src/params.rs` ## Notes > Filled by the implementation agent (2026-09-23). - `src/params.rs`: the four wire-stable constants (`OP_SOCKS5_OPEN`, `SOCKS5_ALPN`, `SOCKS5_OPEN_SCOPE`, `SOCKS5_BIND_SCOPE`), the trivial-params struct `Socks5Params {}` (serde `deny_unknown_fields`, + `Serialize` so tests round-trip it; `as_json()` names the canonical wire form), `socks5_open_spec()` (Sub/External/channel-open marker on `alk/socks5`; input schema `{type: object, additionalProperties: false}`; reply schema `channel_id` integer minimum 1 required; `required_scopes = ["socks5:open"]`), and the `establishment_reason` helper re-export (the alktunnels posture — delegates to `ChannelOpenError::establishment_reason`). - `src/error.rs`: `SocksOpenError` (single `Open` variant wrapping `ChannelOpenError` — the same shape as alktunnels' `TunnelOpenError`; the `From` impl covers the `channel:open_failed`/`FORBIDDEN`/`INVALID_INPUT` classes), the typed `SocksEstablishmentReason` vocabulary (ADR-049 §3's five codes, `parse`/`as_str`; unknown codes are `None` — never silently aliased, the vocabulary is one-way), `SocksOpenError::reason()` as the typed view, and `SocksSessionError` (open errors vs in-stream `Rfc(SocksError)` vs `Io`; the RFC reply-code refusals ride the session type per consumer.md §Error surface). `SocksError`/ `SocksServerError`/`ReplyError` re-exported from `rfc1928` (`SocksServerError` lives in `rfc1928::server`, not the module root). - lib.rs re-exports both modules' public surface (convention 15). - Deliberate deviations from the task text: the params struct derives `Serialize` (test round-trip needs it; harmless on the wire — the consumer sends `json!({})`); `SocksEstablishmentReason` is a separate named type rather than an inline match so the mapping table is testable in both directions; unknown reason codes do NOT alias a variant (one-way vocabulary discipline). - rustdoc links to `error::SocksOpenError` from `params` are fully-qualified (`crate::error::…`) — plain `[SocksOpenError]` does not resolve cross-module in this crate's layout. ## Summary > Filled on completion (2026-09-23). Implemented the wire-stable open-op surface and the typed open-error vocabulary: - `params`: constants + `socks5_open_spec()` per ADR-001/ADR-006 exactly (op id `channels/socks5/sub`, ALPN `alk/socks5`, scopes `socks5:open`/`socks5:bind`, trivial empty-object input schema with loud `additionalProperties: false`, reply `{channel_id: integer, minimum 1}` required). - `Socks5Params {}` with `deny_unknown_fields` — empty object valid, unknown fields rejected. - `error`: `SocksOpenError` mapping the ADR-049 §4 reasons (`dial_failed`/`unknown_resource`/`resource_shortage`/ `handler_error`/`timeout`) through the typed `SocksEstablishmentReason` (unknown codes never aliased); the registry's `FORBIDDEN`/`INVALID_INPUT` classes correctly non-establishment; `SocksSessionError` shaped open-vs-Rfc-vs-Io per consumer.md §Error surface. - 9 new tests (spec shape, constants, serde round-trip + unknown-field rejection, the reason mapping table, non-establishment classes, future-reason non-aliasing, the three-plane session surface). - lib.rs re-exports the public surface. Verification: `cargo test` (104 green: 67 lib + 37 rfc1928 + 0 doc), `cargo clippy --all-targets -- -D warnings` clean, `cargo fmt --check` clean, `cargo doc --no-deps` zero warnings, both wasm checks (`cargo check` + `cargo clippy` on `wasm32-unknown-unknown`) clean.