docs(research): POC-3 findings — mapping bake-off validated end to end

All four stages ran against vanilla clients (OpenSSH -L/-R, real git,
real curl) and the published crates.io seams with zero seam fixups
(51/51 checks, reproduced twice; POC source /workspace/alkssh-poc3):

- stage A: vanilla git push/clone/second-push through russh exec
  parse -> alkgit serve_duplex_resolved + alkgit-substrate mem
  composition; exit-status proven both directions; denial is
  repo-blind before any protocol byte
- stage B: alkgit serve_socks5_over over a session channel with the
  shipped tcp_dial; real curl through the proxy; 8 MiB throughput
- stage C: direct-tcpip via alktunnels route 1 (TunnelSession::
  open_direct over a real alkcall ChannelClient + local_dial);
  vanilla ssh -L + curl; -R global request declines (honest
  advertisement); opener identity rides the transport into the
  establisher witness (CF-005)
- stage D: session/pty mapping via a translation bridge (SSH bytes
  <-> alktty chunk plane) and drive_session_pre_negotiated over a
  real PTY: pty params, window-change resize, pipe stderr as
  extended_data, exit chunk -> exit-status/exit-signal

Corrections recorded in mapping-table.md (git subsystem over SSH is
exec-only; vanilla ssh -D speaks SOCKS client-side and opens
direct-tcpip; the alktty bridge is chunk-framed both ways with the
exit-chunk signal-identity gap), plus the load-bearing russh 0.64.1
runtime facts in russh.md (inline handler dispatch — serving paths
must spawn; Session::handle() exit reporting; client drain-to-target
message routing).

Verification: cargo test / clippy -D warnings / fmt --check / doc on
alkssh (docs-only change); POC crate builds warning-free and runs
overall=true.
This commit is contained in:
glm-5.3-flash committed 2026-10-25 14:44:35 +00:00
1 parent 06f546c72a
commit 9fab51d4a8
4 files changed
+422 -8

No files matched your search

