Files
glm-5.3-flash f4b5f896b4 docs: sweep em dashes from all markdown prose
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.
2026-11-04 20:28:00 +00:00

158 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.