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.
248 lines
19 KiB
Markdown
248 lines
19 KiB
Markdown
---
|
||
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) |