Renames the written-usage rule into practice across the contract surface: docs/ (architecture, research, plans, sdd), tasks/ (all 43 task files, taskgraph validate passes), AGENTS.md, README.md, CHANGELOG.md, and the .opencode/agents/ prompts. Every replacement is a sense-preserving rewrite (colons, semicolons, commas, parentheses, sentence splits, varied), never a mechanical swap; no spaced hyphens were substituted. Enum fields, frontmatter, tables, wire data, and cited identifiers survive verbatim; en dashes inside numeric ranges keep their meaning. Verification: taskgraph validate (43 tasks), repo grep clean (single exempt hit: the verification command's own search pattern in AGENTS.md), cargo test, clippy --all-targets -D warnings, fmt --check, doc all green.
158 lines
5.7 KiB
Markdown
158 lines
5.7 KiB
Markdown
# alkssh
|
||
|
||
SSH adapter for the alk family: a thin wrapper around
|
||
[russh](https://docs.rs/russh) terminating the vanilla-ssh wire and
|
||
bridging to/from alkcall channels, the SSH counterpart of the
|
||
alkhttp adapter (`family` ADR-003). Vanilla clients terminate vanilla
|
||
ssh; family protocol halves (alktty, alktunnels, alkgit) receive their
|
||
own chunk planes; the door itself speaks no family protocol and owns
|
||
no deployment policy.
|
||
|
||
The crate is a **door, not a backend**: no binary, no front doors; a
|
||
downstream app (alkforge) embeds it, enables the features it wants,
|
||
and brings identity/ACL configuration and the family fabric handles.
|
||
It is two-directional: a server mount serves vanilla ssh into the
|
||
family, and a dial points family ops at a vanilla `sshd`.
|
||
|
||
## The fail-closed feature graph
|
||
|
||
`default = []` by design: **a default build has no capabilities and
|
||
can serve nothing but clean declines.** Every channel-type mapping is
|
||
a cargo feature; each feature is flat and independent (no feature
|
||
implies another):
|
||
|
||
| Feature | Maps | Family protocol |
|
||
|---|---|---|
|
||
| `session` | pty / shell / exec (pipe) rows → chunk-framed session | alktty |
|
||
| `git` | the exec-only git rows (`git-{upload,receive}-pack`) | alkgit |
|
||
| `tunnel` | `direct-tcpip` / `direct-streamlocal`, the `-R` legs | alktunnels |
|
||
| `sftp` | reserved decline placeholder (OQ-13), always declines until the alkfs mapping lands | n/a |
|
||
|
||
Non-mapped surface (x11, `auth-agent`, the `env` request, git *subsystem*
|
||
requests, `sftp` until alkfs) declines fail-closed in every build:
|
||
never a half-capability, never a silent fallback, and never advertised;
|
||
an unfeatured channel type is declined, not advertised. A request
|
||
whose config fields are unset declines too (an unset seam or shell
|
||
argv configures nothing; the feature alone is not a capability).
|
||
|
||
## Usage
|
||
|
||
The assembly injects the seams: an alkcall
|
||
`IdentityProvider` (verified fingerprint → identity), key sources, and
|
||
the per-feature family handles. The examples below are the crate's
|
||
compile-checked doctests (see the crate docs in `src/lib.rs`).
|
||
|
||
A server mount (per accepted TCP connection, run the door):
|
||
|
||
```rust,no_run
|
||
use std::sync::Arc;
|
||
|
||
use async_trait::async_trait;
|
||
|
||
struct Identity;
|
||
|
||
impl alkcall::core::auth::IdentityProvider for Identity {
|
||
fn resolve_from_fingerprint(&self, _: &str) -> Option<alkcall::core::auth::Identity> {
|
||
None // the assembly's registry decides
|
||
}
|
||
|
||
fn resolve_from_token(&self, _: &alkcall::core::auth::AuthToken) -> Option<alkcall::core::auth::Identity> {
|
||
None
|
||
}
|
||
}
|
||
|
||
struct HostKeys;
|
||
|
||
#[async_trait]
|
||
impl alkssh::HostKeySource for HostKeys {
|
||
async fn load(&self) -> Result<Vec<russh::keys::PrivateKey>, alkssh::KeySourceError> {
|
||
Err(alkssh::KeySourceError::Absent) // sketch; vault-backed in prod
|
||
}
|
||
}
|
||
|
||
let serve = alkssh::ServeConfig::new(Arc::new(Identity), Arc::new(HostKeys));
|
||
let adapter = alkssh::SshAdapter::new(serve).await?;
|
||
|
||
// per accepted TCP connection from the assembly's listener:
|
||
// adapter.handle(stream).await?;
|
||
```
|
||
|
||
A dial, family ops pointed upstream at a vanilla server:
|
||
|
||
```rust,no_run
|
||
use std::sync::Arc;
|
||
|
||
use async_trait::async_trait;
|
||
|
||
struct DialKey;
|
||
|
||
#[async_trait]
|
||
impl alkssh::KeySource for DialKey {
|
||
async fn load(&self) -> Result<russh::keys::PrivateKey, alkssh::KeySourceError> {
|
||
Err(alkssh::KeySourceError::Absent) // sketch; vault-backed in prod
|
||
}
|
||
}
|
||
|
||
let conn = alkssh::dial(alkssh::DialConfig::new(
|
||
"hub.example:22",
|
||
"runner",
|
||
Arc::new(DialKey),
|
||
Arc::new(alkssh::RejectAll),
|
||
))
|
||
.await?;
|
||
|
||
// op rows (under their features): conn.exec, conn.tunnel_direct,
|
||
// conn.git_service, conn.reverse_listen; see the crate docs.
|
||
conn.close().await; // clean disconnect; drop is never the clean path
|
||
```
|
||
|
||
Minimal builds compile and doctest with no features enabled:
|
||
|
||
```console
|
||
$ cargo build
|
||
$ cargo test
|
||
```
|
||
|
||
Every capability row additionally rides its feature build (the
|
||
matrix: no features / `git` / `session` / `tunnel` / `sftp` /
|
||
`--all-features`).
|
||
|
||
## Key custody and security posture
|
||
|
||
- **Keys are sources, never static material** (alknet-main's `main`
|
||
key model, ADR-005): the dial/host key seams are
|
||
decrypt-at-use handles over an injected
|
||
[`alkvault`](https://docs.rs/alkvault) service; keys are held
|
||
encrypted at rest, decrypted at use, zeroized through the seam.
|
||
The vault is a secret store on this path, not a key generator.
|
||
- **No env vars, no config-file key material, no secrets on the
|
||
wire.** The standing family posture (family ADR-002 decision 5).
|
||
- **Publickey-only authentication**, exactly one attempt; no
|
||
password, keyboard-interactive, cert, or fallback methods exist on
|
||
the surface.
|
||
- **Host-key trust is assembly policy**: the crate ships a
|
||
`HostKeyPolicy` seam with a fail-closed default (`RejectAll`; a
|
||
dial with it connects to nothing) and **no accept-all built-in**;
|
||
any permissive policy is the assembly's to write.
|
||
- **Injected identity**: every wire-exposed op is checked against
|
||
alkcall's `AccessControl::check(peer_identity)`; internal ops are
|
||
never wire-callable; there is no unauthenticated capability.
|
||
- **Bounded resources everywhere** (`Limits`: wall clock always
|
||
set, channel caps, listener caps, window/packet budgets, auth
|
||
attempt limit): the crate never constructs an unbounded session.
|
||
Known, recorded limits ride the published docs (the crate-root
|
||
rustdoc §Known limits).
|
||
- **Every disabled feature declines**: the decline matrix is contract
|
||
territory tested in every build.
|
||
|
||
## Documentation
|
||
|
||
- Crate docs: `cargo doc --open` (the crate-root docs carry the
|
||
feature graph and the known-limits records).
|
||
- `docs/architecture/`: the authoritative contract: the spec
|
||
(`overview.md`, `mapping.md`, `client.md`, `auth.md`), ADRs
|
||
001–006, and the open-questions tracker.
|
||
|
||
## License
|
||
|
||
MIT OR Apache-2.0; see `LICENSE-MIT` / `LICENSE-APACHE`. |