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:
glm-5.3-flash committed 2026-10-30 19:29:23 +00:00
1 parent d37cbc0c8a
commit ab5236ebbb
9 files changed
+634 -53

No files matched your search

+143 -27
View File
@@ -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).
+158
View File
@@ -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`.
+3 -3
View File
@@ -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]
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-3
View File
@@ -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**
+154 -9
View File
@@ -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.