fix: channels parse-failure path writes the negotiation error frame (R4)

Review #002 R4 — a NegotiateRequest parse failure of the open op's
schema-validated input died silently (log + return, channel teardown,
consumer observed NoExitChunk — indistinguishable from a crashed
producer), while the other post-open failure classes (unknown backend,
allocate_failed, ownership denial) wrote the 0x00-prefixed error frame.

- make_tty_open_handler now accepts the channel's BiStream and writes
  a malformed_negotiation frame via the shared
  crate::adapter::send_negotiation_error (now pub(crate)) before
  returning; the consumer's M1 peek surfaces NegotiationRejected
  unchanged
- the frame type and layout are unchanged (ADR-001 wire-stable
  contract); no new frame type, no wire change
- tests: make_tty_open_handler seam test with a hand-built
  schema-bypassing input (cwd: 42) + a real-registry end-to-end test
  via ChannelClient::open_channel (bypasses open_via_channels's local
  fail-fast parse — R5's path — so it exercises the producer handler)
- docs: ADR-009 amended (Parse-failure error frame section);
  tty-adapter.md malformed_negotiation row covers both paths;
  session.rs post-open failure lists updated; review #002 R4 resolved

Note: the review's "unreachable end-to-end" premise was refined —
open_via_channels parses params locally (fail-fast) so a TtySession
consumer never hits the producer-side parse failure, but direct
ChannelClient callers do; the schema is deliberately partial so a
schema-valid value (cwd typed as a number) reaches the handler.

Verification: cargo test 95 lib (default) / 138 (--all-features);
clippy -D warnings native + wasm clean; fmt clean; doc 0 warnings.
This commit is contained in:
glm-5.3-flash committed 2026-09-05 08:37:02 +00:00
1 parent 5b608117ff
commit 8ee9216a07
7 files changed
+263 -25

No files matched your search

+8 -1
View File
@@ -168,7 +168,14 @@ fn has_scope(identity: &Option<Identity>, scope: &str) -> bool {
/// Send a negotiation error frame and close the write half. Consumes the
/// writer so the underlying transport's shutdown runs after the frame is
/// flushed.
async fn send_negotiation_error<W: AsyncWrite + Unpin>(
///
/// Shared by the direct path's failure arms and the channels open
/// handler (`make_tty_open_handler` writes the same `malformed_negotiation`
/// frame on a `NegotiateRequest` parse failure of the open op's `input` —
/// the pre-negotiated driver has no writer to consume, so the handler
/// builds its own over the channel's write half). `pub(crate)` so the
/// channels handler reuses it; not public API.
pub(crate) async fn send_negotiation_error<W: AsyncWrite + Unpin>(
mut writer: NegotiationWriter<W>,
error: &str,
fields: &[(&str, &str)],