+13 -7
View File
@@ -1,12 +1,18 @@
# Mapping table — SSH channel types → family protocol seams
# Mapping table — SSH channel types × family protocol seams
Status: Draft (phase 0 research). The crate's spine. Every row maps a
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
@@ -30,10 +36,10 @@ request-level extras.
| 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) |
| `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. New OQ: the exit chunk carries no signal identity (negative code only) |
| `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). `subsystem` names also map here (`git-upload-pack` etc. arrive as either); unknown subsystems decline |
| `session` → SOCS5-in-channel | `ssh -D` | `dynamic-forward` | alksocks: `serve_socks5_over(channel_duplex, dial, egress, accept, can_bind)` | alkssh embeds `Socks5Producer`-shaped config from the assembly; `DialFn`/`EgressFactory` injected at assembly |
| `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 | `ssh -D` | `dynamic-forward` | alksocks: `serve_socks5_over(channel_duplex, dial, egress, accept, can_bind)` | alkssh embeds `Socks5Producer`-shaped config from the assembly; `DialFn`/`EgressFactory` injected at assembly. **[POC-3 corrected]** this row is the family-native client shape — a vanilla `ssh -D` does its SOCKS5 client-side and opens plain `direct-tcpip` channels (the `tunnel` row above), so the two rows split by client shape, not just feature. POC-3: SOCKS-detect (bounded first-byte sniff) + real curl through the session channel |
| `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) |
@@ -83,8 +89,8 @@ spec detail deferred to phase 1.
| `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) — alkssh sends the SSH `exit-status` request before channel close |
| `exit_signal` | Only reachable in signal-termination cases; mapped from the same exit-code path (OQ: exact encoding) |
| `exit_status` | from `TtyHandle.exit_code` (`TtySession.wait()` semantics: i32; negative = signal-terminated) — 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) |
| `exit_signal` | Only reachable in signal-termination cases; mapped from the same exit-code path (OQ: exact encoding). **[POC-3]** the alktty exit chunk is `{"type":"exit","code":N}` — a negative code implies signal termination but carries no *name*, so the faithful `exit-signal` mapping needs alktty to widen the chunk or an assembly-policy default |
## Data-plane / limits contract
+375
View File
@@ -0,0 +1,375 @@
# POC-3 findings — the mapping bake-off: channel bridge through real sibling seams
Status: Complete (phase 0). POC source: `/workspace/alkssh-poc3`
(standalone crate, disposable — never merges). All four stages ran
against **vanilla clients** (OpenSSH 9.6 `ssh -L`/`-R`, real `git`
2.43, real `curl` 8.5) and the **published crates.io forms** of every
sibling — no path dependencies, no in-family forks. The doc records
the evidence: three corrections to `mapping-table.md`, the russh
0.64.1 runtime facts phase 1 must code around (the inline-dispatch
rule), the identity/scope live check, and the OQ seeds.
## Setup exercised
- russh **0.64.1** (crates.io, ring backend — the same published tree
POC-1/POC-2 pinned; re-verified still the newest release). Same
dependency tree as both prior POCs across all four stages.
- Sibling seams, published forms: **alkgit 0.1.0 +
alkgit-substrate 0.1.0** (the shipped backend composition, with
`alkstore-mem` engine + `alkblobs::Store::mem_only()` object pool —
the substrate's own mem-assembly shape, `SubstrateAssembly::mem`),
**alksocks 0.3.1** (`local` feature — the shipped `tcp_dial`),
**alktunnels 0.4.1** (`local` feature — the shipped `local_dial`),
**alktty 0.8.1** (`local` feature — the shipped `LocalTtyBackend`
over portable-pty), **alkcall 0.10.1** (channels + identity types).
- Servers per stage were built as **feature-shaped builds**: stage A's
server serves only git-parseable exec (a git-only build), stage B's
only SOCKS-on-session (a dynamic-forward shape), stage C's only
direct-tcpip (an egress-tunnel shape that also declines the `-R`
global request), stage D's only pty/shell/pipe-exec (a session
shape). The auth loopback is POC-1's verbatim shape (fingerprint
registry fed from `ssh-keygen -lf`, `auth_publickey_offered`
accept-and-decide, all other methods declined).
- POC mechanics: single binary, stage functions returning check
reports (POC-1's harness); full clean runs reproduced twice.
## Stage A — git exec mapping (24/24 PASS)
### 1. Vanilla git works through the seam with zero seam fixups
`git push` → `git clone` → rev parity → second push (have/star
negotiation on the populated repo) all succeeded through:
```
vanilla git (GIT_SSH_COMMAND) → OpenSSH → russh server
→ exec_request parse (the alkssh-shaped grammar)
→ serve_duplex_resolved(repo_id, identity, service, channel.into_stream(),
Limits, registry, refs, packgen, ingest, false, pipeline)
```
with the **real alkgit-substrate mem composition** as the backend —
registry/ref-rows/packgen/ingest, an in-memory alkstore engine, and an
in-memory object pool. The published alkgit seam needed no wrapper, no
adapter glue beyond the command parse, and no dialect nudges. The
hypothesis that phase-1's mapping ADR gated on (stage A gates the
ADR work) is confirmed.
### 2. The SSH exec-command grammar (the alkssh-side parse)
What real OpenSSH git actually sends: `git-upload-pack '/owner/demo'`
— single-quoted repo path with a leading `/` (the parse strips quotes
and the leading slash; no `\0host=` rides the SSH exec — the
mapping-table's "same grammar as preamble.rs, without `\0host=`" is
exactly right). The POC grammar table (all rows verified):
| exec command | verdict |
|---|---|
| `git-upload-pack '/r'` / `git-receive-pack '/r'` / `git-upload-pack /r` | git parse, dispatched |
| `git-send-pack '/r'`, `git-upload-archive '/r'` | declined (unknown service, honest surface) |
| `git-upload-pack` (service-only, no repo) | declined — SSH exec must carry the repo |
| `uptime` (non-git) | declined parse-then-fail, no resource alloc |
### 3. Subsystem mapping row — corrected at the detail level
`mapping-table.md` says git subsystem names "arrive as either" (exec
or subsystem). On the SSH wire a subsystem request carries **only the
name** — no repository is available to resolve. The POC proves the
forced decline: `subsystem("git-upload-pack")` and `subsystem("sftp")`
both fail cleanly with a channel failure, with the finding recorded:
*over SSH, git always uses exec; a subsystem git request can never
dispatch (no repo id), so the correct mapping is decline-with-reason*.
The mapping-table row needs the detail fix.
### 4. russh runtime facts phase 1 must code around (new, load-bearing)
- **Server handler callbacks run inline inside the session's packet
loop.** Blocking inside `exec_request`/`pty_request`/`shell_request`
(or any handler) suspends packet processing for the *entire
connection* — the awaited channel data is only routed by that same
loop, so an inline serve **deadlocks** (receive-pack pushes bytes
immediately after the exec request). Confirmed by construction: the
serving entry is spawned and the handler returns immediately. This
is the binding implementation constraint for the alkssh adapter:
**every serving path is `tokio::spawn` + handler returns fast**.
- The spawned task gets exit reporting through
**`Session::handle()`** — a cloneable server-side `Handle`
carrying `data` / `extended_data` / `eof` / `close` /
`exit_status_request` / `exit_signal_request`. The exit-status
mapping row is implementable exactly as the table prescribes: send
`exit-status` before `close` from the serving task.
- The accepted-but-held `Channel` can be stored in the handler and
taken later in the request callback (`channels.remove(&id)` →
`into_stream()` in the spawned task) — no `wait()` anywhere
(POC-1's caveat holds server-side too).
### 5. Exit-status fidelity, denial fidelity, vanilla diagnostics
- Vanilla git paths exit 0 (the exit-status request rode the wire);
an unknown repo produces the collapsed generic denial *before any
protocol byte* (no existence oracle) and vanilla git reports
`fatal: remote error: access denied: owner/absent`, exit 128.
- A russh client exec against the absent repo observed
**`ExitStatus{1}`** after the denial band — the exit-status flow is
proven in both directions, including the failure leg.
- The git-only build's decline matrix held under vanilla ssh:
`ssh host uptime` failed with the parse-then-fail decline logged;
`pty_request`/`shell_request`/`env_request` declined with failure
replies (russh client observed `ChannelMsg::Failure`); the fail-
closed feature matrix of the mapping table is live end to end.
## Stage B — SOCKS5-in-channel (6/6 PASS)
### 1. `serve_socks5_over` works over a russh session channel
Construction: server accepts the session channel, spawns the
**SOCKS-detect** (first-byte sniff via `BufReader::fill_buf`: `0x05`
= RFC 1928 greeting), and routes to
`serve_socks5_over(stream, dial, None, None, false)` with the shipped
`alksocks::tcp_dial()` (the `local` feature's DialFn, resolving
domain-form targets). Client side: a local bridge listener pumps each
accepted TCP socket bidirectionally into one authenticated session
channel. **Real `curl --socks5-hostname` through the whole chain**
returned the endpoint body byte-identically — CONNECT over the SSH
wire, dialed at the server through the family seam.
### 2. OpenSSH's own `-D` never speaks SOCKS on the wire — mapping row corrected
This is a **mapping-table correction**: vanilla `ssh -D` performs the
SOCKS5 handshake *client-side* and then opens plain **`direct-tcpip`
channels** per connection. The `session` → SOCKS-in-channel row is
therefore the family-native client shape (an embedder/alktty-style
client speaking SOCKS5 on the channel), not what a vanilla `ssh -D`
emits — vanilla `-D` traffic takes the stage-C row (direct-tcpip →
egress). Both rows are real; the table should say which client shape
each serves. (POC evidence: this stage's proxy uses the session
channel; stage C's `-L`-style row serves forwarded and SOCKS-originated
direct-tcpip alike.)
### 3. The dispatch split is real — and needs a bounded detect window
Parse-then-route (stage A) and SOCKS-detect (this stage) are the two
session-channel dispatch modes the mapping table predicted, and both
work. The detect runs in a spawned task with a bounded window (500 ms
in the POC): the first byte decides the route; a silent channel is
declined. Two consequences for the ADR: (a) the detect must not block
inline (the §A.4 deadlock rule applies — the sniffed byte arrives via
the same loop that awaits it); (b) the *detect window* is a design
shape to pin — an OQ seed: whether the dispatch is a first-byte sniff,
an in-band preamble, or an exec-request marker chosen by the assembly.
### 4. Refusals are RFC-faithful through the seam
A CONNECT to a dead port: the dial seam returns
`SocksDialError::Refused` → alksocks replies the mapped RFC code →
curl exits **97** (proxy-connection-not-allowed class) with no data
crossing. Backpressure/throughput: **8 MiB through the session-channel
proxy in ~585 ms** (in-process route; the 1 MiB channels-reassembly
leg is not on this path). Non-SOCKS first byte on a session channel →
declined, logged — no HTTP serving happens on the SOCKS route.
## Stage C — direct-tcpip via alktunnels route 1 (8/8 PASS)
### 1. The bridged route works end to end with the published crates
Route 1 (adapter-side bridge) exercised as the mapping table's route
note describes: the russh server holds a **real `ChannelClient`** into
a family fabric — an in-process `alk/channels` connection whose
serving side (the `ChannelsAdapter` accept end) registers
`ChannelOperations` + `register_tunnel_direct_openable` with the
shipped `alktunnels::local::local_dial()` injected. Per vanilla `ssh
-L` channel:
```
channel_open_direct_tcpip → TunnelSession::open_direct(
&client, SubstrateAddr::HostPort{host, port}, Substrate::Tcp)
→ take_halves() → pump_bidi(channel.into_stream(), read, write)
```
Vanilla `ssh -L` + `curl` to the bridged port returned the endpoint
body byte-identically; **8 MiB through the bridge in ~333 ms**. The
`direct_egress` precedent in alksocks' `composed` feature is the same
shape — the family precedent survives contact with the SSH door.
### 2. Failure shapes at both ends of the bridge
- Fabric open failure → the SSH channel is rejected **explicitly**
(`ChannelOpenHandle::reject(ConnectFailed)`); accept is possible
only after `open_direct` succeeds because `accept(mut self)`
consumes the handle — so the handle moves into the spawned task.
A vanilla client dials a dead target and gets a clean curl failure
(exit 56) instead of a hang.
- The `-R` ingress direction is **declined** on this egress-only
build: `Handler::tcpip_forward` returns false (the global request
refuses), vanilla `ssh -R` dies with `remote port forwarding
failed for listen port` (exit 255), and the event log records the
refusal — honest advertisement, gate-before-wire.
- Unfeatured session-channel opens drop (auto-reject) unchanged from
POC-1 on this and the other stage builds.
### 3. Identity/scope propagation observed live
The direct establisher's identity witness recorded **`u-writer`** —
the SSH-authenticated identity rides the transport connection into
the channels layer (CF-005 propagation), and the establisher's
per-call auth (CF-006) sees it. The visible-surface invariant's
alkcall half (opener identity resolved from the authenticated
transport) is confirmed inside the mapping, not just documented.
### 4. Wiring recipe worth carrying into phase 1
The in-process fabric leg (worth recording because it was the POC's
only real lift, and the alkforge assembly will build exactly this):
`OperationRegistry` + `InstallChannelZero` hook (spawn: accept channel
0's `accept_bi`, `split_single_stream`, `set_identity(caller)`,
`CallConnection::new_single_stream`, register ops + openables,
`Dispatcher::serve_single_stream`) on the accept side;
`CoreConnection::from_bidi` on both ends of a `tokio::io::duplex`
transport; `ChannelClient::from_connection` on the alkssh side with
the identity attached before dialing.
## Stage D — session/pty mapping (13/13 PASS)
### 1. The mapping is a translation bridge, not a pump — mapping-table note confirmed with a shape
alktty's data plane is **chunk-framed in both directions**
(STREAM_STDIN/STDOUT/STDERR/CTRL_IN/CTRL_OUT, 5-byte header + payload
— `alktty::wire`). The SSH mapping therefore wraps the channel with
two small adapters: SSH data/EOF → stdin chunks (+ zero-length EOF
sentinel); alktty stdout chunks → SSH data, stderr chunks → SSH
`extended_data(1)`, ctrl-out exit chunk → SSH `exit-status` request +
close. `drive_session_pre_negotiated(writer, reader, NegotiateRequest
built locally, backends, ownership, identity)` — the exact producer
construct the mapping table names — then runs the real session. Proven
against a **real PTY** (`LocalTtyBackend` over portable-pty):
- `pty_request` args → `TerminalParamsWire` (`term`/cols/rows/
pixels; SSH modes → the reserved `{}` OQ-44 JSON) — live: bash on
the PTY answered `stty size` with the requested 24×80.
- Keystrokes ride in, PTY echo and shell output ride out (real bash
prompt captured in the run output).
- `window_change_request` → `ControlMessage::Resize` as a CTRL_IN
chunk → the PTY resized: `stty size` flipped to `40 120` after the
client sent `window_change(120, 40)` — the mapping row proven
behaviorally.
- Pipe-mode `exec_request`: stdout rode SSH data, stderr rode SSH
`extended_data(1)` (client received both distinctly), exit
chunk → `exit-status(0)`. The PTY/pipe stderr split the mapping
table describes (PTY merges, pipe separates) is exactly what the
local backend produces.
- `signal(TERM)` → `ControlMessage::Signal` CTRL_IN chunk → the
backend killed the session; the exit chunk carried the negative
code and the SSH reply was an **`exit-signal`** request — the
mapping-table's "only reachable in signal-termination cases" row,
now with live wire evidence.
- The git-parseable exec declined in the session-only build
(parse-then-route: the git parse is the `git` feature's gate) —
the feature-split failure matrix holds in this build too.
### 2. The signal-identity gap (new OQ)
The exit chunk is `{"type":"exit","code":N}` — a negative code says
*signal-terminated* but **carries no signal name**. The adapter cannot
map SSH `exit-signal` faithfully from the chunk alone (the POC's check
asserts TERM because the *request* was TERM, not because the chunk
said so). Options for the ADR: alktty widens the exit chunk with the
signal name, or alkssh fixes the exit-signal name by assembly policy.
Also recorded: pipe-mode signals are **pid-only** (a SIGTERM to `sh -c
"sleep 60"` leaves the orphaned `sleep` holding the stdout pipe — no
exit chunk until the whole group dies); PTY-mode signals reach the
process group (correct terminal semantics). The signal row's fidelity
therefore depends on the tty/pipe mode split the table already notes.
### 3. Client-visible mechanics (russh 0.64 API facts from this stage)
- russh client has `Channel::exec()` (there is no `request_exec`);
pty/subsystem request *failures* surface as `ChannelMsg::Failure`
via `wait()` — **not** as `Err` from the request call; the caller
must read the channel until Success/Failure.
- `window_change`/`signal` are fire-and-forget (no want-reply), and
must be sent **before** `into_stream()` (after the conversion the
Channel is consumed) — server-side they carry no SSH reply, so the
handler must not send one.
- The server handler loop dispatches `CHANNEL_SUCCESS`-style replies
only through explicit handler calls; request-level failure replies
in stage A/D all used `Session::channel_failure`.
- The channel message loop client-side delivers interleaved
Data/ExtendedData/Success/ExitStatus on one queue — drain to the
expected message, `continue` past the rest (the POC hit the
break-on-first-message trap twice; noted so the alkssh consumer
half doesn't).
## Hypothesis verdicts (pocs.md §POC-3)
- "one-seam-per-mapping hypothesis" — **confirmed** across all four
seams with zero seam fixups; the mapping glue is adapter-side
(command parse, chunk translation, handle plumbing).
- "parse-then-route dispatch" — **confirmed** (stage A), with the
SOCKS-detect variant confirmed in stage B plus the bounded-window
and spawn-not-block constraints.
- "EOF/close/signal fidelity end to end" — **confirmed**: exec →
serve → exit-status (0 and 1 legs), exit-signal for signal
termination, close mapping, refusal diagnostics at both bridge ends.
- "stderr semantics in pipe vs PTY mode" — **confirmed** exactly as
the table describes (separate extended_data in pipe; merged under PTY).
- "throughput sanity" — **confirmed** with margin: 8 MiB in ~0.3–0.6 s
per bridged route.
- "stage D may surface PTY-merge stderr surprises that reshape the
request-level mapping rows" — partially: no surprises in the
PTY/pipe split, but the **signal-identity gap** and the
**translation-bridge shape** (two adapters, not a pump) do reshape
the session row's implementation notes.
## Corrections recorded against phase 0 research
1. `mapping-table.md` §channel-type rows: the git **subsystem** claim
("git-upload-pack etc. arrive as either") is wrong over SSH —
subsystem cannot carry the repo id, so even a git service name must
decline. Git over SSH is exec-only.
2. `mapping-table.md`: `ssh -D` from a vanilla client does **not**
produce SOCKS5-in-channel traffic (OpenSSH does SOCKS client-side
and opens `direct-tcpip`). The `dynamic-forward` row serves
family-native clients; vanilla `-D` rides the `tunnel` row.
3. `mapping-table.md` session/pty row implementation notes: the
alktty bridge is a **translation bridge** (chunk framing both
directions), not a byte pump; plus the exit-chunk signal-identity
gap above.
## OQ seeds carried forward
1. Session-channel dispatch shape for SOCKS (first-byte sniff vs
in-band preamble vs exec marker; the detect window's bound).
2. Exit-chunk signal identity (alktty exit chunk widening vs
assembly-policy mapping; pipe-mode pid-only vs PTY process-group
scope).
3. Shell command convention (already seeded; the POC's stand-in —
`bash` for shell, `sh -c <cmd>` for exec — is an explicit assembly
policy, not wire-derived).
4. The `-R` listen flow (forwarded-tcpip mapping): untouched by the
POC beyond the refusal shape; route choice (ingress via
`register_tunnel_forwarded_openable`) is phase 1 work with this
stage's route-1 recipe as the template.
5. The identity-attachment seam (SSH fingerprint → alkcall
`Identity` → transport identity → establisher witness) worked
in-POC; whether the assembly composes these as one configuration
surface is an assembly-layer question to keep explicit.
## Reference run
Full clean run at findings time: **51 checks across 4 stages — 24 + 6
+ 8 + 13, all PASS, `overall=true`, exit 0** (reproduced twice;
second run byte-identical PASS set with different ephemeral ports).
Server-side event logs captured per stage in the run output (the
diagnostics quoted above are verbatim from that run).
## References
- `pocs.md` §POC-3 (plan), `mapping-table.md` (the rows under test —
three corrections recorded above), `poc-1-auth-bridge.md` (the auth
loopback everything rides), `russh.md` (API inventory)
- POC source: `/workspace/alkssh-poc3` (`src/stage_{a_git,b_socks,
c_tunnel,d_tty}.rs`; the fabric/serving recipes and the translation
bridge are the reusable shapes)
- russh 0.64.1, alkgit 0.1.0, alkgit-substrate 0.1.0, alksocks 0.3.1
(`local`), alktunnels 0.4.1 (`local`), alktty 0.8.1 (`local`),
alkcall 0.10.1 (crates.io, ring backend); git 2.43.0, OpenSSH
9.6p1, curl 8.5.0 (vanilla tooling)
+16 -1
View File
@@ -13,6 +13,10 @@ the point of running it.
## POC-1 — auth bridge + fail-closed declines end to end
Findings: `poc-1-auth-bridge.md` (complete — 31/31; all hypotheses
confirmed, one correction to the research docs' russh default-impl
assumption). POC source: `/workspace/alkssh-poc1`.
**Question:** does the russh 0.64-era server auth surface compose with
alkcall's fingerprint-based identity model as cleanly as
`auth-bridge.md` assumes, and does the fail-closed decline contract
@@ -49,6 +53,9 @@ confirms or corrects.
## POC-2 — client-side at-rest key handling (vault-backed storage)
Findings: `poc-2-vault-keys.md` (complete — 39/39; OpenSSH-PEM-inside-
`EncryptedData` confirmed, Signer seam dismissed with evidence).
**Question:** how are held SSH keys stored and used — the vault as
pure at-rest encryption (the key is just another bearer-token-class
secret: opaque bytes, vault-encrypted at rest, decrypted at use), what
@@ -89,6 +96,11 @@ wrapper — confirming the alkhttp ADR-014 analogy end to end.
## POC-3 — the mapping bake-off: channel bridge through a real sibling seam
Findings: `poc-3-mapping-bakeoff.md` (complete — 51/51 across stages
A–D; every hypothesis confirmed, three mapping-table corrections
recorded, plus the russh inline-dispatch rule). POC source:
`/workspace/alkssh-poc3`.
**Question:** does "russh channel duplex → family serve-over-a-stream
entry" actually work against the real published seams — and which of
the mapping-table's route choices survives contact with reality?
@@ -138,7 +150,10 @@ reshape the request-level mapping rows.
path alkforge phase 0 consumes first.
4. Phase 0 completes when the findings docs exist and the OQ list in
`vision.md` §scope questions is either answered by findings or
carried into phase 1's `open-questions.md` as seeded OQs.
carried into phase 1's `open-questions.md` as seeded OQs. POC-1/2/3
findings docs now exist; scope question 5 (mapping-table details)
is answered per `poc-3-mapping-bakeoff.md` §hypothesis verdicts,
and the POC-seeded OQs are listed in each findings doc.
## References
+18
View File
@@ -98,6 +98,24 @@ inventory:
`cancel_streamlocal_forward` (the `-R` listen flow), and
`lookup_dh_gex_group`.
**Dispatch/runtime facts (POC-3, load-bearing for the adapter):** the
session's event loop invokes handler callbacks *inline* — a blocking
handler suspends all packet routing for the connection (channel data
is only routed by that same loop; an inline serve deadlocks against
a peer's follow-up bytes, e.g. a receive-pack push). Every serving
path must `tokio::spawn` and return fast. Exit reporting from spawned
tasks rides `Session::handle()` — a cloneable server-side `Handle`
with `data` / `extended_data(channel, ext, data)` / `eof` / `close` /
`exit_status_request` / `exit_signal_request` (send `exit-status`
before `close`). Client-side: `Channel::exec()` (no `request_exec`),
`window_change`/`signal` are fire-and-forget and must be sent before
`into_stream()`, and request-level failures surface as
`ChannelMsg::Failure`/`Success` on `wait()`, not as `Err` from the
request call; drain past unexpected messages (`continue`), a client
`Handler::check_server_key` default that refuses must be accepted
explicitly. Details and wire evidence:
`poc-3-mapping-bakeoff.md` §A.4/§D.3.
### `server::Config` knobs (bounded-resources posture)
`server_id`, `methods: MethodSet` (advertised auth methods — honest