feat(observe): land the decline-path event vocabulary (w3-decline-tracing)

The wave-2 gate's booked finding: observability on the decline paths.
`tracing` rides the manifest per the ADR-002 toolkit but nothing in
src/ emitted — a decline's "reason" was only the routing decision
documented per build.

- src/observe.rs: the event vocabulary, adopting the family posture
  read from alktty/alktunnels/alkcall/alkhttp (free tracing fns,
  message-last lowercase, flat fields; no info!/trace!, no macros,
  no spans): feature_build + row + channel_id + decline_class
  (open/request/serving/auth) + reason + exec_route (exec rows);
  declines at debug, the serving-path failure arm the vocabulary's
  one warn
- handler.rs: every decline site emits — auth dispositions
  (non-ed25519 / fingerprint-unresolved / the four unimplemented
  methods), open-level declines (unfeatured / never-feature /
  cap-exceeded / tunnel connect-failed stub), request-level
  capability declines (exec route-aware, subsystem, env, x11, agent,
  the -R listen forms), and the seam-stub serving declines with the
  spawned-path lifecycle (started / ended / failed — the shape the
  wave-3 mappings inherit)
- tests/observe.rs: the event-presence pin — a hand-rolled global
  capture subscriber (the recorded test convention; no event-capture
  precedent exists in the family), field-matching rows across the
  default/git/session/tunnel/--all-features builds, and the safety
  scan (no username, no fingerprint ever in an event)
- helper-level exact shapes pinned in src/observe.rs unit rows

Verification: cargo test (default, --features git/session/tunnel,
--all-features all green), clippy x2 --all-targets -D warnings,
fmt --check, cargo doc --no-deps 0 warnings, taskgraph validate.
This commit is contained in:
glm-5.3-flash committed 2026-10-28 17:50:49 +00:00
1 parent 270365ea4c
commit 58d0b86ec5
6 files changed
+1490 -29

No files matched your search

