docs(release): wave 5 release readiness — crate docs, README, CHANGELOG, matrix, dry-run (w5-release-readiness)
- resolve the rustdoc warning set (9 incl. private-link class in client_ops.rs + redundant explicit links + a duplicated tty_map paragraph): cargo doc warning-free on default and --all-features - docs honesty: the five recorded limits ride the published crate-root rustdoc (§Known limits: write_all no-deadline, PTY rule-5, port-0-only mint echo, no-reconnect policy, no-accept-all); crate docs state the feature graph + the fail-closed invariant - two no_run usage doctests (server mount + dial) — the README examples stay compile-checked in every matrix build - README.md lands (the door, feature graph, examples, custody posture, license); CHANGELOG.md carries the 0.1.0 founding entry - feature matrix green (6 test builds, clippy x2 -D warnings, fmt, doc) + the fuzz corpus gate (11 invariants) - cargo publish --dry-run --allow-dirty clean (76 files packaged + verified; file list checked against the package policy; manifest sanity recorded); the freeze inventory recorded for the gate Verification: scripts/matrix.sh (all builds), fuzz corpus gate, taskgraph validate
This commit is contained in:
1 parent
d37cbc0c8a
commit
ab5236ebbb
9 files changed
+634
-53
No files matched your search
+143
-27
@@ -4,33 +4,149 @@ All notable changes to this crate are documented here. The format is
|
||||
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
this crate adheres to [Semantic Versioning](https://semver.org/).
|
||||
|
||||
## [Unreleased]
|
||||
## [0.1.0] - 2026-10-30
|
||||
|
||||
The founding entry — the crate's first release. `alkssh` is the alk
|
||||
family's SSH adapter (family ADR-003): a thin wrapper around russh
|
||||
0.65 terminating the vanilla-ssh wire and bridging to/from alkcall
|
||||
channels — the two-direction door for vanilla clients. The wire is
|
||||
russh's; the crate's substance is the channel-type mappings
|
||||
(session/pty → alktty, direct/forwarded-tcpip → alktunnels, git exec →
|
||||
alkgit), each behind its cargo feature in a flat, independent graph
|
||||
(ADR-003): **a default build has no capabilities and serves nothing
|
||||
but clean declines** — every unfeatured or unimplemented request is
|
||||
declined fail-closed, never half-served or silently fallen back, and
|
||||
never advertised. Identity is the family's: a verified key's
|
||||
fingerprint → alkcall `IdentityProvider`; keys the crate holds come
|
||||
from alkvault-backed sources, encrypted at rest, decrypted at use,
|
||||
never env vars (ADR-005). The crate is a door, not a backend — no
|
||||
binary, no deployment policy; a downstream app embeds it, enables
|
||||
features, and brings identity/ACL configuration.
|
||||
|
||||
### Added
|
||||
|
||||
- Initialized: docs-only phase 0 base — SDD process (`docs/sdd_process.md`),
|
||||
opencode agent definitions, rust crate manifest (lib only, no
|
||||
dependencies yet), MIT OR Apache-2.0 licensing. The russh pin, the
|
||||
capability feature graph, and the channel-type mapping table are
|
||||
phase 0 research output (family ADR-003, OQ-08 remaining scope).
|
||||
- Phase 0 research set under `docs/research/`: `vision.md` (scope
|
||||
questions), `russh.md` (0.64.1-era inventory + pin considerations),
|
||||
`auth-bridge.md` (fingerprint → identity server path; vault-backed
|
||||
at-rest client key storage — the vault as secret store, not key
|
||||
derivation), `mapping-table.md` (channel type/request → family seam
|
||||
+ feature), `pocs.md` (POC-1..3 plan).
|
||||
- Phase 0 completed — all three POCs passed against the current
|
||||
published russh with vanilla tooling (121 checks total); findings
|
||||
in `poc-1-auth-bridge.md` (31), `poc-2-vault-keys.md` (39),
|
||||
`poc-3-mapping-bakeoff.md` (51, three mapping-table corrections
|
||||
recorded), plus the reverse-runner client-direction extraction
|
||||
(`reverse-runner.md`). POC source is disposable, never merged.
|
||||
- Phase 1 architecture under `docs/architecture/`: the index +
|
||||
open-questions tracker (OQ-01…13), the spec docs
|
||||
(`overview.md`, `mapping.md`, `client.md`, `auth.md`), and ADRs
|
||||
001–006 (Accepted) — the `alk/ssh` ALPN pin; the russh 0.65.0 pin
|
||||
(aws-lc-rs, no compression, `rsa` off); the capability feature
|
||||
graph (flat, two-direction, `dynamic-forward` retired); auth
|
||||
dispositions; vault-key custody; and the mapping/dispatch
|
||||
decisions (route-1 bridge, exec-only git, parse-only session
|
||||
dispatch).
|
||||
**Phase 0/1 — research and architecture** (the contract the
|
||||
implementation lands):
|
||||
|
||||
- SDD process, phase 0 research set (`docs/research/`), and the three
|
||||
passed POCs (auth bridge, vault keys, mapping bake-off; findings in
|
||||
`poc-1..3` + `reverse-runner.md`; POC source never merged).
|
||||
- Phase 1 architecture under `docs/architecture/`: the spec docs and
|
||||
ADRs 001–006 (Accepted) — the `alk/ssh` ALPN pin; the russh 0.65.0
|
||||
pin (aws-lc-rs, no compression, rsa off); the capability feature
|
||||
graph; auth dispositions; vault-key custody; the mapping/bridge/
|
||||
dispatch decisions.
|
||||
|
||||
**Wave 1 — foundations:**
|
||||
|
||||
- The manifest: the russh 0.65 pin (`aws-lc-rs`, default features
|
||||
off), the flat feature graph (`session` / `git` / `tunnel` /
|
||||
`sftp`, `default = []`).
|
||||
- The fail-closed error taxonomy (`thiserror`): `AdapterError`,
|
||||
`DialError`, `KeySourceError`, `OpError`, `OpenDeclineReason` —
|
||||
declines are dispositions, never panics; no `unwrap` outside tests.
|
||||
- Vault-backed key custody in code: the `KeySource` /
|
||||
`HostKeySource` seams and the ADR-005 implementations
|
||||
(`VaultKeySource`, `VaultHostKeySource`) — decrypt-at-use /
|
||||
decrypt-at-construction, the `Zeroizing` seam, one
|
||||
`decode_secret_key` conversion.
|
||||
- The `HostKeyPolicy` seam (OQ-05, policy-blind) with the fail-closed
|
||||
default `RejectAll` — no accept-all built-in exists in the crate.
|
||||
- The contract-invariant fingerprint computation (the crate's single
|
||||
fingerprint path, the `SHA256:` format both the auth bridge and the
|
||||
host-key policy consume).
|
||||
- The config surface: `ServeConfig`, `DialConfig`, `Limits` (the
|
||||
bounded-resources budget model — wall clock always set, channel
|
||||
caps, keepalive posture) — unset fields decline at request time
|
||||
(OQ-08), construction never validates.
|
||||
|
||||
**Wave 2 — the server stack:**
|
||||
|
||||
- `SshAdapter` — the mount point: `SshAdapter::new(serve)` builds the
|
||||
russh server from the config (fail-closed on key-source failure);
|
||||
`adapter.handle(stream)` serves one connection.
|
||||
- The auth bridge: russh auth hooks → ADR-004 dispositions —
|
||||
verified-key fingerprint → `IdentityProvider::resolve_from_fingerprint`,
|
||||
fail-closed on every path; bounded auth attempts.
|
||||
- Parse-only dispatch + the fail-closed decline core: every unroutable
|
||||
or unfeatured request answers a clean, channel-scoped decline; the
|
||||
spawned serving-path shapes (`channel_failure` then close).
|
||||
- Spawn-and-return plumbing (ADR-006 decision 4): serving paths run
|
||||
spawned (russh handlers return fast at this pin), with the
|
||||
exit-status-before-close mechanics.
|
||||
- Bounded resources at the handler surface: per-connection channel
|
||||
caps (refuse-never-queue) and listener caps.
|
||||
- The in-process loopback harness (the contract-test vehicle).
|
||||
|
||||
**Wave 3 — the server mappings (the spine):**
|
||||
|
||||
- Per-feature seam handles (`SessionSeam`, `GitSeam`,
|
||||
`TunnelListenSeam`): unset seam → decline even with the feature on.
|
||||
- Decline-path tracing (`tracing` events on every decline class).
|
||||
- The git mapping (exec-only, ADR-006): the full exec grammar —
|
||||
`git-{upload,receive}-pack '/<repo>'` parsed and dispatched to
|
||||
alkgit; a subsystem request can never carry the repository and
|
||||
declines; `1` is honestly reported for push `ng`.
|
||||
- The session mapping: pty/shell/exec through the alktty seam over a
|
||||
translation bridge (chunk-framed both directions), OQ-44's reserved
|
||||
ttymodes JSON, the exit-chunk → SSH exit derivation (rules 1–6),
|
||||
control rows (window-change, signal) on a bounded per-channel
|
||||
surface.
|
||||
- Tunnel leg 1: the route-1 bridge — `direct-tcpip` /
|
||||
`direct-streamlocal` opens ride the assembly's `TunnelSession` over
|
||||
the family fabric; fabric failure is a clean `ConnectFailed` reject.
|
||||
- Tunnel leg 2: the `-R` listen flow — `tcpip_forward` /
|
||||
`streamlocal_forward` dial the fabric, arrivals correlate through
|
||||
the token router (miss → teardown-at-birth), the data leg opens
|
||||
`forwarded-tcpip`/`forwarded-streamlocal` back toward the client,
|
||||
port-0 mints echo the actually-bound port.
|
||||
|
||||
**Wave 4 — the client direction:**
|
||||
|
||||
- The dial surface: `dial(DialConfig)` — key load (decrypt-at-dial),
|
||||
bounded russh client config, the host-key policy call, exactly one
|
||||
publickey attempt; `SshConnection` the handle.
|
||||
- The op rows: `exec` / `pty_exec` / `shell` (the alktty chunk plane),
|
||||
`tunnel_direct` / `tunnel_direct_unix` (byte duplex),
|
||||
`git_service` (pack duplex + consumed exit) — plus the `sftp`
|
||||
decline row (always declines, every build).
|
||||
- The `-R` leg: `reverse_listen` / `reverse_listen_unix` + the
|
||||
`AcceptorRegistry` (refuse-never-replace) and the acceptor traits —
|
||||
the hub asks the vanilla server to listen and serves the accepted
|
||||
channels.
|
||||
- Lifecycle primitives (OQ-12): explicitly cancellable listener
|
||||
handles, clean `close()` (`Disconnect::ByApplication`) — drop is
|
||||
never the clean path at this pin; no reconnect/restore policy (the
|
||||
crate ships primitives, the assembly composes).
|
||||
- Hardening riders: the git row's status-before-close by construction,
|
||||
the channel-not-held guard wire rows, tty control-row coverage, the
|
||||
`ListenRouter` token-collision pre-check.
|
||||
|
||||
**Wave 5 — validation and release:**
|
||||
|
||||
- The decline-matrix contract suite: every mapping-decline row × every
|
||||
build it can appear in × both directions (the matrix runner
|
||||
`scripts/matrix.sh` — one command over all feature builds).
|
||||
- Real-tool e2e both directions: OpenSSH (pty/shell/exec, `-L`, `-R`,
|
||||
`-D` + curl), git clone and push through the door, and a real
|
||||
`sshd` against the client direction (exec, direct-tcpip dial, `-R`
|
||||
listen + acceptor) — check-gated, skipped rows carry reasons.
|
||||
- The `fuzz/` sidecar: the ingress surfaces (exec parse grammar,
|
||||
fingerprint format, key decode, `TerminalParamsWire`) with the
|
||||
committed-seed corpus gate (`cargo test --manifest-path
|
||||
fuzz/shared/Cargo.toml`).
|
||||
- Release readiness: warning-free crate docs with the known-limits
|
||||
records riding the published rustdoc, this README, and the publish
|
||||
dry-run posture.
|
||||
|
||||
### Notes
|
||||
|
||||
- `sftp` is a feature of decline: enabling it adds no capability —
|
||||
the `sftp` subsystem request still declines in every build until
|
||||
the alkfs mapping lands (OQ-13; the decline stays contract
|
||||
territory).
|
||||
- The `env` request declines unconditionally (OQ-09): session cwd/env
|
||||
are assembly-supplied, never wire-derived.
|
||||
- The russh pin (0.65, `aws-lc-rs`, `des`/`dsa` never enabled) is a
|
||||
wire-relevant one-way door: bumps are deliberate, tested, and
|
||||
recorded (ADR-002).
|
||||
@@ -0,0 +1,158 @@
|
||||
# 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 feature graph — fail-closed
|
||||
|
||||
`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 | — |
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
— and 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 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`.
|
||||
@@ -35,7 +35,7 @@
|
||||
//! `ForwardedAcceptor` vocabulary is grown, not forked: the payload
|
||||
//! fields are the same shapes (the open's originator fields + the
|
||||
//! channel duplex), the duplex now family-typed
|
||||
//! ([`TunnelChannel`](crate::TunnelChannel) — the `-L` rows' byte
|
||||
//! ([`TunnelChannel`] — the `-L` rows' byte
|
||||
//! duplex, one type both legs ride).
|
||||
//! - **Install is refuse-never-replace** ([`AcceptorRegistry::
|
||||
//! install_tcpip`]'s held-slot error): the same posture recorded
|
||||
@@ -119,7 +119,7 @@
|
||||
//! ## The data leg
|
||||
//!
|
||||
//! Per accepted open: the hook accepts the channel and hands the
|
||||
//! assembly the open — the [`TunnelChannel`](crate::TunnelChannel)
|
||||
//! assembly the open — the [`TunnelChannel`]
|
||||
//! duplex + the open's echo/originator fields (the payload types
|
||||
//! below) — on a spawned task (the spawn-and-return rule's client
|
||||
//! mirror: the hook runs in the packet loop and returns fast). The
|
||||
@@ -221,7 +221,7 @@ impl std::fmt::Debug for ForwardedStreamlocal {
|
||||
/// packet loop): the assembly's pump/home of the open lives inside
|
||||
/// it; the crate releases its handoff counter when the handoff task
|
||||
/// ends — the call returning *or the task unwinding*
|
||||
/// ([`HandoffCapGuard`]) — so the family shape (hand off to a queue,
|
||||
/// (`HandoffCapGuard`) — so the family shape (hand off to a queue,
|
||||
/// return) keeps the counter's meaning "open accepted, handoff in
|
||||
/// flight".
|
||||
#[async_trait]
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
//! through the **acceptor registry** ([`crate::client_accept`] —
|
||||
//! the `w4-reverse-listen` surface): served only under a registered
|
||||
//! acceptor for that type and under the `tunnel` feature (the
|
||||
//! [`with_tunnel_surface`](ClientHandler::with_tunnel_surface)
|
||||
//! [`with_tunnel_surface`]'s tunnel surface
|
||||
//! wiring; a build without `tunnel` has no registry — every hook
|
||||
//! declines); unregistered/unfeatured opens answer
|
||||
//! [`ChannelOpenFailure::AdministrativelyProhibited`] — a clean
|
||||
|
||||
+7
-6
@@ -221,7 +221,7 @@ impl SshConnection {
|
||||
/// A remote **session** — the family session plane over the runner
|
||||
/// (the session rows' surface): the alktty chunk framing in both
|
||||
/// directions, exactly the plane the server direction's session
|
||||
/// bridge speaks ([`crate::tty_map`]), reversed. Two duplex halves
|
||||
/// bridge speaks (`tty_map`), reversed. Two duplex halves
|
||||
/// the hub drives; the pumps are spawned at the op's launch (§the
|
||||
/// family plane in the module docs for the full chunk map). Dropping
|
||||
/// the session (both halves) is wire-clean: the input pump's
|
||||
@@ -269,7 +269,8 @@ impl SshConnection {
|
||||
}
|
||||
|
||||
/// The pty-mode exec row (`ssh -t host "cmd"`): the pty request
|
||||
/// carries the family [`TerminalParamsWire`](alktty::negotiation::
|
||||
/// carries the family
|
||||
/// [`alktty::negotiation::TerminalParamsWire`](alktty::negotiation::
|
||||
/// TerminalParamsWire) first (its acceptance drained), then the
|
||||
/// exec. **A refused pty refuses the whole row** — never a
|
||||
/// half-capability, never a silent pty-less fall.
|
||||
@@ -683,17 +684,17 @@ impl std::fmt::Debug for GitPackSession {
|
||||
impl SshConnection {
|
||||
/// The upstream git row: the shared exec grammar's command
|
||||
/// (`git-{upload,receive}-pack '/<repo>'` — built through
|
||||
/// [`crate::dispatch`]'s construction arm, the parse arm's exact
|
||||
/// `crate::dispatch`'s construction arm, the parse arm's exact
|
||||
/// inverse; no forked grammar), pointed at the vanilla `sshd`:
|
||||
/// typed open + `exec`, the pack duplex pumped, the exit report
|
||||
/// consumed.
|
||||
///
|
||||
/// **The pre-wire gates, riding the shared grammar's verdicts:**
|
||||
/// the repo id validates against the ADR-008 grammar (the git
|
||||
/// row's server-direction gate mirrored,
|
||||
/// [`crate::git_map`]) → [`OpError::InvalidRepoId`](crate::OpError);
|
||||
/// row's server-direction gate mirrored, `crate::git_map`) →
|
||||
/// [`OpError::InvalidRepoId`](crate::OpError);
|
||||
/// the constructed command is checked against the exec-length
|
||||
/// budget ([`Limits::max_exec_command_len`]) →
|
||||
/// budget ([`crate::Limits::max_exec_command_len`]) →
|
||||
/// [`OpError::CommandTooLong`](crate::OpError) — both **before any
|
||||
/// wire op**. (The grammar's other decline classes —
|
||||
/// `unknown-service` / `service-only` — are structurally
|
||||
|
||||
+1
-1
@@ -92,7 +92,7 @@ pub enum KeySourceError {
|
||||
/// ADR-005 semantics: the vault decrypted *something*
|
||||
/// successfully and the plaintext is not an OpenSSH
|
||||
/// private-key PEM (decision 1's inner format). Distinct from
|
||||
/// [`Decryption`](KeySourceError::Decryption) on purpose — the
|
||||
/// [`KeySourceError::Decryption`] on purpose — the
|
||||
/// custody boundary held, what was guarded is not a key. The
|
||||
/// underlying parse error's message is deliberately *not*
|
||||
/// carried — the machinery's text can echo plaintext-shaped
|
||||
|
||||
+167
-3
@@ -24,6 +24,170 @@
|
||||
//! lifecycle primitives — are landed through wave 4; the validation
|
||||
//! suite and the release are wave 5). See also `docs/research/`,
|
||||
//! `docs/plans/implementation.md`, and family ADR-003.
|
||||
//!
|
||||
//! # The feature graph (ADR-003 — flat, independent, fail-closed)
|
||||
//!
|
||||
//! Every channel-type mapping is a cargo feature; a session opening an
|
||||
//! unfeatured or unimplemented channel type is declined fail-closed —
|
||||
//! never a half-capability, never a silent fallback. **A default build
|
||||
//! (no features) has no capabilities**: it can serve nothing but clean
|
||||
//! declines. No feature implies another; the graph is flat.
|
||||
//!
|
||||
//! - (default, no features) — serves declines only: every open, exec,
|
||||
//! listen request answers a clean, channel-scoped refusal.
|
||||
//! - `session` — pty/shell/exec rows → the `RemoteSession` chunk
|
||||
//! plane (alktty).
|
||||
//! - `git` — the exec-only git rows (`git-{upload,receive}-pack`) →
|
||||
//! alkgit; a git *subsystem* request always declines (the repository
|
||||
//! can never ride a subsystem channel).
|
||||
//! - `tunnel` — `direct-tcpip` / `direct-streamlocal` and the `-R`
|
||||
//! legs (listen, `forwarded-tcpip` / `forwarded-streamlocal`) →
|
||||
//! alktunnels.
|
||||
//! - `sftp` — a reserved, currently-empty placeholder (OQ-13): the
|
||||
//! `sftp` subsystem request still declines in every build until the
|
||||
//! alkfs mapping lands; the feature exists so the decline stays
|
||||
//! contract territory (tested in every matrix build).
|
||||
//!
|
||||
//! Advertisement is honest: an unfeatured channel type is never
|
||||
//! advertised, only declined (`docs/architecture/mapping.md` §Decline
|
||||
//! matrix — the fail-closed invariant is contract territory on both
|
||||
//! sides of every feature).
|
||||
//!
|
||||
//! # Known limits (recorded dispositions riding the published docs)
|
||||
//!
|
||||
//! The records below are deliberate dispositions — documented limits
|
||||
//! with their reasons, mirrored in the module docs and the
|
||||
//! architecture spec, not defects awaiting a fix:
|
||||
//!
|
||||
//! - **The session rows' stdout `write_all` carries no deadline.**
|
||||
//! Backpressure is bounded (the bounded bridging duplex, russh's
|
||||
//! per-channel window, the client's own flow control) and liveness
|
||||
//! is covered by the always-set
|
||||
//! [`Limits::inactivity_timeout`]; a deadline here would be a
|
||||
//! behavior change (a slow client may stall its stream — vanilla
|
||||
//! sshd's posture included). (`tty_map.rs` module docs.)
|
||||
//! - **The PTY path's exit-signal fidelity is limited by upstream**
|
||||
//! (exit-currency rule 5): signal-terminated PTY sessions surface as
|
||||
//! `exit-status(1)` because the backend's reap surface loses the
|
||||
//! signal number; the pipe path encodes negatives faithfully. An
|
||||
//! alktty/portable-pty ask — the contract adapts automatically
|
||||
//! when upstream conforms.
|
||||
//! - **The port-0 mint is the only bound-port echo the wire carries**
|
||||
//! (russh/sshd parity): a `tcpip_forward` success reply carries the
|
||||
//! allocated port only for a `port = 0` request; an explicit-port
|
||||
//! request's bound port is by definition the requested one — the
|
||||
//! crate surfaces it as such, never a fabricated number.
|
||||
//! - **No reconnect/restore policy.** The client direction ships
|
||||
//! lifecycle *primitives* (bound ports, cancellable handles, clean
|
||||
//! [`SshConnection::close`]); reconnect-and-restore (rebinding
|
||||
//! persisted tunnel ports) is assembly composition over them — the
|
||||
//! crate never owns the policy. Drop is never the clean path at
|
||||
//! this russh pin: only an explicit `close` sends the clean
|
||||
//! disconnect.
|
||||
//! - **No accept-all host-key policy exists in the crate.** The
|
||||
//! built-in default is [`RejectAll`] (a dial with it can never
|
||||
//! connect — by design); the crate is policy-blind and any
|
||||
//! permissive trust policy is the assembly's to write. A
|
||||
//! fail-closed policy posture: when a policy cannot decide, it must
|
||||
//! decline.
|
||||
//!
|
||||
//! # Usage
|
||||
//!
|
||||
//! The crate is a door, not a deployment: an assembly embeds it,
|
||||
//! enables the features it wants, and injects the seams (identity,
|
||||
//! key sources, the per-feature family handles). The examples are
|
||||
//! compile-checked (`no_run`) doctests — honest sketches, not
|
||||
//! executable code.
|
||||
//!
|
||||
//! A server mount (the default build — a decline-only door; the
|
||||
//! assembly's real sources are the vault-backed [`VaultKeySource`] /
|
||||
//! [`VaultHostKeySource`] shapes):
|
||||
//!
|
||||
//! ```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
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! # tokio::runtime::Runtime::new()?.block_on(async {
|
||||
//! 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?;
|
||||
//! # Ok::<(), alkssh::AdapterError>(())
|
||||
//! # })?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! A dial (the client direction; the built-in [`RejectAll`]
|
||||
//! host-key policy connects to nothing — by design, the assembly's
|
||||
//! policy replaces it):
|
||||
//!
|
||||
//! ```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
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! # tokio::runtime::Runtime::new()?.block_on(async {
|
||||
//! let conn = alkssh::dial(alkssh::DialConfig::new(
|
||||
//! "hub.example:22",
|
||||
//! "runner",
|
||||
//! Arc::new(DialKey),
|
||||
//! Arc::new(alkssh::RejectAll),
|
||||
//! ))
|
||||
//! .await?;
|
||||
//!
|
||||
//! # #[cfg(feature = "session")]
|
||||
//! # {
|
||||
//! # let _ = conn.exec("uptime"); // the op rows (feature-gated)
|
||||
//! # }
|
||||
//! conn.close().await; // clean disconnect — drop is never the clean path
|
||||
//! # Ok::<(), alkssh::DialError>(())
|
||||
//! # })?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
mod adapter;
|
||||
mod channel_cap;
|
||||
@@ -71,11 +235,11 @@ pub use client_accept::{
|
||||
#[doc(hidden)]
|
||||
pub use client_handler::ClientHandler;
|
||||
/// The client op rows' surfaces (`w4-op-rows`):
|
||||
/// [`RemoteSession`](client_ops::RemoteSession) — the exec/pty rows'
|
||||
/// [`RemoteSession`] — the exec/pty rows'
|
||||
/// family chunk plane (under `session`),
|
||||
/// [`TunnelChannel`](client_ops::TunnelChannel) — the direct
|
||||
/// [`TunnelChannel`] — the direct
|
||||
/// rows' byte duplex (under `tunnel`),
|
||||
/// [`GitPackSession`](client_ops::GitPackSession) — the upstream git
|
||||
/// [`GitPackSession`] — the upstream git
|
||||
/// row's pack duplex + consumed exit (under `git`). The op methods
|
||||
/// live on [`SshConnection`] (the op-row surface). Not a re-export
|
||||
/// here: the `sftp` row is [`SshConnection::sftp`] itself — the
|
||||
|
||||
@@ -152,9 +152,6 @@
|
||||
//! The report is sent **before** close, from the spawned serving
|
||||
//! task (ADR-006 decision 4, via the cloned `Session::handle()`).
|
||||
//!
|
||||
//! The report is sent **before** close, from the spawned serving
|
||||
//! task (ADR-006 decision 4, via the cloned `Session::handle()`).
|
||||
//!
|
||||
//! ## Cancellation (bounded resources + alktty's kill-on-drop)
|
||||
//!
|
||||
//! The serving task watches two signals: the **per-channel cancel**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: w5-release-readiness
|
||||
name: Wave 5 — release readiness (docs, feature matrix, CHANGELOG/README, dry-run)
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [w5-decline-matrix, w5-e2e-server, w5-e2e-client, w5-fuzz-harness]
|
||||
scope: moderate
|
||||
risk: low
|
||||
@@ -52,18 +52,18 @@ bullet; the alkgit `w5-release-readiness` shape):
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `cargo doc --no-deps` (+ `--all-features`) warning-free or
|
||||
- [x] `cargo doc --no-deps` (+ `--all-features`) warning-free or
|
||||
every residual dispositioned; the docs-honesty pass complete
|
||||
(the known-limits records ride the published docs)
|
||||
- [ ] README.md landed (what/why, feature graph + fail-closed,
|
||||
- [x] README.md landed (what/why, feature graph + fail-closed,
|
||||
compile-checked examples, custody posture, license)
|
||||
- [ ] CHANGELOG.md's 0.1.0 entry landed
|
||||
- [ ] The feature matrix green (default / each feature /
|
||||
- [x] CHANGELOG.md's 0.1.0 entry landed
|
||||
- [x] The feature matrix green (default / each feature /
|
||||
`--all-features`: test + clippy + doc)
|
||||
- [ ] `cargo publish --dry-run --allow-dirty` clean; the packaged
|
||||
- [x] `cargo publish --dry-run --allow-dirty` clean; the packaged
|
||||
file list verified against the package policy; manifest sanity
|
||||
recorded
|
||||
- [ ] The freeze inventory recorded for the gate
|
||||
- [x] The freeze inventory recorded for the gate
|
||||
|
||||
## References
|
||||
|
||||
@@ -79,8 +79,153 @@ bullet; the alkgit `w5-release-readiness` shape):
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
**Crate docs.** `cargo doc --no-deps` is warning-free on both the
|
||||
default build and `--all-features`. The pre-existing doc-link debt is
|
||||
resolved (not dispositioned): 9 warnings (1 duplicate) at
|
||||
`--all-features` — the `client_ops.rs` private-link class
|
||||
(`crate::tty_map` / `crate::dispatch` / `crate::git_map` — private
|
||||
modules; de-linked to backtick prose), the two
|
||||
`rustdoc::redundant_explicit_links` spans (the `(client_ops::…)`
|
||||
destinations on the `lib.rs` re-export docs and two
|
||||
`[`TunnelChannel`](crate::TunnelChannel)` + `with_tunnel_surface` /
|
||||
`KeySourceError::Decryption` same-item links in
|
||||
`client_accept.rs`/`client_handler.rs`/`error.rs`), the broken
|
||||
`TerminalParamsWire` path (now a resolution-correct target through
|
||||
its wrapped re-export form) and the unresolved `Limits::…`
|
||||
intra-doc link (now `crate::Limits::…`); plus the twin
|
||||
duplicated "report is sent before close" paragraph in `tty_map.rs`
|
||||
removed one. **Docs honesty:** the five recorded limits now ride the
|
||||
*published* docs (the crate-root rustdoc §Known limits — the
|
||||
`write_all` no-deadline record, the PTY rule-5 upstream fidelity
|
||||
limit, the port-0-only mint echo wire fact, the no-reconnect-policy
|
||||
boundary, the no-accept-all policy posture) with their module-doc
|
||||
mirrors verified still in place (`tty_map.rs`, `exit_code.rs`
|
||||
rule 5, `client_accept.rs`/`tunnel_map.rs`, `client.rs`
|
||||
§Lifecycle posture, `host_key.rs`); crate-level docs state the
|
||||
feature graph (each feature's row + the `default = []` decline-only
|
||||
fact + no-feature-implies-another + honest advertisement) and the
|
||||
fail-closed invariant.
|
||||
|
||||
**Compile-checked examples.** Two `no_run` doctests in the
|
||||
crate-root docs (they run in every matrix build — the publish task's
|
||||
verification set keeps them compile-checked with zero extra
|
||||
vehicle): a server mount (`ServeConfig::new` → `SshAdapter::new` →
|
||||
`adapter.handle`, on the default build — sketch key/identity seams
|
||||
returning `Absent`/`None`, prose points at the vault-backed shapes)
|
||||
and a dial (`dial(DialConfig)` → `close()`, the op rows referenced in
|
||||
prose with the feature gate noted; `RejectAll` named as
|
||||
connects-to-nothing by design). README mirrors them.
|
||||
|
||||
**README.md.** Landed (none existed): what the crate is (the
|
||||
two-direction door, russh owns the wire, door-not-backend), the
|
||||
feature matrix table + the fail-closed posture (a default build serves
|
||||
nothing but declines; unset-config declines; never advertised), the
|
||||
two usage sketches (mirroring the doctests), the custody/security
|
||||
posture summary (vault-backed sources — encrypted at rest, decrypted
|
||||
at use, zeroized; no env vars, no secrets on the wire; publickey-only
|
||||
single attempt; fail-closed host-key default with no accept-all;
|
||||
ACL-gated surface; bounded resources + the known-limits pointer; the
|
||||
decline matrix as contract), the docs pointers, the license note.
|
||||
|
||||
**CHANGELOG.md.** The 0.1.0 entry landed (founding entry, dated
|
||||
2026-10-30): the crate story (the two-direction door, the flat
|
||||
fail-closed graph, ADR-003) + the posture statement, then the
|
||||
landed surface wave by wave — the phase 0/1 contract, waves 1–5
|
||||
(foundations, server stack, server mappings/spine, client direction,
|
||||
validation+release) — plus Notes (the sftp decline-only feature fact,
|
||||
the OQ-09 env posture, the russh-pin one-way-door record). Only
|
||||
served surface is listed; the never-features
|
||||
(x11/`auth-agent`/subsystem/`env`) are named as the declines they
|
||||
are.
|
||||
|
||||
**Feature matrix** (`scripts/matrix.sh`, the one command): all six
|
||||
builds' `cargo test` green (none / git / session / tunnel / sftp-only
|
||||
/ `--all-features` — 108 result-ok reports across the matrix,
|
||||
including both doctests per build), clippy ×2 (`--all-targets -D
|
||||
warnings`, with and without `--all-features`) clean, `cargo fmt
|
||||
--check` clean, `cargo doc --no-deps` clean; plus the fuzz corpus
|
||||
gate (`cargo test --manifest-path fuzz/shared/Cargo.toml` — 11
|
||||
invariants green).
|
||||
|
||||
**Publish dry-run.** `cargo publish --dry-run --allow-dirty` clean —
|
||||
packaged 76 files, verified (the packaged tree compiles), upload
|
||||
aborted by dry run as expected. The packaged file list verified
|
||||
against the package policy (`cargo package --list`): exactly `src/`
|
||||
(20 files), `tests/` (+ `common/` doubles + `vectors/`), the
|
||||
architecture docs (`docs/architecture/` — the authoritative
|
||||
contract), `README.md`, `CHANGELOG.md`, `LICENSE-MIT` /
|
||||
`LICENSE-APACHE`, the manifests (`Cargo.toml`/`.orig`,
|
||||
`Cargo.lock`, `.gitignore`, `.cargo_vcs_info.json`) and
|
||||
`scripts/matrix.sh` (the verification tooling, not process surface);
|
||||
none of the excluded surface appears (`.opencode/`, `AGENTS.md`,
|
||||
`docs/reviews|plans|research/` / `docs/sdd_process.md`, `tasks/`,
|
||||
`fuzz/` — none listed). **Manifest sanity recorded:** version 0.1.0
|
||||
(no bump — `w5-publish` verifies), `rust-version` 1.89, edition
|
||||
2024, `license = "MIT OR Apache-2.0"` with both license files
|
||||
present, the pins as ADR-002 records them (russh 0.65
|
||||
`default-features = false` + `aws-lc-rs`; alkcall 0.10; alkvault
|
||||
0.1; the per-feature optional siblings alktty 0.8.1 / alkgit 0.1.0 /
|
||||
alktunnels 0.4.1), the feature graph exactly ADR-003's, the
|
||||
package-policy `exclude` list in place.
|
||||
|
||||
**The freeze inventory (for `review-wave-5` and `w5-publish` — the
|
||||
surfaces the 0.1 publish makes one-way; further wire-relevant change
|
||||
is a new ADR + a semver event):**
|
||||
|
||||
1. **The mapping contracts** — the channel-type → family-protocol
|
||||
row semantics as landed (`docs/architecture/mapping.md` executed
|
||||
by `dispatch.rs`/`tty_map.rs`/`git_map.rs`/`tunnel_map.rs`/
|
||||
`client_ops.rs`): the route-1 bridge flow (open-direct →
|
||||
accept-after-fabric-success), the exec-only git grammar + its
|
||||
verdicts, the parse-only dispatch + the translation bridge
|
||||
(chunk framing, the zero-length stdin sentinel, the
|
||||
negotiation-error frame → 255), the exit derivation rules 1–6,
|
||||
the client op-row planes (the family chunk/duplex surfaces, the
|
||||
acceptor registry + payload types, the lifecycle primitives).
|
||||
2. **The honest-advertisement posture** — the fail-closed invariant
|
||||
as behavior: an unfeatured/unimplemented channel type is declined
|
||||
(never advertised, never half-served); a default build serves
|
||||
declines only; the `sftp` placeholder stays a decline in every
|
||||
build (OQ-13).
|
||||
3. **The decline-matrix behavior** — the decline classes and shapes:
|
||||
the `OpenDeclineReason` vocabulary as exposed, the
|
||||
spawned-`channel_failure`-then-close serving-path shape, the
|
||||
`ConnectFailed` fabric reject, teardown-at-birth, the
|
||||
token-collision refusal, the config-driven
|
||||
(`seam-unconfigured`/unset-field) declines, the always-`sftp`
|
||||
decline row — as pinned by the matrix suite.
|
||||
4. **The fingerprint string format** — the `SHA256:` contract
|
||||
format of `crate::fingerprint()` (the crate's single fingerprint
|
||||
path): both the auth bridge's identity resolution and the
|
||||
host-key policy's exact-string pin comparison consume it;
|
||||
changing the format breaks every pin store and is a one-way door.
|
||||
5. **The russh pin's wire posture** — russh 0.65.0 with
|
||||
`aws-lc-rs` (no compression, `rsa` off, `des`/`dsa` never): the
|
||||
kex/algorithm negotiation surface crosses the crate boundary, so
|
||||
the pin is wire-relevant (ADR-002) — a bump is a deliberate,
|
||||
tested, recorded event. (The `alk/ssh` ALPN pin, ADR-001, is
|
||||
already its own ledger one-way door; the auth surface ships as
|
||||
publickey-only, exactly one attempt.)
|
||||
|
||||
## Summary
|
||||
|
||||
> To be filled on completion
|
||||
Release readiness landed. Crate docs: warning-free × builds
|
||||
(default + `--all-features`), the 9-warning doc-link debt resolved in
|
||||
place (`client_ops.rs`'s private-link class among them), the
|
||||
docs-honesty pass riding the published crate-root rustdoc (§Known
|
||||
limits: the five recorded dispositions — `write_all` no-deadline, PTY
|
||||
rule-5, port-0-only mint echo, no-reconnect policy, no-accept-all —
|
||||
with module-doc mirrors verified) and crate-level docs stating the
|
||||
feature graph + fail-closed invariant; two `no_run` usage doctests
|
||||
(server mount + dial) keep the README examples compile-checked in
|
||||
every matrix build. README.md landed; CHANGELOG.md carries the 0.1.0
|
||||
founding entry (wave-by-wave, honest advertisement). The feature
|
||||
matrix is green end to end (six test builds, clippy ×2, fmt, doc) +
|
||||
the fuzz corpus gate (11 invariants). `cargo publish --dry-run
|
||||
--allow-dirty` clean (76 files packaged + verified; the file list
|
||||
checked against the package policy — architecture docs ship, the
|
||||
process surface does not; manifest sanity recorded). The freeze
|
||||
inventory (mapping contracts, honest advertisement, decline-matrix
|
||||
behavior, fingerprint format, russh pin posture) is recorded above
|
||||
for the gate's read. Ready for `review-wave-5`; `w5-publish`
|
||||
executes only over a green gate.
|
||||
Reference in new issue
Block a user