The wave-3 gate read the landed server mappings against mapping.md/ADR-006/ADR-003/auth.md line by line: session/pty/shell/pipe (translation bridge + the derivation contract rules 1-6), git exec (full grammar, no-oracle validation, collapsed denial byte order), route-1 bridge direct rows, the full -R listen flow + forwarded-tcpip data leg, per-build decline matrices, identity observed at every seam, bounded bookkeeping, and the uniform safe tracing vocabulary. All four decomposition-time interpretations confirmed. Fixed inline (this gate): - install_forwarded_surface pre-checks the tunnel/forwarded op — alkcall's register silently replaces an existing entry, so a second adapter over the same shared seam registry silently stole the first adapter's arrival routes; now a failed construction (AdapterError::ListenRegistration), pinned by a new wire row - git take drops a stored pty entry (combined-build stale state) - doc hygiene: channel_cap release ordering, the session-row scoping of the handler Drop, the cancel-order recording, the post-cancel arrival caveat, the exit-chunk ADR lineage cross-ref, the stale lib.rs wave-state doc, the superseded dispatch test name, and mapping.md's listen-resource wording (address verbatim + tcp port) Booked for wave 4: the git row's status-before-close honored by scheduling not construction (needs a seam/shape change), the channel-not-held wire test rows, the tty coverage minors (cancel path, Closed/Break shape, stdout write deadline), and a defensive ListenRouter token-collision pre-check. Verified: cargo test (default) + --all-features (173 rows, 13 binaries), clippy --all-targets -D warnings ×2 (default + --all-features), fmt --check, cargo doc --no-deps (0 warnings), taskgraph validate (26 tasks).
18 KiB
status, last_updated
| status | last_updated |
|---|---|
| reviewed | 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 — 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 |
per fabric arrival the door opens a forwarded-tcpip channel toward the vanilla client (a vanilla client never initiates this type — 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; correlation-miss → 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 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 (+ both cancel forms) — gate on
tunnel (unfeatured declines both the listen requests and the data
channels). The served flow (w3-tunnel-listen, decidable against
alktunnels 0.4.1's actual surfaces 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_hostparity), so the door passes through), with the tcp port appended (host:port;0stays0— 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 sshdGatewayPortsposture) 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 u32out-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) — recorded as the flow's one inline serving step. - The reply contract: the minted
listentoken (alkcall ADR-049 amendment 3 establisher field) is REQUIRED — a producer that minted no token (or noboundreport) 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 u32out-param (port = 0→ the actually-bound number — sshd'sallocated_listen_portreply parity; theclient.mdlifecycle primitive). - The door-side arrival surface — the fabric's bind-first accept
dispatcher (
make_bindfirst_accept_handler) dials thetunnel/forwardedop toward the door per arrival (theForwardedOpenerclosure); the door serves it through the alkcall openable registered into the shared per-connection registry — the serve-config'sTunnelListenSeam(thew3-seam-configsecond course-correction: the assembly'sArc<ChannelClient>alone carries no inbound-op serving loop; the seam fronts theServingConfig.registrythe assembly wired at the leg'sfrom_connection_with_serving). The registration runs atSshAdapter::new(a failure is a failed construction —AdapterError::ListenRegistration); alkcall's own gates carry the per-identity cap + thetunnel:forwardedscope per arrival, then the crate's arrival pump takes over. - The arrival pump — per the
forwarded-tcpiprow above; unix substrate arrivals (a mintedstreamlocal_forwardroute) openforwarded-streamlocal@openssh.comwith the path echoed (russh's serverHandlecarries 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
channelsstore) matches cancels by (the wire endpoint as the request carried it, the bound port — sshd'sc->path == host && listening_port == portparity; 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),falsefor an unknown pair. The listen count is a bounded resource: a dedicated counter against the sameLimits::max_concurrent_channelsceiling (the listener/channels mix stays unconstrained; both stay bounded — 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, 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 (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:
code ≥ 0→exit-status(N).- 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_dumpedis never asserted — alktty discardsExitStatus::core_dumped(), so nothing better is knowable from the chunk, and SSH'sexit-signalbool stays false. (POC-3 §D.2 observed the full derivation live: signal TERM →−15→exit-signal("TERM").) −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, notexit-signal("HUP"). If the wait-failure vs SIGHUP distinction ever becomes load-bearing, alktty's additiveexit_errorcontrol type is the extension seam (recorded in alktty's own ADR); alkssh derives from whatever that carries, with no contract change here.- Non-Unix: no signal termination exists;
−1is the only negative that can appear → rule 3. - 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). Faithfulexit-signalthere 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 (thesignalrow note) is backend semantics, not a mapping detail. - Unmapped negatives → rule 3. The convention's producers are
−1and−signumonly; a negative value with no entry in the signal-name table (defensive case — the mapping never invents a name) reportsexit-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 inoverview.md. - alkcall bridge leg (when the family half is remote): channels-layer
reassembly cap 1 MiB/channel; ADR-034 grants
alk/sshstream_types[0, 1]because SSH multiplexes internally. - Honest advertisement:
Config::methodsmirrors 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 + 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
(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/sshstream_types) client.md(the same door, client-side)