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

201 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)