Files
alkssh/docs/research/mapping-table.md
T
glm-5.3-flash 1a40bfb498 docs(architecture): OQ-10 resolved as a decided mapping contract; tracker statements made accurate
- OQ-10 resolved: alkssh derives SSH exit-signal from the alktty exit
  chunk's negative code (−N = signal N, pipe path) server-side; −1
  (alktty's could-not-determine sentinel) → exit-status(255), never a
  signal claim; core_dumped never asserted in v1; no alktty wire change
- new verified backend finding recorded: portable-pty 0.9 reduces
  PTY-mode signal termination to a plain exit code 1 (signal number
  unrecoverable through its trait surface) — faithful exit-signal on
  the PTY row is an alktty/portable-pty upstream ask, not an alkssh one
- research docs (poc-3 §D.2, pocs.md, mapping-table rows) restated
  accurately instead of carrying inline correction scaffolding

Verification: cargo doc --no-deps earlier this session (docs-only change)
2026-10-27 14:19:02 +00:00

14 KiB
Raw Blame History

Mapping table — SSH channel types × family protocol seams

Status: Draft (phase 0). The crate's spine. Every row maps a russh server-side entry point onto exactly one family protocol seam plus exactly one cargo feature. Names below are feature-name candidates, not decisions — the feature graph is phase 1 ADR territory. All downstream surfaces verified against published sibling source (alktty 0.8.1, alktunnels, alksocks, alkgit 0.1.0, alkcall 0.10).

POC-3 validated the load-bearing rows against the published crates (poc-3-mapping-bakeoff.md): git exec, SOCKS-in-channel, direct-tcpip (route 1), and session/pty all work end to end with vanilla clients and zero seam fixups. Three row corrections from that run are marked [POC-3 corrected] below.

The shape that makes this work

Every family protocol crate already exposes a generic duplex-stream or Connection-serving entry — no crate needs to know SSH exists:

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, T: AsyncRead+AsyncWrite, limits, backend traits)
alksocks serve_socks5_over (T: AsyncRead+AsyncWrite+Unpin, dial, egress, accept, can_bind)
alktunnels — no serve-over-stream entry; see absence note consumer wrappers / open-op registrations instead

