--- id: tunnels/corrmiss-forwarded-gate name: "API: the forwarded-op establisher variant (ForwardedGate + register_tunnel_forwarded_openable_with_establisher)" status: completed depends_on: [tunnels/corrmiss-adr] scope: moderate risk: medium impact: component level: implementation tags: [api, establisher, forwarded-op, correlation-miss, review-003] --- ## Description Implement the additive establisher variant for the forwarded op's serving half (plan 003 W1's API item): an assembly-injected correlation gate that the crate wraps into the alkcall `OpenEstablisher` dance, registered via `register_openable_with_establisher`: a stale `listen` token now returns a typed `channel:open_failed` before the channel's data plane is served, instead of succeeding at the call plane and declining post-open (the phantom-channel shape the review records as fully silent on the fabric side). Candidate shape (the plan's W1 design decision to finalize, which this task finalizes it): ```rust /// The injected correlation gate: `Err` at the gate → typed /// `channel:open_failed` before the channel is born. pub type ForwardedGate = Arc< dyn Fn(&TunnelForwardedParams) -> Result<(), TunnelEstablishError> + Send + Sync, >; pub fn register_tunnel_forwarded_openable_with_establisher( core: &ChannelCore, on_registry: &Arc, auth: AuthContext, gate: ForwardedGate, handler: ForwardedHandler, ) -> Result<(), String> ``` ### The design decisions to finalize (and pin in doc comments) 1. **Gate input vs raw input.** The gate receives the PARSED `TunnelForwardedParams`; the establisher parses (the params' serde pins, `deny_unknown_fields`, positive `listen`, are the loud gates; `parse` failure at the establisher maps through `EstablishmentError::HandlerError` → a typed `channel:open_failed`, which is an improvement over the base shape's silent birth-teardown for a malformed ingress; pin this deliberately, do not drift it). The handler still receives the raw input and re-parses as it does today, so keep `make_forwarded_op_handler` unchanged (the double parse is cheap and preserves deny-unknown at both layers; the handler must stay usable by the base shape). 2. **The establishment answer.** A gate pass produces no resource handle (the correlation map is assembly state; the handler needs nothing it does not re-derive). It returns `Establishment::new(plan)` with an empty/marker plan payload (same typed-opaque `ChannelPlan` posture as ADR-049 amendment 2; `()` or an empty marker struct is the payload choice to pin). No reply-field contribution (the forwarded op's reply stays the wrapper's `channel_id`). 3. **Error mapping.** A gate `Err(TunnelEstablishError)` maps through the EXISTING `From for EstablishmentError` table (`src/producer.rs:142-154`); a stale token is `UnknownResource` → `details.reason == "unknown_resource"` on the wire. Do not mint new variants. The gate SHOULD document that a miss is `TunnelEstablishError::UnknownResource` (that is the canonical reason the ADR pins), but the crate passes whatever the gate returns through. 4. **No timeout change**: `timeout: None` (the alkcall 10s default) as the other registrations do; the gate is an in-memory map lookup. 5. **No scope-table change**: the same `tunnel_forwarded_spec()` / `tunnel:forwarded` gate runs (the registry runs the ACL before the establishment phase, alkcall's open-op wrapper order; pin the order in a doc comment + a test, see the tests task). ### Also in this task (the doc half of W1's API item) - Update the in-code docs that describe the miss as assembly-side-only: `make_forwarded_op_handler`'s correlation-miss-semantics block (`src/producer.rs:1014-1020`) and `register_tunnel_forwarded_openable`'s doc (1047-1056); both gain the two-shape posture: the establisher variant is the recommended shape (the miss is a typed failure at the open); the base shape stays for compatibility and keeps the post-open teardown-at-birth semantics (still the residual-race fallback under the establisher variant). - Re-export the new surface from `lib.rs` (convention 16). - The base `register_tunnel_forwarded_openable` stays byte-for-byte compatible (additive-only, per plan decision 4). NOT in this task: the sibling-repo adoptions (alkssh W2 / alksocks W3, pointer tasks exist) and the gate-miss tests (the dedicated tests task; but do not land the API without landing its tests task after). ## Acceptance Criteria - [ ] `ForwardedGate` type + `register_tunnel_forwarded_openable_with_establisher` landed in `src/producer.rs`, re-exported from `lib.rs` - [ ] The establisher parse-failure → typed `channel:open_failed` mapping is deliberate + documented (design decision 1) - [ ] Gate errors pass through the existing `From` table; no new error variants - [ ] `make_forwarded_op_handler` / the base registration are unchanged in behavior (byte-for-byte compat) - [ ] The in-code docs updated: the two-shape posture (establisher = recommended; base = compatibility + residual-race fallback) replaces the assembly-side-only framing - [ ] `cargo test` green (existing suite untouched); clippy `--all-targets -- -D warnings` clean (native + wasm32); `cargo fmt --check` clean; default crate stays wasm-clean - [ ] `--all-features` test run green (the `local` feature surface is untouched) ## References - docs/plans/003-correlation-miss-resolve.md §W1 (the API bullet; the decisions this finalizes) - docs/architecture/decisions/008-listen-metadata-forwarded-op.md §3 + the ADR this task's predecessor lands (Amendment 2 or successor; cite whichever the ADR task landed) - src/producer.rs:982-1082 (the base serving shape: `ForwardedHandler`, `make_forwarded_op_handler`, `register_tunnel_forwarded_openable`); 142-154 (the `From` table); 641-673 (the `bindfirst_establisher`, the wrap-into-`OpenEstablisher` closure pattern to mirror, including the `Box::pin` shape) - alkcall 0.10.0 `ChannelCore::register_openable_with_establisher` (src/channels/operations.rs:761; the establisher runs AFTER allocation, BEFORE the reply; failure → typed `channel:open_failed`, no phantom channel; `timeout: None` = the 10s default) - AGENTS.md conventions 11 (the ADR-049 establishment surface is load-bearing; typed-opaque plans), 8 (teardown-at-birth posture), 16 (re-export posture) ## Notes - `ForwardedGate` (sync closure over the parsed `TunnelForwardedParams`) wraps into a private `make_forwarded_gate_establisher` → `register_openable_with_establisher(spec, Some(...), make_forwarded_op_handler(handler), registry, auth, None)`, so the base handler is reused byte-for-byte and the base registration stays untouched (additive-only). - The design decisions were finalized exactly as the task sketched: (1) gate receives the PARSED params; establisher parse failure maps `TunnelEstablishError::HandlerError` → typed `channel:open_failed` (pinned in the establisher doc, an improvement over the base shape's silent birth-teardown for malformed ingress); the handler re-parses the raw input (deny-unknown at both layers, handler unchanged); (2) gate pass → `Establishment::new(plan)` with an EMPTY MARKER plan payload, pinned as a new pub `ForwardedPlan` unit struct (chosen over `()`: self-documenting, downcast-able by assemblies that want to assert the gate ran; the same typed-opaque posture as ADR-049 amendment 2); no reply-field contribution; (3) gate errors pass through the existing `From` table, no new variants; the gate doc pins `UnknownResource` as the canonical stale-token reason; (4) `timeout: None` (10s default); (5) no scope-table change; the wrapper order (schema → ACL → allocate → establisher → handler) is pinned in the establisher doc; the order-pinning test lands with the tests task. - Doc updates: `make_forwarded_op_handler`'s correlation-miss block + `register_tunnel_forwarded_openable`'s doc now carry the two-shape posture. Two adjacent doc drifts were synced in the same wave per ADR-008 Amendment 2 ("syncs with the API task's in-code doc updates"): `wire.md` §The Forwarded Op's "no establisher" prose line, and `tunnel_forwarded_spec()`'s "No establisher" doc block in `src/params.rs`. ## Summary Landed the forwarded-op establisher variant per ADR-008 Amendment 2: `ForwardedGate`, `ForwardedPlan`, `make_forwarded_gate_establisher` (private), and `register_tunnel_forwarded_openable_with_establisher` in `src/producer.rs`, re-exported from `lib.rs`. A gate `Err` (and an establisher parse failure) now yields a typed `channel:open_failed` before the channel is born; the base shape stays byte-for-byte compatible as the compatibility + residual-race-fallback posture. In-code docs updated (two-shape posture) plus the wire.md/params.rs doc sync the amendment rides on this task. Verification: `cargo test` green (existing suite untouched), clippy `--all-targets -D warnings` clean native + wasm32, `cargo fmt --check` clean, `cargo doc --no-deps` clean (0 warnings), `--all-features` test run green. Follow-ups: the dedicated gate-miss tests task (`tunnels/corrmiss-gate-tests`), then the sibling-repo adoptions (`corrmiss-alkssh-adopt`, `corrmiss-alksocks-adopt`).