feat(review 006 Unit 1): channel-open establishment phase + typed client error (E-01, N-1)

Implements ADR-049 Unit 1 — the open-op wrapper gains an awaited,
bounded establishment phase, and the client stops erasing the error.

- OpenEstablisher hook + Establishment/EstablishmentError types:
  register_openable_with_establisher awaits the establisher bounded
  (earlier of dispatch deadline and per-registration timeout, else
  ESTABLISHMENT_TIMEOUT = 10s) after allocation, before the reply and
  before the pump handler is spawned (ADR-049 §1/§2). Implementation
  note: the establisher takes (input, auth) only — the channel's
  yield-once BiStream belongs exclusively to the pump handler
  (amendment recorded in ADR-049).
- Establishment failure: teardown_channel + opener-ledger take +
  policy.on_close un-increment (allocation and teardown balance;
  the ledger take is the atomic gate, ADR-047 §7), reply
  channel:open_failed with details {reason, message} — reason ∈
  dial_failed / unknown_resource / resource_shortage / handler_error
  / timeout (ADR-049 §3). SSH contract consumer-visible: a failed
  open never returns a channel_id.
- register_openable unchanged (no establisher = always-OK; existing
  registrations compile and behave identically — compat gate test).
- ChannelClient::open_channel returns ChannelOpenError (breaking at
  0.5.0): CallFailed { error: CallError } carries the wire error
  verbatim (establishment_reason() branches on details.reason);
  MissingChannelId / AdoptFailed cover the local-only shapes
  (ADR-049 §4, review 006 N-1).
- Tests cover all four verification gates from the review: e2e
  establisher failure through a real channels connection (typed
  reason + no-channel + ledger un-increment), bounded timeout,
  no-establisher compat, establisher-success pump round-trip; plus
  reason-vocabulary mapping and bound arithmetic.
- Bump to 0.5.0 (open_channel error-type change is semver-relevant).

Verification: cargo test (608 passed), clippy --all-targets -D
warnings, fmt --check, doc --no-deps, wasm32 check — all clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-06 19:00:04 +00:00
1 parent 48ceeba55c
commit 2586c3b217
9 files changed
+1106 -31

No files matched your search

@@ -2,9 +2,11 @@
## Status
Accepted (amends ADR-047 §3 — the open-op wrapper gains an awaited
Accepted — implemented in alkcall 0.5.0 (Unit 1: E-01 + N-1; see the
"Amendment (Unit 1 implementation, 2026-09-06)" at the bottom).
Amends ADR-047 §3 — the open-op wrapper gains an awaited
establishment phase ahead of the spawned pump handler; resolves review
006 E-01 and N-1)
006 E-01 and N-1
## Context
@@ -313,4 +315,42 @@ untouched, which is what keeps the split cheap to revise.
- alktunnels OQ-TN-09 (dial-failure reporting — the first consumer of
the new error) and `docs/research/ssh-socks5-survey.md`
§"Open-failure path" (the reason-code prior art)
- RFC 4254 §5.1, RFC 1928 §6 — SSH/SOCKS5 open-failure semantics
- RFC 4254 §5.1, RFC 1928 §6 — SSH/SOCKS5 open-failure semantics
## Amendment (Unit 1 implementation, 2026-09-06)
Unit 1 landed in alkcall 0.5.0. Two implementation-shape notes, both
within this ADR's two-way door (the hook shape is the revisable
implementation detail; the wire surface is unchanged from §1/§3):
1. **The establisher does not receive the channel `Connection`.** §1's
signature sketch passed `Connection` to the establisher, but the
channel's `BiStream` is yield-once (`ChannelBidiStreamSource`) — it
cannot be handed to both the establisher and the pump handler, and
the establisher is pre-data-plane by design (its dial targets the
backend, not the channel). The implemented signature is
`Fn(Value, AuthContext) -> BoxFuture<'static,
Result<Establishment, EstablishmentError>>`; the `Connection`
belongs exclusively to the `OpenHandler` (unchanged).
2. **The bound is the earlier of the dispatch deadline and the
per-registration timeout.** §2 names the dispatch deadline "when
the `OperationContext` carries one, else the crate constant";
implemented as `min(deadline_remaining, timeout_override |
ESTABLISHMENT_TIMEOUT)` — the registration override stays
meaningful for `Query`/`Mutation`-typed open ops (whose dispatch
carries a 30s deadline; `Sub` clears it), and a deadline already in
the past yields a zero bound (immediate `timeout` reason).
Implemented surface: `OpenEstablisher`, `Establishment`,
`EstablishmentError` (reasons `dial_failed`/`unknown_resource`/
`resource_shortage`/`handler_error` + the wrapper's `timeout`),
`ESTABLISHMENT_TIMEOUT` (10s), `CHANNEL_OPEN_FAILED`
(`channel:open_failed`), `ChannelCore::register_openable_with_establisher`
(`register_openable` delegates with `establisher: None`),
`ChannelOpenError` (client-side typed error: `CallFailed { error:
CallError }` / `MissingChannelId` / `AdoptFailed`, with
`call_error()` + `establishment_reason()` accessors). All four
verification gates from the review landed as tests (establisher
failure e2e through a real channels connection with ledger
un-increment + no-channel assertions, bounded timeout, no-establisher
compat, establisher-success pump round-trip).