Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
10 KiB
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, 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_publickeyafter russh's signature verification →ssh_keyfingerprint (HashAlg::Sha256) string parity with a stub-registered identity;resolve_from_fingerprint→Some→ authenticated; unknown key → clean decline underauth_rejection_time/max_auth_attempts.auth_publickey_offereddisposition (accept-and-let-decide) behaves with OpenSSH's probe-then-sign flow.- Decline matrix:
auth_none/password/keyboard-interactivedeclined (0.64.1 correction: the default impls already reject at this russh: onlyauth_publickey_offereddefaults 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::methodsadvertisespublickeyonly. ChannelOpenHandledrop = auto-reject semantics; unfeatured channel-type declines observable by vanillassh(ssh hostsession-open refused,-Lrefused, 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/pushoverssh://→ russh server → parse exec command →serve_duplex_resolved(...)over the split channel halves with stubGitRefs/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,
-Dmapping: russh server, featuredynamic-forward- shaped build →serve_socks5_overover a session channel →curlthrough the SOCKS5 proxy to a local HTTP endpoint. Confirms: the dispatch-split insidesession(parse-then-route vs SOCKS-detect), duplex backpressure under throughput. - Stage C,
-Lmapping:direct-tcpip→ the mapping-table's two alktunnels routes (adapter-sideTunnelSession::open_directvia a realChannelClientvs 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, alkttylocal). 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, perpoc-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:
- 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. - A real consumer pattern exists: the alkminer hub dialing rented
GPU containers (exec + job management, SFTP binary deploy,
-Land-Rtunnel legs in both directions, keepalives tuned for NAT'd hosts). Extracted (concepts only, never code) inreverse-runner.md.
Sequencing and gates
- POC-1 first; everything downstream presumes its auth plumbing.
- POC-2 after POC-1 (reuses POC-1's server loopback).
- POC-3 last; stage A gates phase 1's mapping ADR work; it is the path alkforge phase 0 consumes first.
- 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'sopen-questions.mdas seeded OQs. Done: POC-1/2/3 findings docs exist; scope question 5 (mapping-table details) is answered perpoc-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 (thealk/sshtopology) 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.mddocs/sdd_process.md§Phase 0 (POC modes, standalone placement)