russh channels convert to duplex via Channel::{into_stream, split, make_reader/make_writer} (ChannelStream/com halves). So each mapping is: accept-or-decline the channel open (ChannelOpenHandle; dropping it auto-rejects — russh's default is already fail-closed), pump/serve the channel through the seam, map the request-level extras.

Channel-type rows

SSH channel type Vanilla use Feature (candidate) Family seam Notes
session → pty_request+shell_request ssh host interactive shell session alktty producer: NegotiateRequest constructed locally → drive_session (or pre-allocated variant) pty_request args → TerminalParamsWire (term/cols/rows/pixels; SSH modes → modes JSON reserved); shell case needs a cmd convention (OQ); PTY mode merges stderr into stdout (kernel PTY). [POC-3 corrected] the data-plane mapping is a translation bridge, not a byte pump: alktty's plane is chunk-framed both directions (stdin/stdout/stderr + CTRL_IN/CTRL_OUT chunks), so alkssh encodes SSH data→stdin chunks and decodes stdout→data, stderr→extended_data, exit chunk→exit-status/exit-signal. The exit chunk encodes pipe-path signal termination as the negative code −N (alktty ADR-004's convention — pipe.rs::exit_code_from); the PTY path reduces signal termination to a plain exit code (portable-pty's exit surface), so PTY-mode exit-signal fidelity is an alktty-backend question
session → exec_request (non-git command) ssh host "cmd" session alktty pipe mode: terminal: None, separate stderr stderr flows back as SSH extended_data code 1 — only valid without PTY
session → exec_request/subsystem_request git git push/clone ssh:// git alkgit: parse command → Service + repo (same grammar as preamble.rs, without \0host=) → serve_duplex_resolved(repo_id, identity, service, duplex, ...) Process-mode dialect is correct (SSH supplies service+repo out of band; server speaks first = advertisement). [POC-3 corrected] git over SSH is exec-only: a subsystem request carries the service name but no repo id, so even a git-service subsystem must decline — vanilla git always executes git-upload-pack '/repo'. POC-3: vanilla clone/push/second-push through the published seam + substrate composition, exit-status in both directions
session → SOCKS5-in-channel (n/a — retired) dynamic-forward retired (ADR-003 §1a) was alksocks: serve_socks5_over Post-architecture retirement: no consumer is ever the family-native client shape this row served. Vanilla ssh -D does SOCKS client-side and opens plain direct-tcpip (POC-3 correction 2 → the tunnel row); the family -D behavior is a client-local alksocks door over the alkssh client's direct-tcpip opens (assembly composition). POC-3 stage B remains the evidence that SOCKS-in-channel is feasible if a family client ever wants a remote SOCKS door (back via explicit ADR). [ADR-003 amended]
direct-tcpip ssh -L tunnel alktunnels (two routes, below) Target = SubstrateAddr::HostPort/Path; Substrate = Tcp/Unix
forwarded-tcpip ssh -R (data channel) tunnel alktunnels: register_tunnel_forwarded_openable's ForwardedHandler(params, BiStream) shape — in the adapter, hand the russh channel duplex to the handler listen flow via global-request rows below
direct-streamlocal unix-socket forward tunnel same SubstrateAddr::Path treatment unix-only; decline on other platforms (compile-time feature + runtime decline)
x11 X forwarding never (no feature) — decline, always; not even feature-gated initially (OQ: revisit years later if a use case arrives)
sftp (subsystem_request("sftp")) sftp clients sftp (future) alkfs (deferred, family ADR-002) The declinable placeholder — the feature name exists in the feature graph, decline until alkfs lands
auth-agent (agent_request) ssh-agent forwarding never — decline; family holds keys in alkvault — forwarding agents to remote sessions is the anti-pattern this posture rejects

Notes: the rows deliberately split one channel type (session) across three features, because vanilla SSH multiplexes everything through a session channel: a git command (exec_request), a SOCKS proxy client, a shell, a pipe-mode command. The failure matrix this produces:

  • git-only build: declines pty/shell, declines plain exec, declines SOCKS-in-channel; serves only the git-parseable exec/subsystem path.
  • session-only build: serves pty/shell/pipe-exec; the git-parseable exec path declines (parsing to a git service is gated on git).
  • No features: every session channel declines at open; every unfeatured channel type declines at open.

Dispatch is parse-then-route by command shape: parsing to a git service and repo requires git; otherwise the alktty pipe/pty path requires session. Parsing happens before dispatch and before any resource allocation (a leaked channel alloc is a capability leak — decline first). (Post-architecture: the former SOCKS-detect dispatch arm is retired with the dynamic-forward feature — ADR-003 §1a / ADR-006 amendment; request-less session channels decline.) Grammar/ precedence per ADR-006.

Global-request rows (tcpip_forward / streamlocal_forward) — the -R listen flow

Handler::tcpip_forward(address, &mut port) -> bool + per-open channel_open_forwarded_tcpip. Consumer-side alktunnels: TunnelListener, open_reverse_channel; the forwarded data channel maps to the ForwardedHandler route above. Dispositions follow the tunnel feature (decline both listen and data when tunnel absent). port = 0 (server picks) interacts with the &mut u32 out-param — spec detail deferred to phase 1.

Request-level mapping on a session channel

SSH request Family mapping
pty_request NegotiateRequest.tty = TerminalParamsWire{term, cols, rows, pixel_w, pixel_h, modes}
shell_request NegotiateRequest.cmd per the backend's shell convention (OQ — probably cmd + cwd/env from assembly policy, not from the wire)
exec_request Parse command → git/service dispatch or alktty pipe cmd; unknown commands fail with session.channel_failure (parse-then-decline, no resource alloc)
subsystem_request sftp → decline until sftp feature serves alkfs; git subsystem names → same parse as exec; anything else → decline
env_request Decline by default. Wire-supplied env is not family identity config; TtyParams.env comes from assembly. (OQ: allowlist shape, e.g. LANG, later.)
window_change_request TtyControlHandle::resize(cols, rows, pixel_w, pixel_h)
signal TtyControlHandle::signal(name) (alktty's signal_from_name accepts the HUP/INT/TERM/... names verbatim)
data / extended_data(1) channel duplex in/out; stderr only meaningful in pipe mode (PTY merges)
exit_status from TtyHandle.exit_code (TtySession.wait() semantics: i32; negative = signal-terminated on the pipe path — pipe.rs::exit_code_from maps Unix ExitStatus::signal() to −sig; the PTY path reduces signal termination to a plain exit code, see the exit_signal row) — alkssh sends the SSH exit-status request before channel close (POC-3: proven both directions via the cloned Session::handle() from the spawned serving task); −1 (alktty's could-not-determine sentinel, alktty ADR-004 §4) → exit-status(255)
exit_signal derived server-side from the exit chunk's negative code — −N → name via a Unix signal-number→name table; core_dumped never asserted (alktty discards ExitStatus::core_dumped()); no chunk change is needed or proposed (the encoding is alktty's documented contract, alknet ADR-055 lineage). POC-3 observed −15 → exit-signal("TERM") live on the pipe path. PTY path: portable-pty 0.9's ExitStatus conversion yields code = status.code().unwrap_or(1) (1) for signal-terminated children, keeping the signal only in a separate humanized (strsignal) string field alktty's waiter discards — faithful exit-signal on the PTY row needs the alktty backend to conform to its own ADR convention (upstream ask: alktty/portable-pty trackers; the alkssh contract adapts automatically)

Data-plane / limits contract

  • russh windows: Config::window_size, maximum_packet_size, channel_buffer_size cap per-channel memory; Limits caps rekey volume; inactivity_timeout the session wall clock — the family bounded-resources convention maps onto these knobs plus the crate's own max-concurrent-channels (per session) and the per-length parse limits on exec commands.
  • alkcall bridge leg (when the family half is remote): channels-layer reassembly cap 1 MiB/channel, chunk max 16 MiB, per-identity channel-count policy — an SSH channel bridged over alk/channels rides one reassembled byte stream per SSH channel; SSH's own multiplexing is why ADR-034 grants alk/ssh only stream_types [0, 1] ("SSH multiplexes internally, including its own control").
  • Honest advertisement: Config::methods mirrors implemented auth; an unfeatured channel type is declined at open, never half-opened. Vanilla clients see a clean failure — which is the contract.

alktunnels absence note

alktunnels has no standalone serve_*_over(stream) producer entry (unlike alktty/alkgit/alksocks). The direct-tcpip mapping therefore has two legitimate routes, and choosing between them is a phase 1 decision:

  1. Adapter-side bridge: alkssh is itself an alk/channels client (it holds a ChannelClient into the family fabric) and calls TunnelSession::open_direct(client, target, substrate) / TunnelSession::adopt(...) directly, pumping take_halves()/ stream_halves() against the russh channel. (alksocks' composed::direct_egress is the family precedent for exactly this bridging.)
  2. Registry-side bridge: within an alkcall serving endpoint, register_tunnel_direct_openable with a DialFn — but that puts alkssh-shaped work in the serving endpoint's registry, which inverts the door posture. Likely rejected on composability grounds; recorded here for the ADR to decide explicitly.

Topology: where the alk/ssh ALPN itself lives

Three alkcall ADR constraints collide productively:

  • ADR-004 reserves alk/ssh → SshAdapter (custom protocol).
  • ADR-034's per-ALPN table: alk/ssh gets stream_types [0, 1] — SSH multiplexes internally, the channels layer is byte-blind to it.
  • ADR-042/051: "The hub never runs a handler for alk/tty, alk/ssh, or alk/tunnel" — alk/ssh channels relay payload-blind between spokes; ACL fires at channel/open.

ADR-009 additionally sketches a browser route: a WebTransport session to /alk/ssh where each bidi stream is handed to SshAdapter::handle as a Connection, with a WASM SSH parser speaking SSH over it.

Reading: alk/ssh is plausibly SSH-over-channels — the raw vanilla- SSH byte stream carried between alkssh instances (edge termination at one end, mapped serving at the other), reused by the browser route. Whether the edge-terminated topology (russh at the edge, family seams in-process or via alk/channels hops) ever needs to speak alk/ssh itself, and what SshAdapter::handle(connection) does for the browser route (it would be alkssh's russh wrapper serving an inbound alkcall connection), is the phase 0 topology question. POC-3's design will attempt the simplest composition; the decision records itself in the phase 1 ADR-001 (ALPN pin, ledger rule 1).

Open questions seeded for phase 1

  • Session-channel dispatch precedence + the exec-command grammar (git parse vs pipe cmd) — parse-then-fail-closed contract.
  • Shell command convention for shell_request (assembly policy vs wire-supplied; wd/env provenance).
  • env_request policy (decline-only vs allowlist).
  • OQ: OpenSSH certificate auth (auth_openssh_certificate) — decline until a use case.
  • OQ: username disposition (ignored entirely vs a routing hint).
  • Feature-graph names + the session-vs-git-vs-dynamic-forward split inside one channel type.
  • The alk/ssh topology question above.
  • Windows: direct-streamlocal is unix-only; pty on Windows targets depends on alktty backend capability (decline rather than degrade).

References

  • russh.md §server surface (handler inventory, Config)
  • auth-bridge.md (identity arrives before any channel opens)
  • alkcall ADRs 002, 004, 009, 034 (+035 chunk header), 037, 042, 051
  • Sibling sources: alktty src/adapter.rs/src/negotiation.rs/ src/backend.rs/src/channels.rs/src/session.rs, alktunnels src/consumer.rs/src/producer.rs/src/params.rs, alksocks src/producer.rs/src/composed/mod.rs, alkgit src/duplex.rs/src/preamble.rs/src/substrate.rs/src/session.rs