Files
alkssh/docs/research/pocs.md
T
glm-5.3-flash 31207057ff docs(research): phase 0 closed — pocs.md refreshed, reverse-runner pattern extracted
- pocs.md: marked complete; corrected the stale 0.5x-era accept-defaults
  hypothesis wording (0.64.1: only auth_publickey_offered defaults to
  accept), stage D recorded as ran-in-full, completion gate updated with
  per-question resolution status
- added client-side scope section: no dedicated POC needed (client API
  exercised throughout POC-1/POC-3) + the acceptor-default fact (russh
  client accept-and-drop for unhandled forwarded opens is ours to fix)
- new reverse-runner.md: alkminer pattern extraction (concepts only,
  never code) — hub-dials-runner surface inventory, both tunnel legs,
  the OAI-proxy secret-hygiene shape, and the client-direction lessons
  (keepalive defaults, reconnect-restore, acceptor disposition)
- vision.md: scope-question resolution addendum + whitespace typo fix
2026-10-26 06:09:38 +00:00

10 KiB
Raw Blame History

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/<poc-name> 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 — that 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 — 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 alktty exit chunk carries no signal identity (poc-3-mapping-bakeoff.md §Stage D).

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 — 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)