+108 -22
View File
@@ -128,6 +128,18 @@
//! serving task (ADR-006 decision 4, §The spawn-and-return
//! structural constraint above); the wire shape is unchanged.
//!
//! ## The event vocabulary on every decline (`w3-decline-tracing`)
//!
//! Every decline site here emits its event through
//! [`crate::observe`] — the wave-2 gate's booked finding: the
//! deciding context (row, feature build, channel id, decline class,
//! reason) rides the tracing event, not just the routing decision
//! documented per build. The vocabulary and its safety posture (no
//! secrets, no username, no fingerprint — structurally) live in that
//! module; the serving-path lifecycle (started / ended / failure)
//! hangs off the spawned stub body and is what the wave-3 mappings'
//! real serving paths inherit.
//!
//! ## Bounded resources at the handler surface (`w2-bounded-resources`)
//!
//! (AGENTS.md convention 9; `overview.md` §Bounded-resources policy —
@@ -171,6 +183,7 @@ use crate::config::ServeConfig;
use crate::dispatch;
use crate::error::AdapterError;
use crate::fingerprint::fingerprint;
use crate::observe;
use crate::serve;
/// The per-connection handler (one per mounted stream): russh constructs
@@ -261,11 +274,17 @@ impl SessionHandler {
if *signature_algorithm != Algorithm::Ed25519
|| public_key.algorithm() != Algorithm::Ed25519
{
observe::decline_auth("auth_publickey", "non-ed25519");
return None;
}
self.serve
let identity = self
.serve
.identity_provider
.resolve_from_fingerprint(&fingerprint(public_key))
.resolve_from_fingerprint(&fingerprint(public_key));
if identity.is_none() {
observe::decline_auth("auth_publickey", "fingerprint-unresolved");
}
identity
}
/// The `session` channel-open gate (ADR-003 feature graph):
@@ -334,6 +353,7 @@ impl russh::server::Handler for SessionHandler {
/// ADR-004 decision 3: explicit family-contract decline — never the
/// inherited default (a pin bump could silently change it).
async fn auth_none(&mut self, _user: &str) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_none", "method-unimplemented");
Ok(Auth::reject())
}
@@ -341,6 +361,7 @@ impl russh::server::Handler for SessionHandler {
/// auth does not exist on this door; ADR-005's key custody is the
/// whole story.
async fn auth_password(&mut self, _user: &str, _password: &str) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_password", "method-unimplemented");
Ok(Auth::reject())
}
@@ -352,6 +373,7 @@ impl russh::server::Handler for SessionHandler {
_user: &str,
_certificate: &russh::keys::Certificate,
) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_openssh_certificate", "cert-unsupported");
Ok(Auth::reject())
}
@@ -362,6 +384,7 @@ impl russh::server::Handler for SessionHandler {
_submethods: &str,
_response: Option<russh::server::Response<'_>>,
) -> Result<Auth, Self::Error> {
observe::decline_auth("auth_keyboard_interactive", "method-unimplemented");
Ok(Auth::reject())
}
@@ -403,12 +426,14 @@ impl russh::server::Handler for SessionHandler {
_session: &mut Session,
) -> Result<(), Self::Error> {
if !self.serves_session_channels() {
observe::decline_open("session", "unfeatured", _channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
if self.admit_channel_open() {
reply.accept().await;
} else {
observe::decline_open("session", "cap-exceeded", _channel.id().number());
reply.reject(dispatch::cap_exceeded_open_decline()).await;
}
Ok(())
@@ -420,12 +445,13 @@ impl russh::server::Handler for SessionHandler {
/// path, and this hook never has one.
async fn channel_open_x11(
&mut self,
_channel: Channel<Msg>,
channel: Channel<Msg>,
_originator_address: &str,
_originator_port: u32,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_open("x11", "never-feature", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
Ok(())
}
@@ -441,7 +467,7 @@ impl russh::server::Handler for SessionHandler {
/// moves to wave 3's real accept point.
async fn channel_open_direct_tcpip(
&mut self,
_channel: Channel<Msg>,
channel: Channel<Msg>,
_host_to_connect: &str,
_port_to_connect: u32,
_originator_address: &str,
@@ -449,15 +475,18 @@ impl russh::server::Handler for SessionHandler {
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
const ROW: &str = "direct-tcpip";
if !self.serves_tunnels() {
observe::decline_open(ROW, "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
if self.channel_cap_exhausted() {
observe::decline_open(ROW, "cap-exceeded", channel.id().number());
reply.reject(dispatch::cap_exceeded_open_decline()).await;
return Ok(());
}
tunnel_row_stub(reply).await;
tunnel_row_stub(ROW, channel.id().number(), reply).await;
Ok(())
}
@@ -469,7 +498,7 @@ impl russh::server::Handler for SessionHandler {
/// identical to the other tunnel rows.
async fn channel_open_forwarded_tcpip(
&mut self,
_channel: Channel<Msg>,
channel: Channel<Msg>,
_host_to_connect: &str,
_port_to_connect: u32,
_originator_address: &str,
@@ -477,15 +506,18 @@ impl russh::server::Handler for SessionHandler {
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
const ROW: &str = "forwarded-tcpip";
if !self.serves_tunnels() {
observe::decline_open(ROW, "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
if self.channel_cap_exhausted() {
observe::decline_open(ROW, "cap-exceeded", channel.id().number());
reply.reject(dispatch::cap_exceeded_open_decline()).await;
return Ok(());
}
tunnel_row_stub(reply).await;
tunnel_row_stub(ROW, channel.id().number(), reply).await;
Ok(())
}
@@ -495,20 +527,23 @@ impl russh::server::Handler for SessionHandler {
/// everywhere, so the runtime gate has nothing to ride yet.
async fn channel_open_direct_streamlocal(
&mut self,
_channel: Channel<Msg>,
channel: Channel<Msg>,
_socket_path: &str,
reply: ChannelOpenHandle,
_session: &mut Session,
) -> Result<(), Self::Error> {
const ROW: &str = "direct-streamlocal";
if !self.serves_tunnels() {
observe::decline_open(ROW, "unfeatured", channel.id().number());
reply.reject(dispatch::unfeatured_open_decline()).await;
return Ok(());
}
if self.channel_cap_exhausted() {
observe::decline_open(ROW, "cap-exceeded", channel.id().number());
reply.reject(dispatch::cap_exceeded_open_decline()).await;
return Ok(());
}
tunnel_row_stub(reply).await;
tunnel_row_stub(ROW, channel.id().number(), reply).await;
Ok(())
}
@@ -531,6 +566,12 @@ impl russh::server::Handler for SessionHandler {
if self.serves_git_row() {
git_row_stub(channel, session);
} else {
observe::decline_request(
"exec_request",
"unfeatured",
channel.number(),
Some("git"),
);
decline_unfeatured_request(channel, session).await;
}
}
@@ -538,6 +579,12 @@ impl russh::server::Handler for SessionHandler {
if self.serves_session_rows() {
pipe_row_stub(channel, session);
} else {
observe::decline_request(
"exec_request",
"unfeatured",
channel.number(),
Some("pipe"),
);
decline_unfeatured_request(channel, session).await;
}
}
@@ -561,6 +608,7 @@ impl russh::server::Handler for SessionHandler {
if self.serves_session_rows() {
pty_row_stub(channel, session);
} else {
observe::decline_request("pty_request", "unfeatured", channel.number(), None);
decline_unfeatured_request(channel, session).await;
}
Ok(())
@@ -578,6 +626,7 @@ impl russh::server::Handler for SessionHandler {
if self.serves_session_rows() {
shell_row_stub(channel, session);
} else {
observe::decline_request("shell_request", "unfeatured", channel.number(), None);
decline_unfeatured_request(channel, session).await;
}
Ok(())
@@ -597,6 +646,7 @@ impl russh::server::Handler for SessionHandler {
_name: &str,
session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_request("subsystem_request", "unimplemented", channel.number(), None);
decline_unfeatured_request(channel, session).await;
Ok(())
}
@@ -611,6 +661,7 @@ impl russh::server::Handler for SessionHandler {
_variable_value: &str,
session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_request("env_request", "declined-by-design", channel.number(), None);
decline_unfeatured_request(channel, session).await;
Ok(())
}
@@ -626,6 +677,7 @@ impl russh::server::Handler for SessionHandler {
_x11_screen_number: u32,
session: &mut Session,
) -> Result<(), Self::Error> {
observe::decline_request("x11_request", "never-feature", channel.number(), None);
decline_unfeatured_request(channel, session).await;
Ok(())
}
@@ -636,9 +688,10 @@ impl russh::server::Handler for SessionHandler {
/// decline; russh replies the channel failure.
async fn agent_request(
&mut self,
_channel: ChannelId,
channel: ChannelId,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_request("agent_request", "never-feature", channel.number(), None);
Ok(false)
}
@@ -653,6 +706,7 @@ impl russh::server::Handler for SessionHandler {
_port: &mut u32,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("tcpip_forward", "unimplemented");
Ok(false)
}
@@ -664,6 +718,7 @@ impl russh::server::Handler for SessionHandler {
_port: u32,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("cancel_tcpip_forward", "unimplemented");
Ok(false)
}
@@ -675,6 +730,7 @@ impl russh::server::Handler for SessionHandler {
_socket_path: &str,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("streamlocal_forward", "unimplemented");
Ok(false)
}
@@ -685,13 +741,16 @@ impl russh::server::Handler for SessionHandler {
_socket_path: &str,
_session: &mut Session,
) -> Result<bool, Self::Error> {
observe::decline_global_request("cancel_streamlocal_forward", "unimplemented");
Ok(false)
}
}
/// The request-level decline shape for an unfeatured/unimplemented
/// capability: fail the request and leave the channel usable (POC-1
/// §5 — session survives declines, no state poisoning).
/// §5 — session survives declines, no state poisoning). The decline
/// event is emitted by the caller (the hook carries the row/reason
/// context); this is the wire shape only.
async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
let _ = session.channel_failure(channel);
}
@@ -705,17 +764,41 @@ async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
/// IS the row this wave). Decline-first-alloc-never holds structurally:
/// nothing is allocated before the failure lands — the spawn's handle
/// clone is the only resource ever taken.
async fn seam_stub_decline(handle: Handle, channel: ChannelId) {
let _ = handle.channel_failure(channel).await;
let _ = handle.close(channel).await;
///
/// The serving-path lifecycle events ride this body
/// ([`crate::observe`]): started (the task's first action), the
/// serving-path decline (class `serving`, reason `seam-stub`), and —
/// after the wire reporting has completed — ended, or failed when a
/// report send could not complete (the one warn in the vocabulary).
async fn seam_stub_decline(
handle: Handle,
channel: ChannelId,
row: &'static str,
exec_route: Option<&'static str>,
) {
observe::serving_path_started(row, channel.number(), exec_route);
observe::decline_serving(row, "seam-stub", channel.number(), exec_route);
let error = match handle.channel_failure(channel).await {
Ok(()) => handle.close(channel).await.err(),
Err(error) => Some(error),
};
match error {
Some(error) => observe::serving_path_failed(row, channel.number(), &error, exec_route),
None => observe::serving_path_ended(row, channel.number(), exec_route),
}
}
/// Spawns the seam-stub decline as the row's serving path (ADR-006
/// decision 4): the handle is cloned here, the decline runs on its own
/// task, and the hook returns — no serving path awaits inline.
fn spawn_seam_stub(channel: ChannelId, session: &Session) {
fn spawn_seam_stub(
channel: ChannelId,
session: &Session,
row: &'static str,
exec_route: Option<&'static str>,
) {
let handle = session.handle();
serve::spawn_serving_path(seam_stub_decline(handle, channel));
serve::spawn_serving_path(seam_stub_decline(handle, channel, row, exec_route));
}
/// The git row's seam stub — wave 3 lands the alkgit seam here
@@ -723,7 +806,7 @@ fn spawn_seam_stub(channel: ChannelId, session: &Session) {
/// per ADR-006 decision 4; the skeleton declines at the seam, on its
/// own task.
fn git_row_stub(channel: ChannelId, session: &Session) {
spawn_seam_stub(channel, session);
spawn_seam_stub(channel, session, "exec_request", Some("git"));
}
/// The session pipe row's seam stub — wave 3 lands the alktty pipe
@@ -731,7 +814,7 @@ fn git_row_stub(channel: ChannelId, session: &Session) {
/// ADR-006 decision 4; the skeleton declines at the seam, on its own
/// task.
fn pipe_row_stub(channel: ChannelId, session: &Session) {
spawn_seam_stub(channel, session);
spawn_seam_stub(channel, session, "exec_request", Some("pipe"));
}
/// The session pty row's seam stub — wave 3 lands the alktty
@@ -739,7 +822,7 @@ fn pipe_row_stub(channel: ChannelId, session: &Session) {
/// (`drive_session_pre_negotiated`), spawned per ADR-006 decision 4;
/// the skeleton declines at the seam, on its own task.
fn pty_row_stub(channel: ChannelId, session: &Session) {
spawn_seam_stub(channel, session);
spawn_seam_stub(channel, session, "pty_request", None);
}
/// The session shell row's seam stub — wave 3 lands the alktty
@@ -747,13 +830,16 @@ fn pty_row_stub(channel: ChannelId, session: &Session) {
/// crate default), spawned per ADR-006 decision 4; the skeleton
/// declines at the seam, on its own task.
fn shell_row_stub(channel: ChannelId, session: &Session) {
spawn_seam_stub(channel, session);
spawn_seam_stub(channel, session, "shell_request", None);
}
/// The tunnel row's seam stub: the explicit `ConnectFailed` reject
/// ([`crate::dispatch::tunnel_seam_stub_decline`]) — wave 3 lands the
/// route-1 bridge here (`TunnelSession::open_direct` → accept only
/// after the fabric open succeeds → `take_halves` pump).
async fn tunnel_row_stub(reply: ChannelOpenHandle) {
/// after the fabric open succeeds → `take_halves` pump). The
/// open-level decline event is emitted here: the stub's decline
/// reason is the `connect-failed` class ADR-006 decision 1 pins.
async fn tunnel_row_stub(row: &'static str, channel_id: u32, reply: ChannelOpenHandle) {
observe::decline_open(row, "connect-failed", channel_id);
reply.reject(dispatch::tunnel_seam_stub_decline()).await;
}
+1
View File
@@ -33,6 +33,7 @@ mod fingerprint;
mod handler;
mod host_key;
mod key_source;
mod observe;
mod serve;
pub use adapter::SshAdapter;
+458
View File
@@ -0,0 +1,458 @@
//! The decline-path + serving-path event vocabulary
//! (`w3-decline-tracing` — the wave-2 gate's booked finding): the
//! observability surface every decline site in `src/` emits through,
//! and the serving-path lifecycle skeleton ([`serving_path_started`] /
//! [`serving_path_ended`] / [`serving_path_failed`]) the wave-3
//! mappings inherit.
//!
//! ## The family posture read (2026-10-28)
//!
//! Read before choosing this shape — no alkssh-private dialect:
//!
//! - `alktty` (`adapter.rs`, `session.rs`, `local/pipe.rs`,
//! `local/pty.rs`): free `tracing::{debug, warn}` imports,
//! message-last lowercase sentences with a component prefix
//! (`"tty: backend stdin write failed: {e}"`), no spans, no macros.
//! - `alktunnels` (`producer.rs`, `consumer.rs`, `params.rs`): the
//! same free-fn posture, structured fields where identity exists
//! (`listen = …`, `error = %e`, `reason = …`), levels `debug` for
//! structural observations, `warn` for recoverable failures,
//! `error` for abnormal pump terminations.
//! - `alkcall` (`protocol/*`, `channels/*`): the widest use — the same
//! fields convention (`channel_id`, `request_id`, `error = %e`),
//! component-prefixed messages (`"channel client: …"`,
//! `"open wrapper: …"`), `debug`/`warn` only.
//! - `alkhttp` (the closest structural analog): `warn`/`error` with
//! `error = %e` (`"channels session ended"`).
//! - `alkgit`: no tracing at all at this writing.
//!
//! Never observed in the family: `info!` / `trace!` events, subscriber
//! setup in library code, macro or span wrappers — so none here
//! either. Adopted verbatim: free `tracing` fn calls, message-last
//! lowercase with the `"decline: …"` / `"serving path …"` phrasing,
//! flat per-event fields (`feature_build`, `row`, `channel_id`,
//! `decline_class`, `reason`, `exec_route`, `error`).
//!
//! ## The event vocabulary
//!
//! Every event carries exactly:
//!
//! - `feature_build = FEATURE_BUILD` — the crate's compile-time
//! feature set as a stable string (`"none"`, `"git"`,
//! `"session+git"`, …; features joined in the order `session, git,
//! tunnel, sftp`), so a captured event is self-describing per
//! build. The `sftp` feature gates no route, but ships in the
//! build string: it is part of what the assembly deployed.
//! - `row` — the greppable wire row the event is about: the channel
//! type for open-level (e.g. `"session"`, `"direct-tcpip"`, `"x11"`),
//! the request name for request-level (e.g. `"pty_request"`,
//! `"tcpip_forward"`), the handler hook name for auth (e.g.
//! `"auth_publickey"`), the session-row hook names for the
//! serving-path lifecycle.
//! - `channel_id` — the wire channel id (u32), on every event of a
//! channel; the auth class has none (the auth hooks carry no
//! channel at this surface).
//!
//! Decline events additionally carry exactly:
//!
//! - `decline_class` — `"open"` | `"request"` | `"serving"` | `"auth"`:
//! the two wire decline classes of `mapping.md` §Wire decline
//! reasons, plus the serving-path class (this wave the seam stub's
//! decline *is* the serving path; the wave-3 mappings emit the same
//! class from their real spawned paths) and the auth class (the
//! disposition declines of ADR-004).
//! - `reason` — the decline cause, mirroring the architecture's own
//! classifications: `"unfeatured"` (feature-gate absence),
//! `"never-feature"` (ADR-003 §5 rows), `"cap-exceeded"`,
//! `"connect-failed"` (the tunnel seam stub's `ConnectFailed`),
//! `"seam-stub"` (the session-row seam stubs), `"unimplemented"`
//! (the subsystem rows — OQ-13 — and the wave-3 listen forms),
//! `"declined-by-design"` (env, OQ-09), and on the auth class
//! `"method-unimplemented"` / `"cert-unsupported"` /
//! `"non-ed25519"` / `"fingerprint-unresolved"`.
//! - `exec_route` — only on `exec_request`-row events: the dispatch
//! route the parse routed to (`"git"` | `"pipe"`), so a git row's
//! decline is distinguishable from a pipe row's in combined builds.
//! Other rows carry none (a `None` field is not recorded by
//! `tracing`).
//!
//! Levels: declines and the lifecycle's started/ended emit at
//! `debug` — a decline is the door's normal, honest posture (vanilla
//! clients probe unfeatured rows routinely; nothing abnormal
//! happened), matching the family's structural-observation level.
//! Only [`serving_path_failed`] is `warn` — a serving path that could
//! not complete its wire reporting is a real failure (the family's
//! recoverable-failure level).
//!
//! ## Safety posture (conventions 6, OQ-03)
//!
//! **No secret material ever enters an event; never the username.**
//! Structurally enforced, not recalled: the helpers take only
//! `&'static str` vocabulary values and the wire channel id — there
//! is no parameter a username, command string, env name/value, repo
//! path, key, or fingerprint could ride. In particular *the
//! fingerprint stays out of the events* (`auth.md` §Dispositions: log
//! exposure is deployment policy — the crate's events are
//! safe-by-default at every level, so the fingerprint is a
//! deployer-side computation over its own logs, never crate-emitted).
//! The `error` field carries only russh protocol errors. The wire
//! rows in `tests/observe.rs` pin the posture: the username never
//! appears in any captured field. If a future decline site needs
//! request-argument detail (e.g. the git grammar's decline reasons in
//! `w3-git-mapping`), it must add an explicitly-vocabulary'd field and
//! record its safety reasoning — dynamic strings do not ride this
//! module by default.
/// The compile-time feature set, as a stable string (`"none"`,
/// `"session"`, `"session+git"`, …). Features join in the order
/// `session, git, tunnel, sftp`; the featureless build is `"none"`.
pub(crate) const FEATURE_BUILD: &str = feature_build();
const fn feature_build() -> &'static str {
match (
cfg!(feature = "session"),
cfg!(feature = "git"),
cfg!(feature = "tunnel"),
cfg!(feature = "sftp"),
) {
(false, false, false, false) => "none",
(true, false, false, false) => "session",
(false, true, false, false) => "git",
(false, false, true, false) => "tunnel",
(false, false, false, true) => "sftp",
(true, true, false, false) => "session+git",
(true, false, true, false) => "session+tunnel",
(true, false, false, true) => "session+sftp",
(false, true, true, false) => "git+tunnel",
(false, true, false, true) => "git+sftp",
(false, false, true, true) => "tunnel+sftp",
(true, true, true, false) => "session+git+tunnel",
(true, true, false, true) => "session+git+sftp",
(true, false, true, true) => "session+tunnel+sftp",
(false, true, true, true) => "git+tunnel+sftp",
(true, true, true, true) => "session+git+tunnel+sftp",
}
}
/// An open-level decline: the channel-open gate refused an open
/// (`unfeatured` / `never-feature` / `cap-exceeded` / the tunnel seam
/// stub's `connect-failed`). The wire answer is the open-failure code
/// ([`crate::dispatch`]); this is the event for the decision behind it.
pub(crate) fn decline_open(row: &'static str, reason: &'static str, channel_id: u32) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
channel_id,
decline_class = "open",
reason,
"decline: channel open rejected"
);
}
/// A request-level decline on a session channel: a capability the
/// build does not serve (`subsystem` / `env` / `x11` / agent, an
/// unfeatured parse route) or a not-yet-landed listen form. The wire
/// answer is `channel_failure` (the channel stays usable) or the
/// denied-request reply — the event is the deciding context.
pub(crate) fn decline_request(
row: &'static str,
reason: &'static str,
channel_id: u32,
exec_route: Option<&'static str>,
) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
channel_id,
decline_class = "request",
reason,
exec_route,
"decline: channel request rejected"
);
}
/// An auth-class decline: a disposition of the ADR-004 bridge turned
/// the method away. No channel id exists at this surface; the
/// *fingerprint never rides the event* (see §Safety posture) — the
/// `reason` codes distinguish the decline causes without exposing who.
pub(crate) fn decline_auth(row: &'static str, reason: &'static str) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
decline_class = "auth",
reason,
"decline: auth disposition rejected"
);
}
/// A request-level decline on a **global** request — the `-R` listen
/// forms (`mapping.md` §Global-request rows). No channel id exists at
/// this surface; the fields are otherwise the `request` vocabulary.
pub(crate) fn decline_global_request(row: &'static str, reason: &'static str) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
decline_class = "request",
reason,
"decline: global request rejected"
);
}
/// A serving-path decline: a routed row whose serving task answers
/// failure-then-close rather than serving. This wave that is the seam
/// stub (the wire shape pinned in `tests/spawn_return.rs`); the wave-3
/// mappings emit it from their real serving tasks wherever serving
/// itself cannot proceed — the class the spawned paths inherit.
pub(crate) fn decline_serving(
row: &'static str,
reason: &'static str,
channel_id: u32,
exec_route: Option<&'static str>,
) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
channel_id,
decline_class = "serving",
reason,
exec_route,
"decline: serving path rejected"
);
}
/// Spawned-path started — emitted as the spawned task's first action,
/// so "started" is the task's own honest start, not the hook's spawn
/// call. The mapping tasks' serving paths inherit this shape.
pub(crate) fn serving_path_started(
row: &'static str,
channel_id: u32,
exec_route: Option<&'static str>,
) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
channel_id,
exec_route,
"serving path started"
);
}
/// Spawned-path ended — the serving future finished (cleanly, or with
/// the decline it was wired to answer); emitted *after* the path's
/// wire reporting has completed.
pub(crate) fn serving_path_ended(
row: &'static str,
channel_id: u32,
exec_route: Option<&'static str>,
) {
tracing::debug!(
feature_build = FEATURE_BUILD,
row,
channel_id,
exec_route,
"serving path ended"
);
}
/// Spawned-path failure — the serving path could not complete its
/// wire reporting (the session handle's sends failed: the session is
/// gone, or the report never landed). The vocabulary's one `warn` —
/// the family's recoverable-failure level. Only russh protocol errors
/// ride the `error` field (§Safety posture); the mapping tasks emit
/// this from their real paths when serving fails before reporting.
pub(crate) fn serving_path_failed<E: std::fmt::Debug>(
row: &'static str,
channel_id: u32,
error: &E,
exec_route: Option<&'static str>,
) {
tracing::warn!(
feature_build = FEATURE_BUILD,
row,
channel_id,
exec_route,
error = ?error,
"serving path failed"
);
}
#[cfg(test)]
mod tests {
use tracing::subscriber::with_default;
/// A fake error shape — the failure row only pins that a Debug
/// error value rides the `error` field.
#[derive(Debug)]
struct Probe;
static PROBE: Probe = Probe;
use super::*;
/// A minimal field-capturing visitor shared by these rows and the
/// wire rows in `tests/observe.rs` (the test convention recorded
/// in the task notes — the family has no event-capture precedent,
/// only alktunnels' fmt-subscriber dev-dep for debug output, so
/// the convention is a hand-rolled minimal [`tracing::Subscriber`]
/// — no `tracing-subscriber` dependency).
struct Visitor(Vec<(String, String)>);
impl tracing::field::Visit for Visitor {
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.0.push((field.name().to_owned(), value.to_owned()));
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.push((field.name().to_owned(), format!("{value:?}")));
}
}
/// Captures the fields of the one event the emit fn fires, via
/// the thread-local default subscriber (`with_default` — these
/// are direct helper rows; the spawned-task surfaces are driven
/// over the wire in `tests/observe.rs` against the global
/// capture).
fn capture(emit: impl FnOnce()) -> Vec<(String, String)> {
struct Capture(std::sync::Mutex<Vec<(String, String)>>);
impl tracing::Subscriber for Capture {
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
true
}
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::Id {
tracing::Id::from_u64(1)
}
fn record(&self, _span: &tracing::Id, _values: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _span: &tracing::Id, _follows: &tracing::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
let mut v = Visitor(Vec::new());
event.record(&mut v);
self.0
.lock()
.unwrap_or_else(|e| e.into_inner())
.append(&mut v.0);
}
fn enter(&self, _span: &tracing::Id) {}
fn exit(&self, _span: &tracing::Id) {}
fn clone_span(&self, _id: &tracing::Id) -> tracing::Id {
tracing::Id::from_u64(1)
}
}
let holder = std::sync::Arc::new(Capture(Default::default()));
with_default(std::sync::Arc::clone(&holder), || {
let prior = holder.0.lock().unwrap().len();
emit();
assert!(holder.0.lock().unwrap().len() > prior);
});
holder.0.lock().unwrap().clone()
}
fn field<'a>(fields: &'a [(String, String)], name: &str) -> &'a str {
fields
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v.as_str())
.unwrap_or_else(|| panic!("no field {name} in {fields:?}"))
}
/// - *Contract stamp:* the event vocabulary (`w3-decline-tracing`)
/// — an open decline carries exactly the base shape, values
/// included.
#[test]
fn decline_open_carries_the_full_vocabulary() {
let fields = capture(|| decline_open("x11", "never-feature", 7));
assert_eq!(field(&fields, "feature_build"), FEATURE_BUILD);
assert_eq!(field(&fields, "row"), "x11");
assert_eq!(field(&fields, "channel_id"), "7");
assert_eq!(field(&fields, "decline_class"), "open");
assert_eq!(field(&fields, "reason"), "never-feature");
assert_eq!(field(&fields, "message"), "decline: channel open rejected");
let names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
assert_eq!(
names,
vec![
"message".to_owned(),
"feature_build".to_owned(),
"row".to_owned(),
"channel_id".to_owned(),
"decline_class".to_owned(),
"reason".to_owned(),
],
"an open decline carries exactly the vocabulary, nothing else \
(the message field first — the event! macro's shape)"
);
}
/// - *Contract stamp:* the `exec_route` field — present on the
/// exec row, absent elsewhere (a `None` field is not recorded
/// by `tracing`).
#[test]
fn decline_request_carries_the_route_when_the_parse_routed() {
let fields = capture(|| decline_request("exec_request", "unfeatured", 3, Some("git")));
assert_eq!(field(&fields, "decline_class"), "request");
assert_eq!(field(&fields, "exec_route"), "git");
let fields = capture(|| decline_request("subsystem_request", "unimplemented", 3, None));
assert_eq!(field(&fields, "decline_class"), "request");
assert!(
!fields.iter().any(|(n, _)| n == "exec_route"),
"a None route field is not recorded at all"
);
}
#[test]
fn decline_auth_carries_no_channel() {
let fields = capture(|| decline_auth("auth_password", "method-unimplemented"));
let names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
assert!(!names.iter().any(|n| n.starts_with("channel")));
assert_eq!(field(&fields, "decline_class"), "auth");
assert_eq!(field(&fields, "row"), "auth_password");
assert_eq!(field(&fields, "reason"), "method-unimplemented");
}
/// - *Contract stamp:* the `exec_route` field — present on the
/// exec row, absent elsewhere (a `None` field is not recorded
/// by `tracing`). The pty/shell lifecycle rows carry no route.
#[test]
fn serving_path_lifecycle_carries_started_ended_and_the_warn_failure() {
let fields = capture(|| serving_path_started("pty_request", 1, None));
assert_eq!(field(&fields, "message"), "serving path started");
let names: Vec<String> = fields.iter().map(|(n, _)| n.clone()).collect();
assert!(
!names.iter().any(|n| n == "decline_class"),
"lifecycle events carry no decline class"
);
let fields = capture(|| serving_path_ended("exec_request", 1, Some("pipe")));
assert_eq!(field(&fields, "exec_route"), "pipe");
assert_eq!(field(&fields, "message"), "serving path ended");
let fields = capture(|| serving_path_failed("shell_request", 1, &PROBE, None));
assert_eq!(field(&fields, "message"), "serving path failed");
assert_eq!(field(&fields, "error"), "Probe");
}
/// The feature-build string: every enabled feature appears; a
/// featureless build reads `none`. The expected half is derived
/// from `cfg!` independently of the crate's match — divergence
/// breaks this row loudly.
#[test]
fn feature_build_string_reflects_the_cfgs() {
let mut expected = String::new();
for (on, name) in [
(cfg!(feature = "session"), "session"),
(cfg!(feature = "git"), "git"),
(cfg!(feature = "tunnel"), "tunnel"),
(cfg!(feature = "sftp"), "sftp"),
] {
if on {
if !expected.is_empty() {
expected.push('+');
}
expected.push_str(name);
}
}
if expected.is_empty() {
expected.push_str("none");
}
assert_eq!(FEATURE_BUILD, expected);
}
}
+3
View File
@@ -13,6 +13,9 @@
//! and returns. No serving path is ever awaited inline; the dispatch
//! seam stubs ([`crate::handler`]) hang off this helper, so the
//! wave-3 mappings inherit the pattern mechanically from the stubs.
//! The serving-path *events* (started / ended / failure —
//! [`crate::observe`]) are the spawned task's own actions: the stub
//! body emits them, the mappings' real paths will too.
//! - **Exit/status reporting rides the cloned handle** — the spawned
//! task's only report surface is the handle clone it was handed;
//! [`report_exit_status_then_close`] preserves the wire ordering the