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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user