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

+101 -8
View File
@@ -1,7 +1,7 @@
---
id: w2-spawn-return
name: Wave 2 — spawn-and-return plumbing + exit/status reporting mechanics
status: pending
status: completed
depends_on: [w2-dispatch-decline-core]
scope: narrow
risk: medium
@@ -42,16 +42,16 @@ mechanically from the stubs this task leaves behind.
## Acceptance Criteria
- [ ] Every serving path (the dispatch stubs) runs in a
- [x] Every serving path (the dispatch stubs) runs in a
`tokio::spawn`; no handler hook awaits a serving path inline
- [ ] Exit/status reporting from spawned tasks rides the cloned
- [x] Exit/status reporting from spawned tasks rides the cloned
`Session::handle()`; `exit-status` is sent before channel close
- [ ] A harness test proves the mechanics end to end: the client
- [x] A harness test proves the mechanics end to end: the client
observes `exit-status(N)` from a spawned stub that completed
after the handler returned
- [ ] A handler-returns-fast assertion pins the structural constraint
- [x] A handler-returns-fast assertion pins the structural constraint
as a test
- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --check` pass
## References
@@ -67,8 +67,101 @@ mechanically from the stubs this task leaves behind.
## Notes
> To be filled by implementation agent
- `src/serve.rs` (new): the spawn-and-return plumbing as *mechanics*,
not convention — `spawn_serving_path(serving future)` (the one
choke point for the ADR-006 decision-4 spawn; nothing it takes is
ever awaited inline) and `report_exit_status_then_close(&Handle,
channel, N)` (the cloned-handle report with the `mapping.md`
`exit_status` row's ordering baked in: exit-status **before**
close). Both re-exported `#[doc(hidden)]` from the crate root — the
`SessionHandler` §Test-surface pattern — as the contract-test
surface the mechanics rows compose over the real wire. A unit row
(`spawn_returns_while_the_serving_path_is_still_pending` —
latch-gated future, spawn call returns first) pins spawn-then-return
directly. The derivation contract's *rules* (which exit code → which
SSH exit shape) stay wave-3 `mapping.md` material; no rule logic
landed here.
- `src/handler.rs`: the four session-row seam stubs (`git_row_stub`,
`pipe_row_stub`, `pty_row_stub`, `shell_row_stub`) are now spawned
serving paths — `spawn_seam_stub` clones `session.handle()`,
`seam_stub_decline(handle, channel)` runs the failure-then-close
decline on the spawned task, and the hooks return without awaiting
anything. A compile-level fact coded around: the row stubs are
plain `fn`s (not `async`) — an `async fn` holding `&Session` across
the spawn makes the handler future non-`Send` (russh requires `+
Send` on handler futures; `Session` is not `Sync`), so the clone is
taken synchronously and only the handle moves into the spawned
future. Wire shapes are unchanged (all `w2-dispatch-decline-core`
rows pass unmodified with the spawned decline); the tunnel seam stub
and every open-level/capability decline stay inline by class — they
answer the open request *before* any channel is owned, so nothing
spawns yet (documented in the new handler module-doc section §The
spawn-and-return structural constraint; wave 3's
accept-only-after-fabric-open flow is where the tunnel row gains
its spawn).
- `tests/spawn_return.rs` (new): the end-to-end mechanics proof over
the real wire — the russh server stack (duplex transport, harness
client/harness transport posture) mounted over a minimal proof
handler that is *not* the crate's `SessionHandler` (the auth and
dispatch hooks have their own suites; the proof subject is the
mechanics) and that serves execs by composing the exported
plumbing: `spawn_serving_path` of a release-gated serving future
reporting `exit-status(7)` + close through the cloned handle. Two
rows, each with a `Contract stamp:`:
- `handler_returns_fast_while_the_spawned_serving_path_is_pending` —
the structural constraint as a wire test: after the exec hook has
provably returned (a bounded wait on the handler-side timestamp;
the client's exec returns when the request is on the wire, not
when the hook ran) and with the serving path still gated, the
client's *follow-up traffic* (a second session channel + second
exec) is routed and dispatched normally — POC-3's follow-up-bytes
deadlock shape inverted — and the still-gated no-report state is
asserted before the release.
- `spawned_stub_reports_exit_status_before_close_after_the_handler_returned`
— the client observes `ExitStatus{7}` strictly before `Close`
(message-by-message, no draining that could hide ordering), and
the recorded timestamps prove the exit-status left *after* the
handler had already returned (the literal criterion).
- **The proof-handler design note:** the gate is `tokio::sync::watch`-
based (no lost-wakeup race — a late subscriber sees the release,
which a `Notify`-with-`notify_waiters` shape could miss against
multiple spawned paths); the handler-side timestamps ride
`parking_lot::Mutex`.
- Feature independence: the mechanics are feature-independent (the
helpers exist in every build and the proof rows run in every build
they were verified under — default, `git`, `session`, `tunnel`,
`sftp`, `--all-features`); no feature-gated code was added, so no
new decline-path coverage obligations arise.
## Summary
> To be filled on completion
Landed the spawn-and-return plumbing + exit/status reporting
mechanics (`w2-spawn-return`) in three files:
- **`src/serve.rs`** — `cargo test`-pinned mechanics: the
`spawn_serving_path` choke point (the ADR-006 decision-4 rule, in
code, never awaited inline) and `report_exit_status_then_close`
(exit-status before close through the cloned `Session::handle()`),
both `#[doc(hidden)]` contract-test re-exports; a unit row pins
spawn-returns-while-pending directly.
- **`src/handler.rs`** — the four session-row seam stubs became
spawned serving paths (handle clone taken synchronously — the
non-`Send` shape that forcing `async fn` stubs would produce is
coded around and recorded); failure-then-close wire shapes unchanged
(all dispatch-core rows pass unmodified); open-level declines and
the tunnel seam stub stay inline by class, documented; a new
module-doc section carries the constraint and the wave-3
inherit-the-pattern note.
- **`tests/spawn_return.rs`** — the end-to-end mechanics proof: two
`Contract stamp:` rows over the real wire (handler-returns-fast
under follow-up traffic with the spawn still pending; exit-status
before close, timestamps proving the report outlived the handler's
return), driven by a proof handler composing the exported plumbing.
Verification (all green): `cargo test` · `cargo test
--features {git,session,tunnel,sftp} --test dispatch_core` ·
`cargo test --all-features` · `cargo clippy --all-targets -- -D
warnings` · `cargo clippy --all-features --all-targets -- -D
warnings` · `cargo fmt --check` · `cargo doc --no-deps` (no
warnings). 93 lib/unit rows + the per-build dispatch matrices + the
two mechanics rows across 7 test binaries.