docs(architecture): the four spec docs — overview, mapping, client, auth

- overview.md: two-doors framing, family-position table, surface
  categories, bounded-resources policy (max_auth_attempts 10,
  keepalives 15s/3, per-channel caps), non-goals, lifecycle note
- mapping.md: the spine — channel/request rows (git exec-only, route-1
  bridge, translation-bridge session rows), parse-only dispatch,
  global-request rows, spawn-and-return constraint, per-build decline
  matrix
- client.md: dial surface (key source + host-key policy + keepalives),
  op rows (family types in, vanilla out), fail-closed acceptor
  (not russh's accept-and-drop), lifecycle primitives only, POC
  correctness facts carried
- auth.md: the fingerprint bridge + dispositions summary (server),
  vault custody + two-case host-key policy (client)
- README index: doc table filled, current state updated
This commit is contained in:
glm-5.3-flash committed 2026-10-27 03:07:17 +00:00
1 parent e43d2d184d
commit 1bfd07adfc
5 files changed
+513 -15

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
---
status: draft
last_updated: 2026-10-27
---
# Client — client direction: driving vanilla SSH servers as family ops
## The door framing
The client direction is a door *to vanilla servers*: family in,
vanilla out. The motivating consumer is the **reverse-runner** shape
(`docs/research/reverse-runner.md`, alkminer extraction): a family hub
dials vanilla `sshd` on rented/near-ephemeral hosts — remote boxes
expose plain sshd and hold nothing of ours; every op the hub drives
maps family-side onto the same protocols the server direction serves.
The asymmetry is honest (vision principle 2): the server direction
*serves* family seam entries; the client direction *consumes* family
types. Same features gate both (ADR-003 symmetry rule).
**Type symmetry at the seam is the client contract:** a family app
drives alkssh in family-protocol types; alkssh translates to vanilla
wire ops (russh client surface, ADR-002). No vanilla-concept leaks
up into the family surface.
## The dial surface
Every dial needs: address, **key source** (ADR-005 — vault-backed
blob, decrypt-at-dial, fail-closed on locked/absent — never env vars,
never plaintext at rest), **host-key policy** (OQ-05 resolution —
assembly-provided; crate default fail-closed reject-unknown, hard
decline on mismatch; the OpenSSH "add to known_hosts?" moment is an
assembly policy, e.g. learn-and-record / prompt / provider-metadata
match — the crate is policy-blind), and the bounded-resource config
(`keepalive_interval`/`keepalive_max` — 15 s / 3 default posture per
the reverse-runner evidence; `inactivity_timeout`; window/packet
knobs per `overview.md`).
The identity the dial authenticates *as* is a registered fingerprint
at the remote end — for family-run peers (alkforge runner) that's the
alkcall `IdentityProvider`; for vanilla fleets it's whatever the
vanilla server's `authorized_keys` holds — either way the *key* is
ours from the vault and the host's key is checked by the policy.
## Op rows (family op → vanilla translation)
| Family-facing op | Family protocol/types | Vanilla-wire translation | Feature |
|---|---|---|---|
| run a command / own a shell on the runner | alktty session types (pipe vs PTY; exit semantics) | `channel_open_session` + `exec`/pty-req/shell-req; data pump; drain `wait()` per the POC-3 §D.3 mechanics (`Channel::exec()` — no `request_exec`; Success/Failure arrive on the channel queue, not as `Err` from the request call) | `session` |
| open a tunnel target | alktunnels `TunnelSession`/target types | `channel_open_direct_tcpip` per connection — the typed open resolves to `Err(ChannelOpenFailure)` on server reject (POC-1 §6); no `wait()` before `into_stream` | `tunnel` |
| the `-D` behavior (local SOCKS door) | alksocks' local door **composed at the assembly**, fed a `DialFn` backed by row 2's open | the wire sees only `direct-tcpip`; alkssh *does not depend on alksocks* on this path (ADR-003 §1a) | `tunnel` |
| listen + ingest (`-R` leg) | alktunnels consumer/`ForwardedHandler` shape | `tcpip_forward(address, port) -> bound` global request (no mpsc sink param) + **accept the server-initiated `forwarded-tcpip` opens** via `server_channel_open_forwarded_tcpip`, routing each to the family handler | `tunnel` |
| upstream git consumption | alkgit client/service types | the same exec grammar the server side parses (`git-(upload,receive)-pack '/repo'` exec), pointed at an upstream `sshd` | `git` |
| artifact/deploy transfer | alkfs client half (future) | `subsystem "sftp"` + the alkfs protocol | `sftp` (OQ-13) |
## The acceptor: fail-closed client-side
russh's client handler receives server-initiated channel opens; its
default posture for a forwarded open is **accept-and-drop** — a
half-capability, which the family forbids (ADR-003 symmetry +
`reverse-runner.md` lesson 1). The client contract:
- Server-initiated opens are served only when the assembly registered
an acceptor **for that type and under its feature** (`forwarded-tcpip`
under `tunnel` is the real case — the `-R` data path).
- Unregistered/unfeatured accepted opens are **declined cleanly**,
never silently dropped after accept.
- Acceptor handlers may be installed across a live connection's
lifetime (the alkminer `install_forward_sink` late-install dynamic —
consumers turn capabilities on/off per connection; the shape is an
assembly-mutable acceptor registry).
## Lifecycle primitives (OQ-12 resolution)
- Forward listeners return the **actually-bound port** (russh
`tcpip_forward -> u32`); every listener/handle is explicitly
cancellable; disconnects are explicit (`Disconnect::ByApplication`).
- **No reconnect or restore policy is owned by the crate.**
Reconnect-and-restore (rebinding persisted tunnel ports — the
alkminer flow) is assembly policy; the crate provides the
primitives (bound ports, handles, cancellation, clean close) to
make that possible.
## What the crate does *not* include (client direction)
- **Job supervision** — background-job lifecycle (the alkminer
start/stop/logs job model, pid files, host-side ledgers) is
assembly work composed over the exec rows; the crate gives the
channels and the exit semantics (including the pipe-mode
pid-only signal caveat — `mapping.md` / OQ-10).
- **Credential storage policy** — assembly (the vault handle is
injected; ADR-005 / OQ-06).
- **Trust decisions** — the host-key policy is assembly-provided
(OQ-05; the crate default rejects).
- **sftp** — until alkfs (OQ-13); the `sftp` feature exists as the
tested decline on both directions.
## Correctness facts to carry (POC-recorded)
- `Channel::exec()` (no `request_exec` at this pin); pty/subsystem
request failures surface as `ChannelMsg::Failure` via `wait()`, not
`Err` from the call — the caller must drain until the expected
message, `continue` past the rest.
- `window_change`/`signal` are fire-and-forget and must be sent
**before** `into_stream()` (the Channel is consumed by conversion);
server-side they carry no SSH reply.
- Do not consume the channel queue before `into_stream()` — a
`wait()` on a fresh channel breaks it (POC-1 §6 caveat), both sides.
- Keepalives on cloud fleets: 15 s / 3 (OpenSSH
`ServerAliveInterval=15`/`CountMax=3` parity) — with russh's
default (none), dead NAT'd connections sit unnoticed until an exec
health check fails (`reverse-runner.md` lesson 3).
- A rejected channel open is an `Err` from the typed open call (not a
`wait()`-match); the connection stays usable after declines.
## Open Questions
- **OQ-05** (resolved): the host-key policy seam — the contract above;
pin storage is assembly detail.
- **OQ-12** (resolved): lifecycle primitives only — recorded above.
- **OQ-13** (deferred, alkfs): the sftp client half.
- **OQ-07** (open): rides the vault decrypt zeroization ask (ADR-005).
## References
- ADR-002 (the client `russh` surface + config knobs), ADR-003 (§1a the
alksocks composition; the symmetry rule), ADR-004 (fingerprint
identity), ADR-005 (key sources), ADR-006 (route-1 bridge, both
legs)
- `docs/research/reverse-runner.md` (the consumer pattern + lessons),
`poc-3-mapping-bakeoff.md` §D.3 (client mechanics), §C.4 (the fabric
recipe), `poc-1-auth-bridge.md` §6/§7 (open-call `Err` semantics,
plumbing), `poc-2-vault-keys.md` (dial lifecycle)
- `mapping.md` (the server-direction mirror rows)