Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
Renames the written-usage rule into practice across the contract surface:
docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files,
taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the
.opencode/agents/ prompts. Every replacement is a sense-preserving
rewrite (colons, semicolons, commas, parentheses, sentence splits, varied),
never a mechanical swap; no spaced hyphens were substituted. Enum fields,
frontmatter, tables, wire data, and cited identifiers survive verbatim;
en dashes inside numeric ranges keep their meaning.

Verification: taskgraph validate (43 tasks), repo grep clean (single
exempt hit: the verification command's own search pattern in AGENTS.md),
cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
2026-11-04 20:28:00 +00:00

248 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
status: reviewed
last_updated: 2026-10-29
---
# 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; see `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`), which separates 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 | none | **`sftp` name**: declines until alkfs lands, since the feature exists but 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, since 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_with_establisher` gate shape (alktunnels 0.5.0, review-003 W2 adoption): the router's correlation lookup rides the establisher's `ForwardedGate`; the handler's `ForwardedHandler` shape hands the channel duplex to the pump | per fabric arrival the door opens a `forwarded-tcpip` channel **toward the vanilla client** (a vanilla client never initiates this type, per sshd parity; an inbound open is the unvanilla class, declined `ConnectFailed`); echo args = the original `tcpip_forward` request's wire string verbatim + the actually-bound port (the sshd `c->path` + `get_local_port` parity; a `port = 0` mint echoes the allocated number); originator = `TunnelForwardedParams.peer`; a stale/missing correlation token fails the open **typed at the call plane** (`channel:open_failed`, `unknown_resource`) before any channel is born (the gate's miss class); the narrow gate-pass→handler-run residual race and the cap-admission declines still teardown-at-birth (never a half-open channel); the accept is the crate's per-session channel cap (the acceptor vocabulary) |
| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | compiles wherever `tunnel` compiles; serving is **runtime-Unix-gated**: on Unix it serves, on other platforms it is a clean open-decline (OQ-11 resolved: universal compile + runtime decline, no per-platform feature fork) |
| `x11` | X forwarding | none (never) | none | always declines; not feature-gated (ADR-003 §5) |
| `auth-agent` | agent forwarding | none (never) | none | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) |
| `session` → SOCKS5-in-channel | (no vanilla use) | none (retired, ADR-003 §1a) | none | 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` (+ both cancel forms), gated on
`tunnel` (unfeatured declines both the listen requests and the data
channels). The served flow (`w3-tunnel-listen`; the route-1 classes
decided against alktunnels 0.4.1's recorded surfaces, the
establisher shape against 0.5.0 (the review-003 W2 adoption), with
POC-3 §C.4 as the template):
- **The listen dial**, family-ward through the consumer surface:
`TunnelListener::open(channel_client, TunnelParams {resource: <the
wire bind endpoint>, substrate: Tcp/Unix})`. The resource is built
from the wire bind endpoint: the address string rides verbatim (the
OpenSSH client normalizes
the no-address case to `"localhost"` before it sends
(`channel_rfwd_bind_host` parity), so the door passes through), with
the tcp port appended (`host:port`; `0` stays `0`, because the OS-picking
is the fabric's bind step, whose bound report feeds the out-param);
the unix path rides alone; whose
bind policy governs (wildcard vs loopback, the sshd `GatewayPorts`
posture) is the fabric's establisher/bind closure, assembly policy
at the family's provider. The dial is bounded (`LISTEN_DIAL_TIMEOUT`
= 10 s, matching alkcall's own establisher deadline) and awaited
**inline** in the hook by wire necessity (the global request's
answer + the `&mut u32` out-param are the hook's own; the fabric
leg's answer doesn't ride this session's routing, so the stall is
bounded, not deadlock-class); this is recorded as the flow's one inline
serving step.
- **The reply contract**: the minted `listen` token (alkcall ADR-049
amendment 3 establisher field) is REQUIRED: a producer that minted
no token (or no `bound` report) gets a declined listen with its
fabric channel reaped immediately (a listen the door cannot
correlate arrivals onto never answers success). The bound port
writes the `&mut u32` out-param (`port = 0` → the actually-bound
number; sshd's `allocated_listen_port` reply parity; the
`client.md` lifecycle primitive).
- **The door-side arrival surface**: the fabric's bind-first accept
dispatcher (`make_bindfirst_accept_handler`) dials the
`tunnel/forwarded` op **toward the door** per arrival (the
`ForwardedOpener` closure); the door serves it through the alkcall
openable registered into the shared per-connection registry, the
serve-config's `TunnelListenSeam` (the `w3-seam-config` second
course-correction: the assembly's `Arc<ChannelClient>` alone
carries no inbound-op serving loop; the seam fronts the
`ServingConfig.registry` the assembly wired at the leg's
`from_connection_with_serving`). The registration runs at
`SshAdapter::new` (a failure is a failed construction,
`AdapterError::ListenRegistration`); alkcall's own gates carry the
per-identity cap + the `tunnel:forwarded` scope per arrival, then
the crate's arrival pump takes over.
- **The arrival pump**: per the `forwarded-tcpip` row above; unix
substrate arrivals (a minted `streamlocal_forward` route) open
`forwarded-streamlocal@openssh.com` with the path echoed (russh's
server `Handle` carries the channel-open at this pin; the OQ-11
runtime-Unix gate rides the unix listen row the same way it rides
the channel row: off-Unix decline, universal compile).
- **The cancel forms + bounded bookkeeping**: the per-session
listener map (packet-loop-only, like the `channels` store) matches
cancels by (the wire endpoint as the request carried it, the bound
port; the sshd `c->path == host && listening_port == port` parity;
the unix forms by path alone) 1:1 into the family listener's
teardown (`TunnelListener::close`/`Drop`; the route leaves the
shared router *before* the teardown races), `false` for an unknown
pair. The listen count is a bounded resource: a dedicated counter
against the same `Limits::max_concurrent_channels` ceiling (the
listener/channels mix stays unconstrained; both stay bounded, so no
unbounded listener map). A session's teardown removes its tokens
from the shared router (a fabric arrival after the session is gone
is the correlation-miss class).
## Request-level rows on a `session` channel
| SSH request | Family mapping |
|---|---|
| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{…}` (POC-3 §D.1: live `stty size` honored); ttymodes → the reserved modes JSON (OQ-44); a stored pty is consumed by the later `shell_request`/`exec_request` on the channel (`ssh -t host "cmd"`). A pre-serve `window_change_request` resizes the stored params (the final size wins) |
| `shell_request` | `NegotiateRequest.cmd` = the **assembly-supplied serve-config shell argv** verbatim (no crate default; unset → the serving-path decline, failure then close; never a fallback, OQ-08 resolved; never wire-supplied). **A shell request without a preceding pty serves pipe-mode (`tty = None`)**, vanilla sshd's posture; `ssh -T` is legitimate vanilla |
| `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 → the serving-path decline, failure then close; never a fallback); 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 through the serving bridge (POC-3 §D.1: live resize proven); fire-and-forget, no SSH reply, so a full/closed control surface drops the event |
| `signal` | signal control (alktty names verbatim, no door-side filter; PTY mode reaches the process group, pipe mode is pid-only; backend semantics and a fidelity note, OQ-10); fire-and-forget, no decline shape |
| 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 → `exit-signal(name)` (derivation contract rule 2); `−1` → `exit-status(255)`, never a signal claim (rule 3). Both the pipe **and** the PTY path report signal deaths as `−N` (alktty 0.8.2 reaps the PTY child directly; the former portable-pty flattening constraint is retired; 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 (constraint retired, alktty 0.8.2)**: portable-pty
0.9's `ExitStatus` flattens a signal-terminated Unix child to a
plain code 1, so PTY-row signal deaths once arrived as
`exit-status(1)`. alktty 0.8.2 retires the constraint (its Unix
PTY waiter skips portable-pty's conversion and reaps the direct
child via `waitpid`, decoding signal deaths as `−N`); the PTY path
now reports signal termination exactly like the pipe path, and the
derivation here needed **no contract change**: the negative-code
convention is consumed verbatim (OQ-10's upstream constraint
closed). 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**, which means 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, as 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) | nothing | 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 + arrivals (the listen forms additionally require the configured `TunnelListenSeam`; an egress-only door declines just the `-R` forms) | 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, the `shell_request`/`exec_request` rows and the
`session→pty+shell` channel row), OQ-09 (the `env_request` row),
OQ-10 (§the derivation contract, now formalized; the former upstream PTY
constraint is retired as of alktty 0.8.2, rule 5), 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)