--- status: reviewed last_updated: 2026-10-28 --- # Mapping — server direction: SSH channels × family seams (the spine) ## The door framing The server direction is a door *for vanilla clients*: vanilla in, family out. Vanilla `ssh`/`git` connect as they would to any sshd; every accepted channel/request maps to **exactly one family protocol seam** and **exactly one cargo feature** (ADR-003). Rows below are POC-3-validated against published sibling crates with real vanilla tooling (OpenSSH 9.6, git 2.43, curl 8.5 — `poc-3-mapping-bakeoff.md`). ## The seams | Family crate | Serve-over-a-stream entry | Signature shape | |---|---|---| | alktty | `drive_session[_pre_negotiated]` | `(impl AsyncWrite, impl AsyncRead, backends, identity)` | | alkgit | `serve_duplex_resolved` | `(repo_id, identity, service, duplex, limits, backend traits)` | | alktunnels | — no serve entry (**absence note resolved**: route 1 never needed one, ADR-006) | consumer/open-op registrations | russh channels convert to duplex via `into_stream`/`split` — the bridge primitives every mapping uses (POC-1 §7). ## Channel-type rows | SSH channel type | Vanilla use | Feature | Family seam | Contract notes | |---|---|---|---|---| | `session` → `pty_request` + `shell_request` | `ssh host` interactive shell | `session` | alktty: `NegotiateRequest` constructed locally → `drive_session_pre_negotiated` | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → the reserved modes JSON); shell argv = the assembly-supplied serve-config shell (no crate default; unset → `shell_request` declines — OQ-08 resolved); the data plane is a **translation bridge**, not a byte pump — alktty is chunk-framed both directions (stdin/stdout/stderr/CTRL chunks): SSH data→stdin chunks; stdout chunks→SSH data; stderr chunks→`extended_data(1)`; exit chunk→`exit-status`/`exit-signal` + close (POC-3 §D.1) | | `session` → `exec_request` (non-git) | `ssh host "cmd"` | `session` | alktty pipe mode (`terminal: None`) — separate stderr | the wire's command string → the configured exec wrapper argv with the command appended as final element (`wrapper ++ [cmd]`, OQ-08 — never naively split); wrapper unset → clean decline; stderr rides `extended_data(1)` — only valid without PTY; PTY mode merges (kernel PTY, POC-3 §D.1 proven) | | `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions | | `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands — the feature exists, the route doesn't (OQ-13 resolved: the tested decline placeholder is the shipped behavior until an alkfs-mapping ADR changes it). **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves | | `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge — `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds — failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) | | `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | listen flow = the global-request rows below; the data leg is untested in POC-3 beyond refusal (see the POC-3 note below) | | `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | compiles wherever `tunnel` compiles; serving is **runtime-Unix-gated** — on Unix serves, on other platforms a clean open-decline (OQ-11 resolved: universal compile + runtime decline, no per-platform feature fork) | | `x11` | X forwarding | — (never) | — | always declines; not feature-gated (ADR-003 §5) | | `auth-agent` | agent forwarding | — (never) | — | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) | | `session` → SOCKS5-in-channel | (no vanilla use) | — (retired, ADR-003 §1a) | — | vanilla `-D` is client-side SOCKS + `direct-tcpip`; the family local door is alksocks composed at the assembly — see `client.md` | ## Session-channel dispatch (ADR-006, as amended) **Parse-only.** A session channel that receives a request takes the parse path: `exec_request` parses first (a git-parseable command routes to `git`; anything else to `session`'s pipe path), and `pty_request`/`shell_request` route to `session`. Request-less session channels decline as unfeatured. Parsing happens before dispatch and before any resource allocation — a leaked channel alloc is a capability leak, so **decline first, alloc never** (POC-3 stage A's decline matrix is the contract: unknown repo → collapsed generic denial *before any protocol byte* — no existence oracle; non-git command → parse-then-fail). ## Global-request rows (the `-R` listen flow) `Handler::tcpip_forward(address, &mut port) -> bool` + `Handler::streamlocal_forward` — gate on `tunnel` (decline both the listen request and the data channels when absent). `streamlocal_forward` additionally carries the same runtime-Unix gate as its channel row (OQ-11): off-Unix it declines. The bound-port out-param (`port = 0` → server picks) is a spec detail in `client.md`'s lifecycle primitives; disposition follows `tunnel`. *POC-3 note: only the refusal shape was exercised (egress-only build); the served listen flow is implementation-phase contract-test territory.* ## Request-level rows on a `session` channel | SSH request | Family mapping | |---|---| | `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{…}` (POC-3 §D.1: live `stty size` honored) | | `shell_request` | `NegotiateRequest.cmd` = the **assembly-supplied serve-config shell argv** (no crate default; unset → clean decline, never a fallback — OQ-08 resolved; never wire-supplied) | | `exec_request` | parse → git dispatch (`git`) or alktty pipe `cmd` = exec wrapper argv with the command string appended (`wrapper ++ [cmd]`, OQ-08 — vanilla quoting semantics, never naively split; wrapper unset → clean decline); unknown → `channel_failure`, no resource alloc | | `subsystem_request` | decline (per the rows table; honest, with reason) | | `env_request` | **declined unconditionally** (OQ-09 resolved; no allowlist ships) — wire env is not family config | | `window_change_request` | resize control (POC-3 §D.1: live resize proven) | | `signal` | signal control (alktty names verbatim); PTY mode reaches the process group, pipe mode is pid-only (backend semantics — fidelity note, OQ-10) | | data / `extended_data(1)` | the translation bridge (rows table) | | `exit_status` | from the alktty exit chunk — sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs). Derivation contract rules 1, 3–6 below | | `exit_signal` | negative exit-chunk codes (the Unix pipe path's signal encoding) → `exit-signal(name)` — derivation contract rule 2; `−1` → `exit-status(255)`, never a signal claim (rule 3). PTY path: signal-terminated children arrive as a plain code 1 (portable-pty's exit surface) → `exit-status(1)` — the signal-identity loss is an upstream alktty/portable-pty constraint (rule 5) | ## Exit chunk → SSH exit reporting (the derivation contract) The alktty exit chunk (`{"type":"exit","code":N}`, exit-chunk-is-last — alktty ADR-004, alknet ADR-055 lineage) is the sole fidelity input; the chunk is **not widened** (any widening needs its own alktty ADR — the convention is consumed as-is; OQ-10 resolved). Rules, normative for the `exit_status`/`exit_signal` rows above: 1. **`code ≥ 0`** → `exit-status(N)`. 2. **Negative `−N`, N ≥ 2 (the Unix pipe path's signal encoding)** → `exit-signal(name)` via a Unix signal-number→name table (server-side work; signal semantics belong to the serving platform, where the backend reaps). `core_dumped` is **never asserted** — alktty discards `ExitStatus::core_dumped()`, so nothing better is knowable from the chunk, and SSH's `exit-signal` bool stays false. (POC-3 §D.2 observed the full derivation live: signal TERM → `−15` → `exit-signal("TERM")`.) 3. **`−1` → `exit-status(255)`** (OpenSSH's conventional failure value). alktty's could-not-determine sentinel collides with the SIGHUP encoding on the Unix pipe path, and this rule accepts the fidelity cost symmetrically: neither a fabricated signal for a wait failure (a signal that never happened) nor a real one — a *genuine* SIGHUP death on the pipe path also reports 255, not `exit-signal("HUP")`. If the wait-failure vs SIGHUP distinction ever becomes load-bearing, alktty's additive `exit_error` control type is the extension seam (recorded in alktty's own ADR); alkssh derives from whatever that carries, with no contract change here. 4. **Non-Unix**: no signal termination exists; `−1` is the only negative that can appear → rule 3. 5. **The PTY path (fidelity limit, upstream)**: portable-pty 0.9 reaps a signal-terminated Unix child as a plain code 1 (the signal number is unrecoverable through its trait surface — verified against portable-pty 0.9.0's source) → PTY-row signal-terminated sessions arrive as `exit-status(1)`. Faithful `exit-signal` there is an alktty/portable-pty ask with tty/pty-grounded justification; alkssh's contract adapts automatically if the convention ever holds on that path. Pipe-mode pid-only vs PTY process-group *scope* (the `signal` row note) is backend semantics, not a mapping detail. 6. **Unmapped negatives → rule 3.** The convention's producers are `−1` and `−signum` only; a negative value with no entry in the signal-name table (defensive case — the mapping never invents a name) reports `exit-status(255)`. ## The structural constraint: spawn-and-return russh (this pin: ADR-002) runs server handler callbacks **inline inside the session's packet loop** — a blocking handler suspends all packet routing for the connection (channel data is routed by that same loop; an inline serve deadlocks against the peer's immediate follow-up bytes — POC-3 proved by construction with receive-pack). Therefore: **every serving path is spawned; handlers return fast**; exit/status reporting from spawned tasks rides the cloned `Session::handle()`. This is a load-bearing correctness constraint of the whole spine, not an implementation choice. ## Data-plane / limits - russh knobs (`window_size`, `maximum_packet_size`, `channel_buffer_size`, `Limits`, `inactivity_timeout`, `keepalive_*`) + in-crate max-concurrent-channels cap + per-length exec-parse limits — per the bounded-resources policy in `overview.md`. - alkcall bridge leg (when the family half is remote): channels-layer reassembly cap 1 MiB/channel; ADR-034 grants `alk/ssh` stream_types `[0, 1]` because SSH multiplexes internally. - Honest advertisement: `Config::methods` mirrors implemented auth (ADR-004); an unfeatured channel type declines at open, never half-opens; vanilla clients observe clean diagnostics (POC-1 §5: channel-scoped decline, auth intact, no hangs). ## Decline matrix (per feature build, contract-tested) | Build | Serves | Declines | |---|---|---| | none (default) | — | everything | | `git` | git-parseable exec | pty/shell, plain exec, all other channel types, all `session` requests | | `session` | pty/shell/pipe-exec (requires the configured shell/exec wrapper — OQ-08; an unset field declines its row though the feature is on) | git-parseable exec (the `git` parse is its gate), other channel types | | `tunnel` | direct/forwarded-tcpip, streamlocal (unix), `-R` listens | session channels (auto-reject), all others | | any build + `sftp` slot | unchanged from the underlying build | `sftp` subsystems decline-with-reason until alkfs | Every row is tested in *every* build it can appear in — the decline paths are contract territory (AGENTS.md conventions 5–6), including under `--all-features`. ## Open Questions None open or deferred — see [open-questions.md](open-questions.md) (all of OQ-01…13 resolved). Doc-local contract anchors: OQ-02 (session dispatch, ADR-006 as amended — §above), OQ-08 (the shell argv / exec wrapper serve config — `shell_request`/`exec_request` rows and the `session→pty+shell` channel row), OQ-09 (the `env_request` row), OQ-10 (§the derivation contract — formalized, upstream constraint noted there), OQ-11 (`direct-streamlocal` row + the listen-flow gate), OQ-13 (the `sftp` rows — the decline placeholder is the shipped behavior). ## References - ADR-003 (features), ADR-006 (route 1, exec-only git, dispatch), ADR-001 (the ALPN/edge context), ADR-004 (auth) - `docs/research/mapping-table.md` (the validated research ancestor + annotations), `poc-3-mapping-bakeoff.md` (stages A–D), `poc-1-auth-bridge.md` §5/§7 (decline observability, plumbing) - Family ADR-003; alkcall ADR-034 (`alk/ssh` stream_types) - `client.md` (the same door, client-side)