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:
1 parent
270365ea4c
commit
58d0b86ec5
6 files changed
+1490
-29
No files matched your search
+108
-22
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user