feat(producer): the forwarded-op establisher variant — ForwardedGate + register_tunnel_forwarded_openable_with_establisher
Implements plan 003 W1's API item (ADR-008 Amendment 2, task tunnels/corrmiss-forwarded-gate): the assembly-injected correlation gate wrapped into the alkcall OpenEstablisher dance for the forwarded op's serving half. - ForwardedGate: sync gate closure over the parsed TunnelForwardedParams; a gate Err → typed channel:open_failed before the channel is born (no phantom channel) - register_tunnel_forwarded_openable_with_establisher: reuses make_forwarded_op_handler unchanged (additive-only; the base registration stays byte-for-byte compatible as the compatibility + residual-race-fallback posture) - ForwardedPlan: empty marker plan payload (typed-opaque posture, payload choice pinned); no reply-field contribution - Establisher parse failure maps TunnelEstablishError::HandlerError → channel:open_failed (deliberately pinned improvement over the base shape's silent birth-teardown for malformed ingress); gate errors pass through the existing From<TunnelEstablishError> table (no new variants; stale token = unknown_resource); timeout: None; wrapper order (schema → ACL → allocate → establisher → handler) pinned in the establisher doc - Two-shape posture doc updates: make_forwarded_op_handler's correlation-miss block, register_tunnel_forwarded_openable's doc; wire.md §The Forwarded Op + tunnel_forwarded_spec() doc synced per Amendment 2's "syncs with the API task's in-code doc updates" Verification: cargo test (85 green, existing suite untouched); clippy --all-targets -D warnings (native + wasm32); cargo fmt --check; cargo doc --no-deps (0 warnings); --all-features test run green
This commit is contained in:
1 parent
7d2b6d11f0
commit
5b2f7255f7
5 files changed
+213
-23
No files matched your search
@@ -153,7 +153,12 @@ PRODUCER opens a channel back toward the listen-opener side. The
|
||||
producer *calls* the op (the opener SERVES it — the ADR-047 §5
|
||||
connection-owner rule: the serving side allocates the `channel_id`,
|
||||
the producer-side initiator adopts). `Sub`-typed, ALPN marker
|
||||
`alk/tunnel`; no establisher (the data plane starts immediately).
|
||||
`alk/tunnel`; the serving side registers establisher-less by base
|
||||
shape (the data plane starts immediately) or — the recommended shape,
|
||||
ADR-008 Amendment 2 — with the correlation-gate establisher variant
|
||||
(a stale/missing token then fails the op as a typed
|
||||
`channel:open_failed` with `details.reason = "unknown_resource"`; the
|
||||
wire vocabulary is unchanged by the amendment).
|
||||
Scope gate (opener side): `["tunnel:forwarded"]`
|
||||
(`TUNNEL_FORWARDED_SCOPE`) — ingress, a separate grant from
|
||||
`tunnel:open`/`tunnel:direct` (ADR-006 Amendment 1).
|
||||
|
||||
@@ -42,6 +42,7 @@ pub use params::{TUNNEL_ALPN, TUNNEL_OPEN_SCOPE};
|
||||
pub use producer::{
|
||||
bindfirst_establisher, make_bindfirst_accept_handler, make_forwarded_op_handler,
|
||||
register_tunnel_bindfirst_openable, register_tunnel_forwarded_openable,
|
||||
register_tunnel_forwarded_openable_with_establisher, ForwardedGate, ForwardedPlan,
|
||||
};
|
||||
pub use producer::{
|
||||
direct_establisher, direct_establisher_with_witness, register_tunnel_direct_openable,
|
||||
|
||||
+8
-4
@@ -426,11 +426,15 @@ where
|
||||
/// fails the registry's schema validation (the typed
|
||||
/// `INVALID_INPUT` class) before the opener-side handler runs.
|
||||
///
|
||||
/// **No establisher** on the forwarded op — the data plane starts
|
||||
/// immediately (the opener-side serving registration is
|
||||
/// `register_openable`, handler = correlate by `listen` + accept the
|
||||
/// **Serving registration** (ADR-008 §3 as amended by Amendment 2):
|
||||
/// establisher-less base shape — `register_openable`, the data plane
|
||||
/// starts immediately (handler = correlate by `listen` + accept the
|
||||
/// `BiStream` + pump; the correlation registry is assembly-side
|
||||
/// bookkeeping, ADR-008 §3).
|
||||
/// bookkeeping) — or the recommended establisher variant,
|
||||
/// `register_tunnel_forwarded_openable_with_establisher` (the
|
||||
/// injected correlation gate; a stale/missing token fails the open as
|
||||
/// a typed `channel:open_failed`). The spec itself is unchanged: the
|
||||
/// same op id, params, scope, and reply surface under both shapes.
|
||||
pub fn tunnel_forwarded_spec() -> OperationSpec {
|
||||
OperationSpec::new(
|
||||
OP_TUNNEL_FORWARDED,
|
||||
|
||||
+152
-15
@@ -1011,13 +1011,22 @@ pub type ForwardedHandler =
|
||||
/// lifetime tracks it, so a correlation miss or a dropped assembly
|
||||
/// handler tears the channel down).
|
||||
///
|
||||
/// **Correlation-miss semantics (documented + test-pinned):** a
|
||||
/// `listen` token with no matching waiting session is the ASSEMBLY's
|
||||
/// rejection (it holds the map): its handler's error path drops the
|
||||
/// stream and returns — teardown-at-birth reaps the forwarded channel.
|
||||
/// The producer's accept loop drops the accepted connection when that
|
||||
/// open fails (the shapes mirror — per-accept isolation,
|
||||
/// `tunnels/bindfirst-producer`'s posture).
|
||||
/// **Correlation-miss semantics (two-shape posture, ADR-008 Amendment
|
||||
/// 2 + test-pinned):** under the BASE shape (this registration,
|
||||
/// establisher-less), a `listen` token with no matching waiting
|
||||
/// session succeeds at the call plane and the miss is the ASSEMBLY's
|
||||
/// post-open rejection (it holds the map): its handler's error path
|
||||
/// drops the stream and returns — teardown-at-birth reaps the
|
||||
/// forwarded channel (`correlation-miss`, the assembled decline
|
||||
/// event at `debug`). Under the ESTABLISHER variant
|
||||
/// ([`register_tunnel_forwarded_openable_with_establisher`] — the
|
||||
/// recommended shape), a no-route token fails at the open as a typed
|
||||
/// `channel:open_failed` before the channel is born; THIS handler's
|
||||
/// post-open miss is then only the residual race (a token vanishing
|
||||
/// between gate pass and handler run) — teardown-at-birth remains the
|
||||
/// fallback shape under both. The producer's accept loop drops the
|
||||
/// accepted connection when that open fails (the shapes mirror —
|
||||
/// per-accept isolation, `tunnels/bindfirst-producer`'s posture).
|
||||
pub fn make_forwarded_op_handler(handler: ForwardedHandler) -> OpenHandler {
|
||||
Arc::new(
|
||||
move |input: Value, _plan: Option<ChannelPlan>, conn: Connection, _auth: AuthContext| {
|
||||
@@ -1044,16 +1053,26 @@ pub fn make_forwarded_op_handler(handler: ForwardedHandler) -> OpenHandler {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register the forwarded op's opener-side serving openable (ADR-008
|
||||
/// §3): `ChannelCore::register_openable` — **no establisher** (the
|
||||
/// forwarded
|
||||
/// channel needs no establishment; its data plane starts immediately)
|
||||
/// — scope-gated `tunnel:forwarded` (ingress, a separate grant from
|
||||
/// `tunnel:open`/`tunnel:direct`, never implied by either — ADR-006
|
||||
/// Amendment 1), via [`crate::params::tunnel_forwarded_spec`]. The
|
||||
/// wrapper's
|
||||
/// Register the forwarded op's opener-side serving openable — the
|
||||
/// BASE shape (ADR-008 §3 as amended): `ChannelCore::register_openable`
|
||||
/// — **no establisher** (the forwarded channel needs no establishment;
|
||||
/// its data plane starts immediately) — scope-gated `tunnel:forwarded`
|
||||
/// (ingress, a separate grant from `tunnel:open`/`tunnel:direct`,
|
||||
/// never implied by either — ADR-006 Amendment 1), via
|
||||
/// [`crate::params::tunnel_forwarded_spec`]. The wrapper's
|
||||
/// `channel_id` allocation IS the reply (the producer adopts it).
|
||||
///
|
||||
/// **Two-shape posture (ADR-008 Amendment 2):** this base shape stays
|
||||
/// byte-for-byte compatible for assemblies that prefer the post-open
|
||||
/// decline (and as the residual-race fallback posture — see
|
||||
/// [`make_forwarded_op_handler`]'s correlation-miss block). The
|
||||
/// ESTABLISHER variant
|
||||
/// ([`register_tunnel_forwarded_openable_with_establisher`]) is the
|
||||
/// recommended shape: a stale/missing token fails the open as a typed
|
||||
/// `channel:open_failed` before the channel is born, instead of
|
||||
/// succeeding at the call plane and declining post-open. Both ride
|
||||
/// the same op id, params, scope, and reply surface.
|
||||
///
|
||||
/// Identity (CF-005 precedence, upstream machinery): the opener-side
|
||||
/// gate sees the PRODUCER's per-call identity (the producer may carry
|
||||
/// an `auth_token` when it is itself forwarding); the `peer` param is
|
||||
@@ -1081,6 +1100,124 @@ pub fn register_tunnel_forwarded_openable(
|
||||
)
|
||||
}
|
||||
|
||||
/// The forwarded op's establisher plan payload (ADR-008 Amendment 2):
|
||||
/// an empty marker — a gate pass establishes nothing (the correlation
|
||||
/// map is assembly state and the handler needs nothing it does not
|
||||
/// re-derive), the same typed-opaque [`ChannelPlan`] posture as ADR-049
|
||||
/// amendment 2 with the payload choice pinned at "marker". Its `Some`
|
||||
/// presence in the handler's `plan` parameter signals the gate passed;
|
||||
/// the handler is free to ignore it.
|
||||
pub struct ForwardedPlan;
|
||||
|
||||
/// The injected correlation gate (ADR-008 Amendment 2, plan 003 W1):
|
||||
/// the assembly's one-shot correlation check for the forwarded op's
|
||||
/// serving half. `Err` at the gate → a typed `channel:open_failed`
|
||||
/// BEFORE the channel is born (no phantom channel — ADR-049's posture
|
||||
/// extended to the forwarded op's serving side).
|
||||
///
|
||||
/// The gate receives the PARSED [`TunnelForwardedParams`] and runs
|
||||
/// synchronously — it must be an in-memory map lookup (an await there
|
||||
/// would extend the open's call-plane latency; the lookup shape is
|
||||
/// what every assembly's correlation registry provides).
|
||||
///
|
||||
/// **Gate error convention (the pin):** a stale/missing `listen`
|
||||
/// token SHOULD return [`TunnelEstablishError::UnknownResource`] —
|
||||
/// the waiting session IS a resource that is gone, and that maps to
|
||||
/// `details.reason == "unknown_resource"` on the wire (the canonical
|
||||
/// reason ADR-008 Amendment 2 pins; the wire vocabulary is NOT
|
||||
/// extended). The crate passes through whatever the gate returns —
|
||||
/// the gate owns its rejection vocabulary, within the existing
|
||||
/// [`TunnelEstablishError`] table.
|
||||
pub type ForwardedGate =
|
||||
Arc<dyn Fn(&TunnelForwardedParams) -> Result<(), TunnelEstablishError> + Send + Sync>;
|
||||
|
||||
/// The gate establisher (ADR-008 Amendment 2 — the correlation gate
|
||||
/// wrapped into the alkcall [`OpenEstablisher`] dance; assemblies do
|
||||
/// not hand-roll the plumbing, the crate-owned registration surface
|
||||
/// below is the only supported path).
|
||||
///
|
||||
/// **Order (pinned, ADR-008 Amendment 2 decision 3):** the registry
|
||||
/// runs the `tunnel:forwarded` ACL BEFORE the establishment phase
|
||||
/// (alkcall's open-op wrapper order: schema → ACL → allocate →
|
||||
/// establisher → handler), so the gate never sees an unauthorized
|
||||
/// open — a scope-less producer is `FORBIDDEN` before any gate
|
||||
/// lookup. The establisher then parses the input and runs the gate;
|
||||
/// the base shape's handler-side parse stays (see below), so
|
||||
/// `deny_unknown_fields` + the positive-`listen` serde pin gate the
|
||||
/// ingress at BOTH layers.
|
||||
///
|
||||
/// Error mapping (pinned):
|
||||
/// - Establisher parse failure →
|
||||
/// [`TunnelEstablishError::HandlerError`] → the existing
|
||||
/// `From<TunnelEstablishError> for EstablishmentError` table →
|
||||
/// `channel:open_failed` with `details.reason == "handler_error"`.
|
||||
/// This is deliberate: a malformed ingress becomes a typed
|
||||
/// call-plane failure instead of the base shape's silent
|
||||
/// birth-teardown (an improvement ADR-008 Amendment 2 pins — do
|
||||
/// not drift it).
|
||||
/// - Gate `Err(TunnelEstablishError)` → the same existing table, 1:1
|
||||
/// (a stale token: `UnknownResource` → `details.reason ==
|
||||
/// "unknown_resource"`). No new variants are minted.
|
||||
/// - A gate pass produces NO resource handle: the plan payload is the
|
||||
/// empty [`ForwardedPlan`] marker, and there is NO reply-field
|
||||
/// contribution (the op's reply stays the wrapper's reserved
|
||||
/// `channel_id`).
|
||||
///
|
||||
/// `timeout: None` (the alkcall 10s default, as the other
|
||||
/// registrations) — the gate is an in-memory lookup; the bound is
|
||||
/// belt-and-braces, not workload.
|
||||
fn make_forwarded_gate_establisher(gate: ForwardedGate) -> OpenEstablisher {
|
||||
Arc::new(move |input: Value, _auth: AuthContext| {
|
||||
let gate = Arc::clone(&gate);
|
||||
Box::pin(async move {
|
||||
let params: TunnelForwardedParams = serde_json::from_value(input.clone())
|
||||
.map_err(|e| TunnelEstablishError::HandlerError(format!("invalid params: {e}")))?;
|
||||
gate(¶ms)?;
|
||||
Ok(Establishment::new(Arc::new(ForwardedPlan) as ChannelPlan))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Register the forwarded op's opener-side serving openable WITH the
|
||||
/// correlation-gate establisher (ADR-008 Amendment 2 — the
|
||||
/// recommended shape): `ChannelCore::register_openable_with_establisher`
|
||||
/// with the crate-owned gate establisher (private
|
||||
/// `make_forwarded_gate_establisher`) wrapping the injected
|
||||
/// [`ForwardedGate`]. A stale/missing `listen` token fails the open
|
||||
/// as a typed `channel:open_failed` BEFORE the channel's data plane
|
||||
/// is served — the phantom-channel shape (succeed at the call plane,
|
||||
/// decline post-open, pump EOFs silently) is closed at the open.
|
||||
///
|
||||
/// The same op, params, scope (`tunnel:forwarded`, run before the
|
||||
/// establisher — see the
|
||||
/// `make_forwarded_gate_establisher` doc) and reply
|
||||
/// surface as [`register_tunnel_forwarded_openable`] ride; the wire
|
||||
/// delta of Amendment 2 is empty (additive registration only).
|
||||
///
|
||||
/// The base shape (establisher-less) stays for compatibility + as the
|
||||
/// residual-race fallback posture: a token that vanishes between
|
||||
/// gate pass and handler run is post-open — the handler's
|
||||
/// teardown-at-birth semantics (see [`make_forwarded_op_handler`]'s
|
||||
/// correlation-miss block) are the only thing that can reap it, the
|
||||
/// same under both shapes. New assemblies take this shape by default
|
||||
/// (ADR-008 Amendment 2 §Additive-only surface).
|
||||
pub fn register_tunnel_forwarded_openable_with_establisher(
|
||||
core: &ChannelCore,
|
||||
on_registry: &Arc<OperationRegistry>,
|
||||
auth: AuthContext,
|
||||
gate: ForwardedGate,
|
||||
handler: ForwardedHandler,
|
||||
) -> Result<(), String> {
|
||||
core.register_openable_with_establisher(
|
||||
crate::params::tunnel_forwarded_spec(),
|
||||
Some(make_forwarded_gate_establisher(gate)),
|
||||
make_forwarded_op_handler(handler),
|
||||
on_registry,
|
||||
auth,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: tunnels/corrmiss-forwarded-gate
|
||||
name: "API: the forwarded-op establisher variant — ForwardedGate + register_tunnel_forwarded_openable_with_establisher"
|
||||
status: pending
|
||||
status: completed
|
||||
depends_on: [tunnels/corrmiss-adr]
|
||||
scope: moderate
|
||||
risk: medium
|
||||
@@ -136,8 +136,51 @@ NOT in this task: the sibling-repo adoptions (alkssh W2 / alksocks W3
|
||||
|
||||
## Notes
|
||||
|
||||
> To be filled by implementation agent
|
||||
- `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)`
|
||||
— the base handler is reused byte-for-byte, so 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 — same typed-opaque
|
||||
posture as ADR-049 amendment 2); no reply-field contribution;
|
||||
(3) gate errors pass through the existing
|
||||
`From<TunnelEstablishError>` 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
|
||||
|
||||
> To be filled on completion
|
||||
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`).
|
||||
Reference in new issue
Block a user