feat(review 008 Unit 1): establisher reply projection (U-2, ADR-049 amendment 3)

The establisher can now contribute to the open-op success reply — the
establisher → opener direction amendment 2 left empty. First consumer:
alktunnels ADR-008's bind-first listen establisher, whose observed
OS-chosen bound address rides the reply as an additive `bound` field
(the SOCKS5 BIND reply#1 BND.ADDR fidelity ask).

- Establishment gains reply_fields (private, builder-constructed):
  with_reply_field / with_reply_fields / reply_fields. The map shape
  generalizes beyond `bound` without a fourth amendment; the
  #[non_exhaustive] carrier (amendment 2) makes the extension additive
  — no construction-site break.
- run_open_wrapper merges contributed fields into the success output
  after reserving `channel_id`. The key is wrapper-owned: an
  establisher supplying it fails the open loudly
  (channel:open_failed, reason handler_error, message naming the
  reserved key), tearing the just-allocated channel down — never
  shadowing. Absent fields leave the reply byte-identical to the
  pre-amendment shape (asserted exactly).
- ChannelClient::open_channel_with_reply returns
  (channel_id, reply, send, recv) — the full success output — so
  consumers read `bound` first-class; open_channel delegates and
  discards the fields, signature unchanged.

Tests (all gates from the review + plan):
- projection produces { channel_id, bound } on the wire; no-fields
  replies are byte-identical (establisher and no-establisher shapes)
- reserved-key establisher fails with reason handler_error; channel
  torn down, ledger decremented, pump handler never spawned
- registry-level register_openable_with_establisher projects fields
- e2e over a real channels connection: bound reaches
  open_channel_with_reply; open_channel unchanged against the same
  accept side
- existing establishment tests (establisher-success round-trip,
  plan-flow, no-establisher compat) pass unchanged

Docs: ADR-049 amendment 3 (projection, reservation, read path,
compatibility posture, door type).

Verification: cargo test 637 passed; clippy -D warnings clean;
cargo fmt --check clean; cargo doc --no-deps clean.
This commit is contained in:
glm-5.3-flash committed 2026-09-16 10:27:12 +00:00
1 parent 1bc937c061
commit 82ddddf986
3 files changed
+560 -14

No files matched your search

@@ -6,7 +6,8 @@ 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. Amendment 3 (2026-09-16, review 008 U-2) adds the
establisher reply projection — see "Amendment 3" below.
## Context
@@ -379,8 +380,8 @@ is unfixable within that shape; alktty documented the same wall
(backend `allocate` cannot cross, so failure classes stayed in-band —
the phantom-channel shape ADR-049 removed, alive one layer down).
The plan is now real: `Establishment { plan: Option<ChannelPlan> }`
with `ChannelPlan = Arc<dyn Any + Send + Sync>` — **typed-opaque, not
The plan is now real: `Establishment { plan: Option<ChannelPlan> }` with
`ChannelPlan = Arc<dyn Any + Send + Sync>` — **typed-opaque, not
`serde_json::Value`**. The review's `Option<Value>` sketch could not
satisfy its own verification gate ("establisher dials, `plan` carries
the handle"): the payloads establishers actually hand off are live
@@ -414,4 +415,67 @@ and the registration entry points, plus a `debug!` telemetry line in
`run_open_wrapper` when a handler exits without having accepted the
channel's `BiStream` (the birth-teardown hint; the accept is
observable in-process via the yield-once source). §6's pinned
EOF-shaped panic semantics are unchanged.
EOF-shaped panic semantics are unchanged.
## Amendment 3 (establisher reply projection, 2026-09-16 — review 008 U-2)
Amendment 2 filled the establisher → handler direction (the plan);
the establisher → opener direction stayed empty — the wrapper
hardcodes the success reply to `channel_id`
(`run_open_wrapper`'s `ResponseEnvelope::ok`). alktunnels ADR-008's
bind-first listen establisher ends establishment at bind time, and the
observed OS-chosen bound address must ride the open-op reply as an
additive `"bound"` field (the SOCKS5 BIND reply#1 `BND.ADDR` fidelity
ask) — the alternative (an out-of-band query op) is a new wire
surface, strictly worse than an optional reply field.
**The decision: `Establishment` gains optional reply fields the
wrapper merges into the success output.**
- The carrier: `Establishment.reply_fields: Option<Map<String,
Value>>` (private, builder-constructed) —
`Establishment::new(plan).with_reply_field("bound", json!({...}))`,
plus `with_reply_fields(map)` (batch) and `reply_fields()` (read).
The map shape generalizes beyond `bound` without a fourth
amendment; `#[non_exhaustive]` (amendment 2) made the carrier
extension additive — no construction-site break.
- The merge: on establisher success the wrapper extends the success
output with the contributed fields AFTER reserving `channel_id`.
**The `channel_id` reservation:** the key is wrapper-owned; an
establisher supplying it is an establisher bug — the wrapper fails
the open loudly (`channel:open_failed`, reason `handler_error`,
message naming the reserved key) and tears the just-allocated
channel down, never silently shadowing its own value. Contributed
fields otherwise merge flat; the output schema remains the op's own
concern (the producing crate documents its optional fields —
alktunnels' listen-op spec documents `bound`).
- The client read path: `ChannelClient::open_channel_with_reply`
returns `(channel_id, reply, send, recv)` — the full success
output — so a consumer reads `bound` without dropping to
`call_open_op` + manual `adopt_channel`;
`open_channel` delegates and discards the extra fields, signature
unchanged.
- Compatibility: absent reply fields leave the reply byte-identical
to the pre-amendment shape (`{ channel_id }`) — verified by test.
Old consumers (`open_channel` extracting `channel_id`, ignoring
unknown fields) are unaffected by a NEW field; new consumers reading
`bound` against an OLD alkcall see the field absent — the additive
posture alktunnels ADR-008 §2 pins.
Door type: the reply fields are an additive call-plane JSON surface
(like `channel:open_failed`'s `details` in §3 — consumers ignore
unknown fields by the envelope's own parse posture); the field set an
op may contribute is the producing crate's schema concern, not a
protocol vocabulary. No data-plane change.
Implemented surface (alkcall 0.7.2): `Establishment::with_reply_field`
/ `with_reply_fields` / `reply_fields`, the wrapper's
reservation check (`RESERVED_REPLY_KEY` = `channel_id`) +
`merge_reply_fields`, `ChannelClient::open_channel_with_reply`.
Verification gates landed as tests: the projection produces
`{ channel_id, bound }` on the wire; no-fields (establisher or not)
produces exactly `{ channel_id }` (byte-identical); the reserved-key
establisher fails with reason `handler_error`, channel torn down,
ledger decremented, handler never spawned; the e2e gate carries
`bound` through a real channels connection to
`open_channel_with_reply` while `open_channel` stays unchanged.