diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 07272b0..f577246 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -9,27 +9,21 @@ last_updated: 2026-10-26 Phase 1 (architecture) — in progress. Phase 0 is closed: all three POCs passed (121 checks; `docs/research/poc-{1,2,3}-*.md`) and the -research docs record their corrections. This directory is being -populated: ADR-001 (the `alk/ssh` ALPN pin) is drafted for review, the -OQ tracker is seeded from the POC findings, and the spec documents -below are being written. No implementation until specs reach -`reviewed` (per `docs/sdd_process.md`). +research docs record their corrections. The ADR set (001–006) is +**Accepted** — including the post-review amendments (ADR-002's +ed25519-only revert; ADR-003/006's `dynamic-forward` retirement) — and +the four spec docs (overview/mapping/client/auth) are drafted; the +architecture review pass is next, after which specs go to `reviewed`. ## Architecture Documents | Document | Status | Scope | |---|---|---| +| [overview.md](overview.md) | draft | Crate purpose, family position, surface categories, bounded-resources policy, non-goals | +| [mapping.md](mapping.md) | draft | The spine — server-direction rows, dispatch, spawn-and-return, decline matrix | +| [client.md](client.md) | draft | Client-direction surface: dial, op rows, the acceptor, lifecycle primitives | +| [auth.md](auth.md) | draft | The auth bridge (server) + custody & host-key policy (client) | | [open-questions.md](open-questions.md) | draft | Centralized OQ tracker (seeded from phase 0 findings) | -| [decisions/001](decisions/001-alkssh-alpn-pin.md) | Proposed | The `alk/ssh` ALPN pin (ledger rule 1) | - -### Planned documents (not yet written) - -| Planned doc | Scope | -|---|---| -| `overview.md` | Crate purpose, exports, dependency posture, bounded-resources policy | -| `mapping.md` | The spine — server-direction channel-type/request→family-seam rows (absorbs the validated `mapping-table.md`) | -| `client.md` | Client-direction surface: dial rows, the forwarded-channel acceptor, reverse-runner shape | -| `auth.md` | Server auth bridge + client-side vault key custody | ## ADR Table diff --git a/docs/architecture/auth.md b/docs/architecture/auth.md new file mode 100644 index 0000000..7a1206c --- /dev/null +++ b/docs/architecture/auth.md @@ -0,0 +1,118 @@ +--- +status: draft +last_updated: 2026-10-27 +--- + +# Auth — identity is the family's, not SSH's + +## The model + +One resolution path exists on every surface: a **verified key's +fingerprint → alkcall `IdentityProvider::resolve_from_fingerprint` → +`Identity`**. russh performs all cryptographic work (ADR-002 pin): +`auth_publickey` fires *after* signature and ownership verification, +and invalid/malformed signatures produce `USERAUTH_FAILURE` before +any handler hook runs (POC-1/POC-2 facts). alkssh never sees or +verifies a signature; its auth surface is *dispositional* (ADR-004). + +## Server direction + +### The bridge + +1. `auth_publickey(user, VerifiedPublicKey)` fires (post-verification; + 0.65.0 shape — the key is the `.public_key` field, and the client's + signature `.algorithm` is exposed for optional hardening). +2. alkssh computes the fingerprint via the `ssh-key` re-export + (`HashAlg::Sha256`) — `SHA256:` + unpadded url-safe base64. The + string format is a **contract invariant** (ADR-004): parity with + assembly-registered values is proven against independent issuers + (`ssh-keygen -lf` — POC-1 §1). +3. `resolve_from_fingerprint` → `Some(identity)` → accept; `None` → + reject — fail-closed, and the resolved-identity-`None` path never + creates a "weakly authorized" session. +4. On success the identity rides the session (`auth_succeeded`): for + mapped channels it propagates through the alkcall transport + (`set_identity`, once) so `AccessControl::check(peer_identity)` + gates every mapped open — the **visible-surface invariant** holds + in the SSH direction with no SSH-shaped exception. POC-3 §C.3 + observed the propagation live (the establisher witness). + +### Dispositions (ADR-004 — summarized) + +`publickey` is the only implemented method; `auth_publickey_offered` +accepts-and-delegates (keeping `max_auth_attempts` honest — the hook +does not consume an attempt, POC-1 verified); `auth_none` / +`auth_password` / `auth_keyboard_interactive` / `auth_openssh_certificate` +reject explicitly (at this pin russh's defaults already reject; the +explicit implementations are the family contract — a pin bump could +change defaults silently). `Config::methods` advertises `{publickey}` +— exactly what is implemented. The username is **not identity and +carries no semantics** (OQ-03 resolved); log exposure is deployment +policy. `max_auth_attempts` defaults `10` (the OpenSSH `none`-probe +consumes one — POC-1 §4). + +## Client direction + +### Key custody (ADR-005) + +Held keys are bearer-token-class secrets: **vault-encrypted at rest +(alkvault `EncryptedData`, OpenSSH-PEM inner), decrypted at use, +zeroized ownership at the adapter seam**; never env vars, never +plaintext at rest, never persisted after use. The vault is a secret +store on this path — not a key generator, not a custodian, and never +involved in resolving *peers'* fingerprints. Failure modes are all +fail-closed with distinct errors: locked / absent / decryption +failure / unknown `key_version` — and rotation preserves key identity. +The same model covers **host keys** (decrypt-at-construction into the +server's `Config::keys`; locked vault → **no server is constructed** — +POC-2 §5). + +### Host-key policy (OQ-05 resolution; the client-side trust contract) + +Two cases, one seam — the crate is **policy-blind**: + +1. **Known beforehand** (anchors exist; family-built runners mint + their host key at assembly time): assembly-supplied pins — + **hard-decline on mismatch** at `check_server_key`. +2. **Unknown beforehand** (vanilla `sshd` on rental fleets; no anchor + until contact): the crate surfaces + (host, port, presented fingerprint) to an **assembly-provided + `HostKeyPolicy`** — prompt-and-record, provider-metadata match, or + learn-and-record. The OpenSSH "add to known_hosts?" moment becomes + assembly policy. + +Crate rules: default policy is fail-closed (reject unknown); the +crate never decides trust; accept-all is never a crate option +(alkminer's accept-all is the recorded anti-pattern; for ephemeral +fleets the *real* anchor is hub-held key material — per ADR-005's +custody model — not host pins). + +## What is out of scope + +- Password / keyboard-interactive anywhere (server: declined; client: + non-goal until a use case). +- OpenSSH certificate auth (`auth_openssh_certificate` declines; + OQ-04 deferred on a CA use case). +- ssh-agent integration/forwarding (external signer processes are out + of family scope; the vault is the custody story). +- TOFO/TOFU *as crate behavior* — policy lives at the assembly, the + crate is policy-blind (above). + +## Open Questions + +- **OQ-04** (deferred — certificate auth, on a use case). +- **OQ-07** (open — the vault `decrypt` plain-`String` gap; upstream + ask to alkvault; adapter-side `Zeroizing` convention stands + meanwhile; ADR-005 decision 3 is the contract). +- **OQ-05** (resolved — recorded above). + +## References + +- ADR-004 (dispositions + parity invariant), ADR-005 (custody), + ADR-002 (the pinned russh/auth pipeline facts) +- `docs/research/auth-bridge.md` (the surface mapping + host-key + refinement), `poc-1-auth-bridge.md` (§1–4), `poc-2-vault-keys.md` + (§2–5), `reverse-runner.md` (lessons 5–6) +- alkcall ADRs 002/003/006 (identity surface), alkhttp ADR-014/073 + analogs (secret flow, one-backend posture), family ADR-002 + decision 5 / family ADR-003 decision 2 \ No newline at end of file diff --git a/docs/architecture/client.md b/docs/architecture/client.md new file mode 100644 index 0000000..4a1aa04 --- /dev/null +++ b/docs/architecture/client.md @@ -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) \ No newline at end of file diff --git a/docs/architecture/mapping.md b/docs/architecture/mapping.md new file mode 100644 index 0000000..b823890 --- /dev/null +++ b/docs/architecture/mapping.md @@ -0,0 +1,140 @@ +--- +status: draft +last_updated: 2026-10-27 +--- + +# Mapping — server direction: SSH channels × family seams (the spine) + +## The door framing + +The server direction is a door *for vanilla clients*: vanilla in, +family out. Vanilla `ssh`/`git` connect as they would to any sshd; +every accepted channel/request maps to **exactly one family protocol +seam** and **exactly one cargo feature** (ADR-003). Rows below are +POC-3-validated against published sibling crates with real vanilla +tooling (OpenSSH 9.6, git 2.43, curl 8.5 — `poc-3-mapping-bakeoff.md`). + +## The seams + +| Family crate | Serve-over-a-stream entry | Signature shape | +|---|---|---| +| alktty | `drive_session[_pre_negotiated]` | `(impl AsyncWrite, impl AsyncRead, backends, identity)` | +| alkgit | `serve_duplex_resolved` | `(repo_id, identity, service, duplex, limits, backend traits)` | +| alktunnels | — no serve entry (**absence note resolved**: route 1 never needed one, ADR-006) | consumer/open-op registrations | + +russh channels convert to duplex via `into_stream`/`split` — the +bridge primitives every mapping uses (POC-1 §7). + +## Channel-type rows + +| SSH channel type | Vanilla use | Feature | Family seam | Contract notes | +|---|---|---|---|---| +| `session` → `pty_request` + `shell_request` | `ssh host` interactive shell | `session` | alktty: `NegotiateRequest` constructed locally → `drive_session_pre_negotiated` | `pty_request` args → `TerminalParamsWire` (term/cols/rows/pixels; SSH modes → the reserved modes JSON); shell convention per OQ-08; the data plane is a **translation bridge**, not a byte pump — alktty is chunk-framed both directions (stdin/stdout/stderr/CTRL chunks): SSH data→stdin chunks; stdout chunks→SSH data; stderr chunks→`extended_data(1)`; exit chunk→`exit-status`/`exit-signal` + close (POC-3 §D.1) | +| `session` → `exec_request` (non-git) | `ssh host "cmd"` | `session` | alktty pipe mode (`terminal: None`) — separate stderr | stderr rides `extended_data(1)` — only valid without PTY; PTY mode merges (kernel PTY, POC-3 §D.1 proven) | +| `session` → `exec_request` git | `git clone/push ssh://` | `git` | alkgit: parse command → `Service` + repo → `serve_duplex_resolved(repo_id, identity, service, duplex, …)` | **exec-only** (ADR-006 decision 2): the repo rides inside the exec command string (`git-upload-pack '/repo'`); vanilla git never speaks subsystem for git; process-mode dialect (server-speaks-first advertisement); exit-status proven both directions | +| `session` → `subsystem_request` | `sftp` clients; manual `ssh -s` | `sftp` (placeholder) / `git` rows | — | **`sftp` name**: declines until alkfs lands (OQ-13) — the feature exists, the route doesn't. **git service names**: decline with reason — a subsystem request carries only a name, never a repo id (POC-3 §A.3); never half-serves | +| `direct-tcpip` | `ssh -L`; all `ssh -D` traffic | `tunnel` | alktunnels **route 1**: adapter-side bridge — `TunnelSession::open_direct` on a held `ChannelClient`, pump `take_halves()` vs the channel | target = `SubstrateAddr::HostPort`/`Path`; `Substrate` = Tcp/Unix; accept only **after** the fabric open succeeds — failure maps to an explicit `ConnectFailed` reject (POC-3 §C.2); vanilla `-D`'s dynamic targets dispatch here (POC-3 correction 2) | +| `forwarded-tcpip` | `ssh -R` data channels | `tunnel` | the `register_tunnel_forwarded_openable`'s `ForwardedHandler` shape — hand the channel duplex to the handler | listen flow = the global-request rows below; *the* data leg untested in POC-3 beyond refusal (OQ-… note below) | +| `direct-streamlocal` | unix-socket forward | `tunnel` | `SubstrateAddr::Path` treatment | unix-only; decline shape per OQ-11 | +| `x11` | X forwarding | — (never) | — | always declines; not feature-gated (ADR-003 §5) | +| `auth-agent` | agent forwarding | — (never) | — | always declines; the anti-pattern of the alkvault custody posture (ADR-003 §5) | +| `session` → SOCKS5-in-channel | (no vanilla use) | — (retired, ADR-003 §1a) | — | vanilla `-D` is client-side SOCKS + `direct-tcpip`; the family local door is alksocks composed at the assembly — see `client.md` | + +## Session-channel dispatch (ADR-006, as amended) + +**Parse-only.** A session channel that receives a request takes the +parse path: `exec_request` parses first (a git-parseable command +routes to `git`; anything else to `session`'s pipe path), and +`pty_request`/`shell_request` route to `session`. Request-less session +channels decline as unfeatured. Parsing happens before dispatch and +before any resource allocation — a leaked channel alloc is a +capability leak, so **decline first, alloc never** (POC-3 stage A's +decline matrix is the contract: unknown repo → collapsed generic +denial *before any protocol byte* — no existence oracle; non-git +command → parse-then-fail). + +## Global-request rows (the `-R` listen flow) + +`Handler::tcpip_forward(address, &mut port) -> bool` + +`Handler::streamlocal_forward` — gate on `tunnel` (decline both the +listen request and the data channels when absent). The bound-port +out-param (`port = 0` → server picks) is a spec detail in +`client.md`'s lifecycle primitives; disposition follows `tunnel`. +*POC-3 note: only the refusal shape was exercised (egress-only build); +the served listen flow is implementation-phase contract-test territory.* + +## Request-level rows on a `session` channel + +| SSH request | Family mapping | +|---|---| +| `pty_request` | `NegotiateRequest.tty = TerminalParamsWire{…}` (POC-3 §D.1: live `stty size` honored) | +| `shell_request` | `NegotiateRequest.cmd` per OQ-08 (assembly convention, never wire-supplied) | +| `exec_request` | parse → git dispatch (`git`) or alktty pipe `cmd` (`session`); unknown → `channel_failure`, no resource alloc | +| `subsystem_request` | decline (per the rows table; honest, with reason) | +| `env_request` | **decline by default** (OQ-09 deferred); wire env is not family config | +| `window_change_request` | resize control (POC-3 §D.1: live resize proven) | +| `signal` | signal control (alktty names verbatim); PTY mode reaches the process group, pipe mode is pid-only (backend semantics, recorded in OQ-10) | +| data / `extended_data(1)` | the translation bridge (rows table) | +| `exit_status` | from the alktty exit chunk — sent **before** close, from the spawned task via `Session::handle()` (POC-3 §D: proven both legs) | +| `exit_signal` | only for signal-terminated exits; OQ-10 (chunk carries no signal name — alktt-y ask + assembly fallback pending) | + +## The structural constraint: spawn-and-return + +russh (this pin: ADR-002) runs server handler callbacks **inline +inside the session's packet loop** — a blocking handler suspends all +packet routing for the connection (channel data is routed by that +same loop; an inline serve deadlocks against the peer's immediate +follow-up bytes — POC-3 proved by construction with receive-pack). +Therefore: **every serving path is spawned; handlers return fast**; +exit/status reporting from spawned tasks rides the cloned +`Session::handle()`. This is a load-bearing correctness constraint of +the whole spine, not an implementation choice. + +## Data-plane / limits + +- russh knobs (`window_size`, `maximum_packet_size`, + `channel_buffer_size`, `Limits`, `inactivity_timeout`, + `keepalive_*`) + in-crate max-concurrent-channels cap + per-length + exec-parse limits — per the bounded-resources policy in + `overview.md`. +- alkcall bridge leg (when the family half is remote): channels-layer + reassembly cap 1 MiB/channel; ADR-034 grants `alk/ssh` stream_types + `[0, 1]` because SSH multiplexes internally. +- Honest advertisement: `Config::methods` mirrors implemented auth + (ADR-004); an unfeatured channel type declines at open, never + half-opens; vanilla clients observe clean diagnostics (POC-1 §5: + channel-scoped decline, auth intact, no hangs). + +## Decline matrix (per feature build, contract-tested) + +| Build | Serves | Declines | +|---|---|---| +| none (default) | — | everything | +| `git` | git-parseable exec | pty/shell, plain exec, all other channel types, all `session` requests | +| `session` | pty/shell/pipe-exec | git-parseable exec (the `git` parse is its gate), other channel types | +| `tunnel` | direct/forwarded-tcpip, streamlocal (unix), `-R` listens | session channels (auto-reject), all others | +| any + `sftp` slot | — | `sftp` subsystems decline-with-reason until alkfs | + +Every row is tested in *every* build it can appear in — the decline +paths are contract territory (AGENTS.md conventions 5–6), including +under `--all-features`. + +## Open Questions + +- **OQ-08** (shell convention — status: open): blocks this doc's + `shell_request`/`NegotiateRequest.cmd` row detail. +- **OQ-10** (exit-chunk signal identity — status: open): blocks the + `exit_signal` row's precise encoding; the fallback direction is + recorded there. +- **OQ-11** (streamlocal platform decline shape — status: open). +- ~~OQ-02~~ (dispatch composition — resolved by ADR-006, as amended). + +## References + +- ADR-003 (features), ADR-006 (route 1, exec-only git, dispatch), + ADR-001 (the ALPN/edge context), ADR-004 (auth) +- `docs/research/mapping-table.md` (the validated research ancestor + + annotations), `poc-3-mapping-bakeoff.md` (stages A–D), + `poc-1-auth-bridge.md` §5/§7 (decline observability, plumbing) +- Family ADR-003; alkcall ADR-034 (`alk/ssh` stream_types) +- `client.md` (the same door, client-side) \ No newline at end of file diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md new file mode 100644 index 0000000..3cc5bc8 --- /dev/null +++ b/docs/architecture/overview.md @@ -0,0 +1,113 @@ +--- +status: draft +last_updated: 2026-10-27 +--- + +# Overview — alkssh + +## What this crate is + +alkssh is the alk family's SSH adapter (family ADR-003): a thin +wrapper around **russh** that terminates the vanilla-SSH wire and +bridges to/from family protocols. It is **a door in both directions, +vanilla on the outside** (vision principle 2; ADR framing in +`mapping.md` / `client.md`): + +- **Server direction** — a door *for vanilla clients*: vanilla `ssh`, + `git`, and (never for us, by design) sftp tooling connect as they + would to any sshd; each accepted SSH channel/request is mapped onto + exactly one family protocol seam and exactly one cargo feature. + Primary current purpose: the alkforge git door. +- **Client direction** — a door *to vanilla servers*: a family app + (the reverse-runner hub) dials vanilla `sshd` (rented fleets, + runpod/vast.ai-style boxes) and the ops map family-side onto + alktty/alktunnels/alkgit types; the runner stays vanilla by design. + +The crate's substance is glue and mapping; no SSH framing is authored +in-family (family ADR-003 decision 1) — russh owns the wire, and the +pin is ADR-002. + +## Position in the family + +| Concern | Owner | +|---|---| +| SSH wire protocol (KEX, ciphers, channel mux, userauth state machines) | russh (pinned, ADR-002) | +| SSH channel-type → family-protocol mapping | **this crate** (the spine, `mapping.md` / `client.md`) | +| Identity (fingerprint → `Identity`) | alkcall `IdentityProvider`, assembly-implemented; never the vault (vault = secrets *we* hold) | +| Secrets we hold (client dial keys, host keys) | alkvault-backed sources (ADR-005) | +| The `alk/ssh` ALPN | ADR-001 (edge-termination contract; relay/browser legs deferred, OQ-01) | +| Deployment policy (which features, which ACLs) | the embedding assembly (alkforge) | + +Composability boundary: alkssh is a door, not a backend — it depends +on alkcall types at the wire layer and the mapped crates' session or +client surfaces, and carries no deployment policy. A downstream app +embeds it, enables features (ADR-003), brings identity/ACL config. + +## Public surface (categories) + +The crate is a library (`[lib]`, no binary — non-goal); the surface +categories the phase-1 specs define: + +- **Server stack** (`mapping.md`): serve configuration (feature-gated + seam wiring, bounded-resource limits, honest-advertisement auth + config) + the serving loop that accepts vanilla connections and + dispatches per the mapping table. +- **Client stack** (`client.md`): dial surface (address, key source, + host-key policy, keepalive/timeout config), the op-row consumers + (session exec/pty, tunnel dial, `-R` listen + acceptor), and + lifecycle primitives (bound-port handles, cancel — OQ-12: no + reconnect policy is owned). +- **Key sources** (`auth.md`): vault-backed source construction per + ADR-005 for both directions (client keys *and* host keys). +- **Identity plumbing** (`auth.md`): the fingerprint→identity bridge + at the server; the identity→seam propagation for mapped channels. + +Feature graph (ADR-003): `session`, `git`, `tunnel`, `sftp` +(placeholder) — flat, independent, `default = []` (no capabilities), +two-direction symmetry, fail-closed declines contract-tested on every +side of every feature. + +## Bounded-resources policy (family convention) + +Every session is bounded by wall clock, size, and channel count +(AGENTS.md convention 9; the knobs are russh's `Config` per ADR-002's +surface notes): + +| Resource | Knob | Default posture | +|---|---|---| +| Session wall clock | `inactivity_timeout` | set (assembly-tunable) | +| Rekey volume/time | `Limits` | russh-enforced (post-0.64-full-enforcement) | +| Per-channel memory | `window_size`, `maximum_packet_size`, `channel_buffer_size` | set | +| Auth attempts | `max_auth_attempts` | 10 default (ADR-004: the counting `none` probe) | +| Keepalives (client direction, cloud fleets) | `keepalive_interval`/`keepalive_max` | 15 s / 3 (reverse-runner evidence: NAT'd runners) | +| Concurrent channels per session | ours (in-crate) + per-identity policy when bridged | ours to cap | + +No unbounded loops, no unbounded buffers — the fail-closed declines +plus the knobs above are the whole posture. + +## Non-goals + +Per `vision.md`: no in-family SSH protocol machinery (ever); not an +sshd (no local accounts, no OpenSSH `authorized_keys` semantics, no +PAM); no sftp serving until alkfs lands (OQ-13 — the feature exists as +a declining placeholder); no binary/CLI; no password or +keyboard-interactive auth in either direction; no x11 or agent +forwarding at all (not even feature-gated — ADR-003 §5). + +## Lifecycle note + +This crate follows the family crate template (alktty root pattern): +`src/` modules re-exported from `lib.rs`, one module per file, public +API = crate-root re-exports. The dependency set (russh per ADR-002, +alkcall types, per-feature sibling deps: alktty/alktunnels/alkgit) and +the manifest (`rust-version` 1.89, edition 2024) land at +implementation start — ADR-002 records the manifest decisions. + +## References + +- [README.md](README.md) (index), [open-questions.md](open-questions.md) (OQ-01…13) +- ADRs 001–006 in [decisions/](decisions/) +- `docs/research/` — vision, POC findings (1/2/3), russh inventory, + auth bridge, mapping table (validated), reverse-runner extraction +- Family ADR-003 (the structural decision), family ADR-002 (alkforge; + why exclusion matters) \ No newline at end of file