# POCs: phase 0 validation plan Status: **Complete** (phase 0). All three POCs ran against the current published russh (0.64.1) and passed; the findings docs (`poc-1-auth-bridge.md`, `poc-2-vault-keys.md`, `poc-3-mapping-bakeoff.md`) are canonical and supersede the planned hypothesis wording below wherever they record corrections. POC source is disposable and never merges to main. Mode for all: **standalone** (repos have no code to build on yet), scratch cargo projects at `/workspace/` unless a POC explicitly builds on repo code (then branch mode per `docs/sdd_process.md`). The hypotheses below are what phase 1's ADRs will cite. If a POC refutes a hypothesis, the finding doc records the refutation, which is 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 hold under a real vanilla client? **Construction:** standalone crate. russh server loopback bound to a local listener + vanilla `ssh` (OpenSSH) as the client (the point is to validate against the *vanilla* wire, not just russh's own client). A config-backed `IdentityProvider` stub registered with two ed25519 keys; russh client available as a secondary client for `Signer`-shaped checks. **Validates:** - `auth_publickey` after russh's signature verification → `ssh_key` fingerprint (`HashAlg::Sha256`) string parity with a stub-registered identity; `resolve_from_fingerprint` → `Some` → authenticated; unknown key → clean decline under `auth_rejection_time`/`max_auth_attempts`. - `auth_publickey_offered` disposition (accept-and-let-decide) behaves with OpenSSH's probe-then-sign flow. - Decline matrix: `auth_none`/`password`/`keyboard-interactive` declined (0.64.1 correction: the *default impls already reject* at this russh: only `auth_publickey_offered` defaults to accept; `poc-1-auth-bridge.md` §3). The family contract still requires implementing every hook explicitly, since the pin is a one-way door; `Config::methods` advertises `publickey` only. - `ChannelOpenHandle` drop = auto-reject semantics; unfeatured channel-type declines observable by vanilla `ssh` (`ssh host` session-open refused, `-L` refused, clean diagnostics). - `Channel` ↔ AsyncRead/AsyncWrite conversion (`into_stream`/`split`) behaves under a trivial echo (the plumbing POC-2 leans on). **Hypothesized outcome:** the auth-bridge design holds; the only uncertainty is fingerprint-format parity details, which this either 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 byte format sits inside, and how does the decrypt→dial lifecycle behave? **Construction:** standalone. alkvault `encrypt`/`decrypt` (`EncryptedData`) wrapping an OpenSSH-encoded private key (generated in-POC) → `ssh-key` `decode_secret_key` → `russh::keys::PrivateKey` → `authenticate_publickey` against POC-1's server loopback (which resolves the key's fingerprint through the same stub `IdentityProvider`). **Validates:** - The at-rest shape: OpenSSH private-key encoding as the inner format vs raw seed bytes or PKCS#8 (the ssh-key crate's own encrypted-key format with a passphrase is an alternative wrapper worth comparing against alkvault's outer encryption). - The at-use lifecycle: decrypt-at-dial → one-time `decode_secret_key` → zeroized handling for the dial's lifetime; what happens locked / absent (fail-closed no-dial) / mid-session unlock rotation. - Whether a `Signer`-shaped seam is still worth anything given the bytes are handled client-side at use anyway (expected answer: no, recorded so the ADR can dismiss it with evidence). - Server and client host-key material riding the same model. **Scope note:** the vault is for secrets *we* hold (client-side outgoing keys and the server's own host key) and is explicitly not involved in resolving *clients'* fingerprints (that path is the assembly-layer `IdentityProvider`). The vault's derivation surface (`derive_ed25519`, SLIP-0010 paths) is not on this path either; that is a different alkvault feature, not SSH key custody. Kept as its own POC because its failure modes (at-rest/lifecycle, zeroization, lock behavior) are unrelated to the auth-flow failure modes of POC-1. **Hypothesized outcome:** OpenSSH-encoded key inside alkvault `EncryptedData`, one-time `decode_secret_key` at dial, zeroizing 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? **Construction:** standalone, but *depending on the real sibling crates* (alktty/alksocks/alkgit + alkcall types via their published forms). russh server loopback + vanilla clients; per stage: - **Stage A, git exec mapping** (the alkforge-critical path): vanilla `git clone/push` over `ssh://` → russh server → parse exec command → `serve_duplex_resolved(...)` over the split channel halves with stub `GitRefs`/`GitPackGen`/`GitPackIngest` (or the substrate composition if a real repo is wanted late in the POC). Confirms: the Parse-mode (server-speaks-first advertisement) dialect satisfies vanilla git's expectations over exec; channel EOF/close maps to session termination; exit-status flows. - **Stage B, `-D` mapping**: russh server, feature `dynamic-forward`- shaped build → `serve_socks5_over` over a session channel → `curl` through the SOCKS5 proxy to a local HTTP endpoint. Confirms: the dispatch-split inside `session` (parse-then-route vs SOCKS-detect), duplex backpressure under throughput. - **Stage C, `-L` mapping**: `direct-tcpip` → the mapping-table's two alktunnels routes (adapter-side `TunnelSession::open_direct` via a real `ChannelClient` vs local-dial stand-in). Whichever route the POC exercises informs the phase 1 ADR on the presence/absence of a serve-over-stream entry in alktunnels. - **Stage D, session/pty mapping**, final stage (ran in full): shell, exec, window-change and signal mapping against a real PTY (`LocalTtyBackend`, alktty `local`). The "optional" hedge in the original plan was unnecessary: stage D completed 13/13 and produced the load-bearing session-row corrections: the alktty bridge is a **translation bridge** (chunk framing both directions, not a byte pump) and the exit/signal findings over the alktty exit-code convention (pipe-path signal termination rides the negative code `−N`; the PTY path reduces signal termination to a plain exit code, per `poc-3-mapping-bakeoff.md` §D.2). **Validates:** the one-seam-per-mapping hypothesis; the parse-then- route dispatch; stderr semantics in pipe vs PTY mode; EOF/close/signal fidelity end to end; throughput sanity (window sizes vs the 1 MiB channels reassembly cap on the bridged route). **Hypothesized outcome:** stages A–C confirm the design with local fixups recorded; stage D may surface PTY-merge stderr surprises that reshape the request-level mapping rows. ## Client-side scope (post-POC addition) The three POCs are server-door-centric by design, but the crate has a **client direction** of equal scope: a family app dials an *upstream* vanilla-SSH server and consumes its surface (exec/pty channels, `direct-tcpip` opens, the `tcpip_forward` + accepted-`forwarded-tcpip` listener flow). No dedicated POC was needed: the POCs exercised the client API throughout (POC-1's russh-client auth leg, POC-3 §D.3's client-side request mechanics (`Channel::exec()`, the `wait()`-drain trap, fire-and-forget `window_change`/`signal` before `into_stream()`)). Two client-side facts phase 1 designs from: 1. **The acceptor rule.** The only server-initiated channel type in the pattern is `forwarded-tcpip`, and russh's client default for an unhandled forwarded open is *accept-and-drop*, which is not the family fail-closed posture. The client-side accepted-open disposition (accept into a registered handler vs decline) is alkssh's contract to define. 2. **A real consumer pattern exists**: the alkminer hub dialing rented GPU containers (exec + job management, SFTP binary deploy, `-L` and `-R` tunnel legs in both directions, keepalives tuned for NAT'd hosts). Extracted (concepts only, never code) in `reverse-runner.md`. ## Sequencing and gates 1. POC-1 first; everything downstream presumes its auth plumbing. 2. POC-2 after POC-1 (reuses POC-1's server loopback). 3. POC-3 last; stage A gates phase 1's mapping ADR work; it is the path alkforge phase 0 consumes first. 4. Phase 0 completed when the findings docs existed and the OQ list in `vision.md` §scope questions was either answered by findings or carried into phase 1's `open-questions.md` as seeded OQs. Done: POC-1/2/3 findings docs exist; scope question 5 (mapping-table details) is answered per `poc-3-mapping-bakeoff.md` §hypothesis verdicts; scope question 4 (auth dispositions + client-side vault keys) is answered per POC-1/POC-2; scope question 2 (russh pin facts) is verified; questions 1 (the `alk/ssh` topology) and 3 (feature naming) are decision territory carried as phase 1 OQs, and the seeded OQ list is in each findings doc. ## References - `vision.md` (principles the POCs must honor, including that POC code carries relaxed constraints but never merges) - `russh.md` (API shapes the POCs exercise), `auth-bridge.md`, `mapping-table.md` - `docs/sdd_process.md` §Phase 0 (POC modes, standalone placement)