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:
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.
|
||||
Reference in new issue
Block a user