--- id: tty/control-messages name: Implement ControlMessage enum and signal_from_name helper status: completed depends_on: [tty/crate-init] scope: single risk: low impact: isolated level: implementation --- ## Description Implement the control channel schema in `src/control.rs`. Control chunks (stream_type 3) carry a JSON payload tagged by `type`. This is a direct port of the POC's `/workspace/alknet-tty-poc/src/control.rs`. ### ControlMessage enum ```rust #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ControlMessage { Resize { cols: u16, rows: u16, #[serde(default)] pixel_width: u16, #[serde(default)] pixel_height: u16, }, Signal { name: String }, Eof, Exit { code: i32 }, } ``` | Direction | Message | Shape | Maps to | |---|---|---|---| | client→server | resize | `{"type":"resize","cols":80,"rows":24,...}` | SSH `window-change`, docker exec resize, `ioctl(TIOCSWINSZ)` | | client→server | signal | `{"type":"signal","name":"INT"}` | SSH `signal`, docker exec signal, `kill(-pgid, sig)` (REQ-TTY-02) | | client→server | eof | `{"type":"eof"}` | SSH channel EOF, docker stdin close, `ChildStdin::drop` | | server→client | exit | `{"type":"exit","code":0}` | the terminal/completion signal (ADR-055) | ### Serialization helpers ```rust impl ControlMessage { pub fn to_json(&self) -> serde_json::Result; pub fn from_slice(b: &[u8]) -> serde_json::Result; } ``` ### signal_from_name (Unix-only) ```rust #[cfg(unix)] pub fn signal_from_name(name: &str) -> Option; ``` Maps uppercase signal names to `libc` signal numbers: `HUP`, `INT`, `QUIT`, `TERM`, `KILL`, `USR1`, `USR2`, `TSTP`, `CONT`. Unknown names return `None`; the caller (the local backend) decides whether to ignore or fall back to the backend's default kill. This helper lives in `alknet-tty` (not `alknet-tty-local`) because the wire spec defines the supported name set (tty-wire.md §"Control Channel") and the local backend consumes it. On non-Unix targets, `signal_from_name` is absent (the local backend's non-Unix signal path falls back to `ChildKiller::kill` directly). The `#[cfg(unix)]` gate matches the POC. ### Extensibility Unknown `type` values are **ignored** (not a protocol error) so that a newer client sending a control message an older server doesn't recognize degrades gracefully rather than tearing down the session. This is handled in the adapter's control dispatch (task `tty/adapter`), but the `Deserialize` impl must tolerate it — use `#[serde(other)]` on a catch-all variant OR handle the `serde_json::Error` in the adapter as "ignore unknown." The POC handles it in the adapter (logs a warning on parse error, continues). Match the POC's approach: do NOT add a catch-all variant to the enum; let `from_slice` return an error on unknown `type` and have the adapter ignore the error. This keeps the enum exhaustive and the wire format's "unknown types are ignored" rule an adapter-level policy, not a schema-level leak. ### Tests - Round-trip: serialize each variant, deserialize, assert equality. - `to_json` produces the expected `{"type":"resize",...}` shape (snake_case tag). - `signal_from_name` returns the right numbers for all 9 names, `None` for unknown names (Unix only). - `from_slice` on `{"type":"unknown"}` returns an error (the adapter will ignore it). ## Acceptance Criteria - [ ] `ControlMessage` enum with `Resize`, `Signal`, `Eof`, `Exit` variants - [ ] `#[serde(tag = "type", rename_all = "snake_case")]` attribute - [ ] `Resize` has `cols`, `rows`, `pixel_width` (default 0), `pixel_height` (default 0) - [ ] `Exit` has `code: i32` - [ ] `to_json` and `from_slice` helpers implemented - [ ] `signal_from_name` implemented for the 9 supported names, `#[cfg(unix)]` gated - [ ] Round-trip unit tests for all 4 variants - [ ] Unit test: `to_json` produces snake_case `type` tag - [ ] Unit test: `signal_from_name` for all 9 names + unknown (Unix only) - [ ] Unit test: `from_slice` on unknown `type` returns error - [ ] `cargo test -p alknet-tty` succeeds - [ ] `cargo clippy -p alknet-tty` succeeds with no warnings ## References - docs/architecture/crates/tty/tty-wire.md — §"Control Channel (stream_type 3)", §"Stdin Closure" - docs/architecture/decisions/055-exit-code-on-control-chunk.md — ADR-055 (Exit variant) - /workspace/alknet-tty-poc/src/control.rs — the reference implementation to port ## Notes > Near-verbatim port of the POC's `control.rs`. The `signal_from_name` helper > is Unix-only; the local backend's non-Unix path uses `ChildKiller::kill` > directly. The "unknown type ignored" policy is enforced in the adapter, not > the schema — keep the enum exhaustive. ## Summary > To be filled on completion