Files
alkssh/docs/research/reverse-runner.md
T
glm-5.3-flash 263f91fdb7 docs(architecture): review-pass cleanup — inconsistencies, stale refs, scaffolding refresh
- OQ-02: rewritten as a clean resolved statement; contradictory
  deferred-table row removed
- OQ-05: 'old-code anti-pattern' framing corrected — ephemeral-fleet
  accept-and-continue is a legitimate case-2 HostKeyPolicy; the crate
  rule is 'no hard-coded trust model', not 'accept-all forbidden';
  dangling 'expected ADR-007' pointer replaced with the real contract
  locations (auth.md, client.md)
- README: OQ counts corrected (6 resolved / 3 open / 4 deferred);
  sibling references normalized to real ADR IDs (alkcall 010 is alkhttp
  014's analog; 'ADR-016-analog' was a wrong number)
- client/auth: OQ-07 status corrected to resolved-at-adapter-level;
  TOFO typo; TOFU framing
- ADR-004: dangling 'ADR-008' -> serve-config defaults
- ADR-006: SOCKS5 typo
- mapping: alktty typo; '(OQ-… note below)' placeholder -> the real
  POC-3 note reference
- research: cross-repo numbers qualified (alktty OQ-44, alkvault
  ADR-021); reverse-runner lesson 5 reframed per correction
- scaffolding: Cargo.toml description/comments and src/lib.rs doc
  comment updated against ADR-002/003/006 (dynamic-forward retired,
  git exec-only, russh pin decided); CHANGELOG logs POC completion and
  ADRs 001-006

Verification: cargo test, cargo clippy --all-targets -- -D warnings,
cargo fmt --check, cargo check — all pass
2026-10-27 10:00:11 +00:00

7.2 KiB
Raw Blame History

Reverse runner — client-direction pattern extraction (alkminer)

Status: phase 0 post-POC extraction. Reading aid only — concepts and wire shapes, never code (the source is not open-sourced and stays that way; per repo license hygiene, reference code is for reading and facts). Source: /workspace/@alkminer/alkminer — specifically crates/alkminer-host/src/remote/{ssh,exec,forward,manager,sftp}.rs and src/oai/mod.rs. Also old (russh 0.62 era) and POC-grade; its weak spots are as instructive as its strengths, and both are recorded below as facts to design from, not precedents to copy.

The direction

The reverse-runner pattern inverts the typical runner flow: the hub/coordinator is the SSH client and dials out to vanilla sshd on rented containers (vast.ai, runpod — any box that exposes SSH). The runner exposes nothing but the wire; the hub is the active party and pushes all work and all channel opens.

Security shape (why this beat the alternatives in practice):

  • No inbound surface on runners — only sshd with one key.
  • Secrets never leave the hub — the pattern's killer use was an OAI-compatible proxy served by the hub (oai/mod.rs, alkminer ADR-016): the instance points its LLM tooling at a localhost URL that rides the tunnel to the hub's proxy, which injects the real provider key. The runner authenticates to the proxy with a scoped dev token — least privilege, no upstream-secret custody.
  • Reachback both ways, deliberately: hub→runner services via -L, runner→hub services via -R (the two legs below were both used, sometimes chained: instance port ‑L→ hub port ‑L→ dev workstation).

Per-connection surface (what the hub actually drove)

One authenticated SSH connection per instance carried:

Surface alkminer use alkssh client-direction translation
blocking exec provisioning commands, health checks session channel open + exec(); alktty pipe-mode seam when the runner is family-built
background jobs (start_job/job_status/job_logs/stop_job) mining worker lifecycle; pid files on the runner, job rows in hub-side storage assembly-level job supervision over exec channels; the alkminer pid-file approach is the ad-hoc analog of POC-3 §D's pipe-mode signal gap (alktty exit-chunk signal identity OQ)
SFTP binary deploy to the instance future alkfs client half — real consumer evidence that the sftp feature's deferred implementation needs both directions (alkforge CI artifact deploy is the consumer)
-L client-side tunnel opencode dev tunnel (instance:4096 → hub) client opens direct-tcpip per accepted local connection and pumps (POC-1's into_stream plumbing, reversed)
-R client-side tunnel hub-service reach tunnel (instance → hub's HTTP/OAI proxy) tcpip_forward global request (returns the bound port — no mpsc sink param, matching russh.md's client inventory) then accept the server-initiated forwarded-tcpip opens and relay each to the hub-local target

Client-direction facts that phase 1 designs from

  1. The acceptor row is unavoidable and its disposition is ours. russh's client handler receives server_channel_open_forwarded_tcpip for -R data. In alkminer's 0.62-era code (and unchanged in kind at 0.64.1), an accepted forwarded channel with no handler installed is accept-and-drop — a fail-open-ish default. alkssh's client contract: only channel types/features the assembly registered are served; otherwise decline cleanly. This is the client side of the feature-graph symmetry rule; unlike the server auth defaults (which 0.64.1 already has rejecting), the client acceptor default is ours to enforce.
  2. The routing sink installs post-connect. alkminer's shape: the handler exposes a late-bound handler slot (install_forward_sink) set after connect by the consumer that owns the forward. The family translation is an assembly-injected acceptor-handlers registry (per channel type / per feature), not a static config — but the late-install dynamic is real: consumers turn capabilities on/off over a live connection.
  3. Keepalives are load-bearing on cloud runners. The hub needed keepalive_interval 15 s + keepalive_max 3 (OpenSSH ServerAliveInterval=15/ServerAliveCountMax=3 parity) — with russh's default (no keepalives), dead NAT'd connections sat unnoticed until a health-check exec failed. Evidence for the client-side bounded-resources defaults (the server side has symmetric knobs; cloud edges need both).
  4. Reconnect-and-restore semantics. Tunnel ports were persisted (hub storage) and re-bound identically on reconnect; forwards were tracked per connection with idempotent stop handles. This is assembly policy (session/forward lifecycle), not wire contract — kept in view so phase 1 doesn't accidentally bake reconnect policy into the crate.
  5. Host-key acceptance for ephemeral fleets — the pattern the fleet model demands. alkminer's check_server_key returned accept-all for rental fleets. That was the correct pattern for that problem, not a lapse: a fresh container per rent mints a fresh host key, so no pin can exist beforehand — strict TOFU/"pin everything" is unusable there, and the churn is exactly why accept-all won. The security anchor is elsewhere: hub-held per-instance dial keys make a captured hub credential worthless, plus what rides the tunnels. The family rule is therefore not "pin everything" — it is "no trust decisions inside the crate": default fail-closed at the crate, with an assembly-provided policy seam so each fleet model gets the control that fits it — host pinning where anchors can exist (family-built runners, whose host key the assembly mints and therefore knows beforehand), permissive accept where they can't (rental fleets). The fleet churn is the reason the two-case policy seam (OQ-05) is the correct shape rather than "pin everything".
  6. Per-runner key registration. Keys were minted per instance and registered hub-side (their provisioning flow, including a real gitea client for dev workspaces) — the deployment precedent for alkforge's per-runner IdentityProvider rows.

Not carried

  • No code extraction in either direction (concepts/wire shapes only).
  • The job-supervision storage model (hub-side SQLite job ledger, pid files on the runner) belongs to the assembly layer in a family deployment; the crate's surface is channels and tunnels.
  • No per-target scoping of proxied traffic existed (dev token proved "a valid dev instance", nothing more) — a real least-privilege gap the family model fixes via per-identity ACLs on the service side.

References

  • pocs.md §Client-side scope (the scope item this doc grounds)
  • mapping-table.md (server-direction rows; POC-3 stage C is the server side of the same bridge legs), poc-1-auth-bridge.md §7 (the channel↔stream plumbing both leg styles reuse), poc-3-mapping-bakeoff.md §D.3 (client-request mechanics)
  • auth-bridge.md §Host keys and check_server_key (the trust-policy seam this doc's ephemeral-fleet acceptance pattern motivates)
  • alkminer ADR-016 (the OAI proxy pattern — secrets stay at the hub)