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:
1 parent
e43d2d184d
commit
1bfd07adfc
5 files changed
+513
-15
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user