feat(serve): land the spawn-and-return plumbing + exit/status mechanics (w2-spawn-return)

- src/serve.rs (new): the ADR-006 decision-4 mechanics in code —
  spawn_serving_path (the one choke point; nothing awaited inline)
  and report_exit_status_then_close (exit-status before close via
  the cloned Session::handle()), #[doc(hidden)] contract-test
  re-exports; a unit row pins spawn-returns-while-pending.
- src/handler.rs: the four session-row seam stubs became spawned
  serving paths (failure-then-close unchanged; stubs are plain fns —
  an async stub holding &Session across the spawn is non-Send); open-
  level declines and the tunnel seam stub stay inline by class, per
  the new module-doc section.
- tests/spawn_return.rs (new): two end-to-end wire rows — the client
  observes exit-status(7) strictly before close and after the handler
  returned; the handler-returns-fast row routes follow-up traffic
  while the serving path is still gated (POC-3 deadlock inversion).
- tasks/implementation/w2-spawn-return.md — completed with notes.

Verification (all green): cargo test; per-feature dispatch matrices
(--features git/session/tunnel/sftp, --all-features); cargo clippy
--all-targets/-all-features with -D warnings; cargo fmt --check;
cargo doc --no-deps (no warnings); taskgraph validate.
This commit is contained in:
glm-5.3-flash committed 2026-10-28 11:04:17 +00:00
1 parent 68d3723f6a
commit 2e5a7c9cee
5 files changed
+640 -36

No files matched your search

