feat: channels path carries the negotiation in the open op (L1, L3)

The channels path no longer carries a second negotiation frame on the
channel's data stream (ADR-009). The open op's registry-validated
input IS the negotiation:

- producer: make_tty_open_handler parses the open op's input into a
  NegotiateRequest and drives the new drive_session_pre_negotiated
  (same three-pump driver as drive_session, minus the wire-frame
  negotiation phase; validate/allocate factored into
  validate_and_allocate, shared by both paths). Post-open failures
  (unknown backend, allocate_failed, ownership denial) still go to the
  client as a 0x00-prefixed negotiation error frame, so the consumer's
  M1 disambiguation read applies unchanged. The tty:open scope gate is
  enforced by the registry's AccessControl (not re-checked in the
  handler).
- consumer: open_via_channels parses params locally (fail-fast before
  a channel is allocated), opens the channel, and starts raw-chunk
  mode directly (from_halves_raw — no negotiation write; the
  0x00-error-frame peek retained).
- tty_open_spec's input schema is now the partial NegotiateRequest
  shape (carriage/backend/cmd required; backend params stay free-form
  — raw JSON Schema is permissive on unknown keys).

Prerequisites landed upstream: alkcall 0.4.0 enforces
OperationSpec.input_schema at dispatch (the registry check this design
leans on never existed before); alkcall 0.4.1 parks early-arrival
chunks for un-adopted channels instead of dropping them — the open
response / producer's-first-write race was silently losing the first
chunks (found by L3's test; the session never resolved).

L3: open_via_channels + from_bidi_stream_via now covered end-to-end
(5 session tests + pre-negotiated adapter test + shared-harness tests
in the new crate::testing module; the channels harness moved there so
session tests share it).

Verification: cargo test 93 lib (default), 116 lib + 19 integration
(--all-features); clippy -D warnings native + wasm clean; fmt clean;
doc 0 warnings; wasm check clean.
This commit is contained in:
2026-09-05 07:08:17 +00:00
parent 9327a73496
commit 96692d3b6a
8 changed files with 781 additions and 243 deletions
+66 -14
View File
@@ -1,6 +1,6 @@
---
status: partially-resolved (M1, M2, L2, L4, L5, N1, N2, N3, N5)
last_updated: 2026-08-17
status: partially-resolved (L6, N4, N6)
last_updated: 2026-09-05
reviewed_artifacts:
- src/lib.rs
- src/wire.rs
@@ -532,8 +532,8 @@ Highlights:
| M2 | `wait()` swallows `MalformedExitChunk` | propagate the variant | small | low | ✅ resolved |
| L2 | consumer stdout/stderr routing untested | add emitting test backend | small | low | ✅ resolved |
| M1 | negotiation-rejection frame unhandled | implement disambiguation read | medium | medium (wire-facing) | ✅ resolved |
| L1 | channels `input` ignored | decide drop-vs-pass-through | small | low | open |
| L3 | `open_via_channels` 0% covered | end-to-end channels consumer test | medium | low | open |
| L1 | channels `input` ignored | decide drop-vs-pass-through | small | low | ✅ resolved (2026-09-05) |
| L3 | `open_via_channels` 0% covered | end-to-end channels consumer test | medium | low | ✅ resolved (2026-09-05) |
| L6 | pty bridge error paths untested | targeted error-path tests | medium | low | open |
| N4 | sleep-based timing | readiness signals | small | low | open |
| N6 | MSRV unverified | CI MSRV job or bump | small | none | open |
@@ -569,23 +569,74 @@ Post-fix coverage: `session.rs` 79.71% → 87.43% lines, total 90.74% →
`cargo test --all-features`, clippy native + wasm, fmt, doc, wasm
check).
### Resolution (2026-09-05, L1 + L3 — the channels consumer path)
**L1 — resolved via the publisher decision: the channels path carries no
second negotiation frame (ADR-009 in `docs/architecture/decisions/`).**
Two upstream alkcall changes were prerequisites (the review's premise
that the registry "validates `input`" was wrong — alkcall never
enforced `input_schema` on any dispatch path):
- alkcall 0.4.0 — `OperationSpec.input_schema` is now enforced at call
time by all three registry dispatch entry points (`invoke`,
`invoke_streaming`, `invoke_sink`), compiled once at registration
(fail-closed, same rule as `publish_schema`/CF-003). Violations
return `INVALID_INPUT`. The `channels/tty/sub` spec's input schema
now declares the shared `NegotiateRequest` fields (`carriage`,
`backend`, `cmd` required); `tty`/`cwd`/`env`/backend-params stay
free-form (raw JSON Schema is permissive on unknown keys, so the
opaque ADR-053 params pass through).
- alkcall 0.4.1 — early-arrival chunks for a not-yet-adopted channel
are parked (bounded per-channel buffer) and drained on
`adopt_channel`, instead of dropped. The open-op-response /
producer's-first-write race silently lost the first chunks of any
push-first producer — found by L3's test (the session never resolved
because the stdout sentinel + exit chunk of an
immediately-resolving backend arrived before the adopt).
Design: the open op's `input` IS the negotiation (design 2 of the
publisher decision). `make_tty_open_handler` parses the
registry-validated `input` into a `NegotiateRequest` and drives
`drive_session_pre_negotiated` (new public API in `adapter.rs`) — the
same three-pump session driver as the direct path, minus the
wire-frame negotiation phase. Validation still runs
(`carriage`/`cmd`/backend lookup + ADR-050 ownership); failures go to
the client as a `0x00`-prefixed negotiation error frame on the channel
stream, so the M1 disambiguation read applies unchanged. The
`tty:open` scope gate is enforced by the registry's `AccessControl`
(not re-checked in the handler). `tty_open_spec()`'s schema is now the
partial `NegotiateRequest` shape. `TtySession::open_via_channels`
parses `params` locally (fail-fast before a channel is allocated),
opens the channel, and starts raw-chunk mode directly
(`from_halves_raw` — no negotiation write, peek retained for the
error-frame path).
Tests: `open_via_channels_end_to_end_negotiates_and_waits`,
`open_via_channels_surfaces_negotiation_rejected`,
`open_via_channels_fails_fast_on_schema_invalid_params`,
`open_via_channels_fails_fast_on_unparseable_params`,
`open_via_channels_routes_backend_stdout_and_stderr`,
`pre_negotiated_happy_path_over_plain_duplex` (adapter), plus the
shared harness tests in `src/testing.rs` (harness-level handler→client
data flow). The channels harness (`wire_client_and_server`) moved to
`crate::testing` so session tests share it with channels tests.
**L3 — resolved** by the same work: `open_via_channels` (and
`from_bidi_stream_via`) are covered end-to-end against the real
producer path (`register_openable` + `drive_session_pre_negotiated`
through alkcall's channels stack).
### Remaining (open)
- **L1** — channels `input` ignored; needs a publisher decision
(drop the parameter vs pass-through to `drive_session`).
- **L3** — `open_via_channels` still 0% covered; needs the channels
harness shared across modules.
- **L6** — pty bridge error paths untested.
- **N4** — sleep-based timing in signal/cancel tests.
- **N6** — MSRV unverified.
### Recommended Order (remaining)
1. **L1 + L3** — the channels consumer path; do together since L3's
test will exercise L1's code.
2. **L6** — pty bridge error paths; medium effort, lower priority than
the consumer-half work.
3. **N4 + N6** — test hardening and MSRV; defer until CI exists.
1. **L6** — pty bridge error paths; medium effort.
2. **N4 + N6** — test hardening and MSRV; defer until CI exists.
---
@@ -593,7 +644,8 @@ check).
- All line numbers refer to the tree at commit `18c4924` (the last
commit on `main` at review time). The resolution section above
reflects the tree at commit `9944153`.
reflects the tree at commit `9944153` and the 2026-09-05 L1+L3
resolution.
- The coverage numbers are from `cargo llvm-cov --all-features` on the
same tree. The `--show-missing-lines` output was used to attribute
gaps; the full report is at `target/llvm-cov/html`.