+68 -28
View File
@@ -69,7 +69,30 @@
//! the store-and-take handoff (`channels.remove(&id)` → `into_stream`
//! in the spawned serving task) is wave-3 mapping material; the
//! routing decisions around it are already this wave's, because the
//! routing stubs decline at the seam.
//! routing stubs spawn their decline at the seam.
//!
//! ## The spawn-and-return structural constraint (ADR-006 decision 4)
//!
//! russh at this pin runs handler callbacks **inline inside the
//! session's packet loop** — a blocking handler suspends all packet
//! routing and deadlocks against the peer's follow-up bytes (POC-3
//! proved by construction with receive-pack). Every serving path is
//! therefore spawned through [`crate::serve::spawn_serving_path`] and
//! the handler returns fast: the session-row seam stubs clone
//! `session.handle()`, spawn their decline through the clone
//! ([`crate::serve`]), and are gone from the hook's stack — no hook
//! awaits a serving path inline. The decline keeps its wire shape
//! (failure then close), riding the spawn. Exit/status reporting from
//! spawned tasks rides the same cloned handle
//! ([`crate::serve::report_exit_status_then_close`] — the mechanics;
//! the derivation rules are wave-3 `mapping.md` material), pinned
//! end-to-end by the mechanics rows in `tests/spawn_return.rs`.
//!
//! The open-level declines (unfeatured/never-feature rows, the tunnel
//! seam stub) stay inline by class: they answer the open *before* any
//! channel is owned — nothing is served yet, so nothing spawns. Wave
//! 3's accept-only-after-fabric-open flow is where the tunnel row
//! gains its spawned serving path, per the same constraint.
//!
//! Request-level declines: `subsystem_request` declines every name —
//! `sftp` is the OQ-13 placeholder (the feature exists, the route
@@ -101,7 +124,9 @@
//! usable (the POC-1 §5 session-survives-declines posture); a
//! *serving-path* decline (the wave-3 seam stubs) answers failure
//! and closes the channel — the vanilla-clean shape for a row that
//! will spawn its serving path next wave.
//! owns the channel. The serving-path decline now rides the spawned
//! serving task (ADR-006 decision 4, §The spawn-and-return
//! structural constraint above); the wire shape is unchanged.
//!
//! ## Test surface
//!
@@ -118,13 +143,16 @@ use std::sync::Arc;
use alkcall::core::auth::Identity;
use russh::keys::Algorithm;
use russh::server::{Auth, ChannelOpenHandle, Msg, PublicKeyOffer, Session, VerifiedPublicKey};
use russh::server::{
Auth, ChannelOpenHandle, Handle, Msg, PublicKeyOffer, Session, VerifiedPublicKey,
};
use russh::{Channel, ChannelId, Pty};
use crate::config::ServeConfig;
use crate::dispatch;
use crate::error::AdapterError;
use crate::fingerprint::fingerprint;
use crate::serve;
/// The per-connection handler (one per mounted stream): russh constructs
/// nothing here — [`crate::SshAdapter::handle`] builds one per accepted
@@ -403,14 +431,14 @@ impl russh::server::Handler for SessionHandler {
match dispatch::classify_exec(command) {
dispatch::ExecRoute::Git => {
if self.serves_git_row() {
git_row_stub(channel, session).await;
git_row_stub(channel, session);
} else {
decline_unfeatured_request(channel, session).await;
}
}
dispatch::ExecRoute::Pipe => {
if self.serves_session_rows() {
pipe_row_stub(channel, session).await;
pipe_row_stub(channel, session);
} else {
decline_unfeatured_request(channel, session).await;
}
@@ -433,7 +461,7 @@ impl russh::server::Handler for SessionHandler {
session: &mut Session,
) -> Result<(), Self::Error> {
if self.serves_session_rows() {
pty_row_stub(channel, session).await;
pty_row_stub(channel, session);
} else {
decline_unfeatured_request(channel, session).await;
}
@@ -450,7 +478,7 @@ impl russh::server::Handler for SessionHandler {
session: &mut Session,
) -> Result<(), Self::Error> {
if self.serves_session_rows() {
shell_row_stub(channel, session).await;
shell_row_stub(channel, session);
} else {
decline_unfeatured_request(channel, session).await;
}
@@ -571,45 +599,57 @@ async fn decline_unfeatured_request(channel: ChannelId, session: &mut Session) {
}
/// The family-seam decline shape for a **routed-but-not-yet-served**
/// row: fail the request and close the channel — the vanilla-clean
/// shape for a serving path that would have owned the channel (wave 3
/// replaces these stubs with the spawned serving paths; a declined
/// seam never leaks a half-served channel, since the decline IS the
/// row this wave). Decline-first-alloc-never holds structurally:
/// nothing is allocated before the failure lands.
async fn seam_stub_decline(channel: ChannelId, session: &mut Session) {
let _ = session.channel_failure(channel);
let _ = session.close(channel);
/// row — now the spawned serving task's body: fail the request and
/// close the channel, through the cloned handle the task was handed.
/// The vanilla-clean shape for a serving path that owns the channel
/// (wave 3 replaces these stubs with the real serving paths; a
/// declined seam never leaks a half-served channel, since the decline
/// 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;
}
/// 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) {
let handle = session.handle();
serve::spawn_serving_path(seam_stub_decline(handle, channel));
}
/// The git row's seam stub — wave 3 lands the alkgit seam here
/// (`parse command → Service + repo → serve_duplex_resolved`), spawned
/// per ADR-006 decision 4; the skeleton declines at the seam.
async fn git_row_stub(channel: ChannelId, session: &mut Session) {
seam_stub_decline(channel, session).await;
/// 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);
}
/// The session pipe row's seam stub — wave 3 lands the alktty pipe
/// seam here (`wrapper ++ [cmd]`, the OQ-08 argv append), spawned per
/// ADR-006 decision 4; the skeleton declines at the seam.
async fn pipe_row_stub(channel: ChannelId, session: &mut Session) {
seam_stub_decline(channel, session).await;
/// 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);
}
/// The session pty row's seam stub — wave 3 lands the alktty
/// `NegotiateRequest.tty` construction here
/// (`drive_session_pre_negotiated`), spawned per ADR-006 decision 4;
/// the skeleton declines at the seam.
async fn pty_row_stub(channel: ChannelId, session: &mut Session) {
seam_stub_decline(channel, session).await;
/// the skeleton declines at the seam, on its own task.
fn pty_row_stub(channel: ChannelId, session: &Session) {
spawn_seam_stub(channel, session);
}
/// The session shell row's seam stub — wave 3 lands the alktty
/// `NegotiateRequest.cmd` = serve-config shell argv here (OQ-08, no
/// crate default), spawned per ADR-006 decision 4; the skeleton
/// declines at the seam.
async fn shell_row_stub(channel: ChannelId, session: &mut Session) {
seam_stub_decline(channel, session).await;
/// declines at the seam, on its own task.
fn shell_row_stub(channel: ChannelId, session: &Session) {
spawn_seam_stub(channel, session);
}
/// The tunnel row's seam stub: the explicit `ConnectFailed` reject