feat(review 008 Unit 3a): in-tree ChannelRelay (ADR-051, ADR-042 amendment)

- src/channels/relay.rs: ChannelRelay::register_relay_openable —
  registers a from_call-imported marked spec on a consumer-leg
  channel-0 registry via the full open-op wrapper (translate hop =
  the ADR-049 establisher calling the producer leg with the
  forwarded payload; byte-forward hop = pump_bidi, awaited inline).
  The relay map dissolves (implicit per-channel mapping; ids are
  per-connection), the spoke reply's channel_id is stripped, the
  reply's other fields ride with_reply_fields (bound flows
  end-to-end), reason mapping per ADR-051 §3 (spoke code+message
  preserved; timeout → dial_failed).
- Rejection postures (ADR-051 §6): unmarked and Pub-typed marked
  specs are loud assembly errors.
- Gate tests (9): registration rejections, reason mapping,
  forwarded_for payload shape, the real-wire consumer → hub → spoke
  round trip (bound survives, data both directions, the hub never
  parses the data plane), spoke-failure teardown (ledger
  decremented, no spoke channel leak), and a channels/tty/sub
  standard-shape run pinning no regression.

Verification: cargo test (658 passed), clippy --all-targets
-D warnings, fmt --check, doc --no-deps — all clean.
This commit is contained in:
2026-09-17 23:27:17 +00:00
parent 91765446b7
commit 90c182039e
3 changed files with 1244 additions and 0 deletions
+5
View File
@@ -27,6 +27,10 @@
//! (ADR-041, amended by ADR-047 §7 — opener ledger).
//! - [`pump`]: `pump_bidi` — the two-pump data-plane helper (alknet
//! ADR-078, pinned upstream by ADR-050).
//! - [`relay`]: `ChannelRelay` — the in-tree relay component (ADR-042
//! as amended by ADR-051): translate hop + `pump_bidi` byte-forward
//! hop for from_call-imported marked specs re-exposed on a hub's
//! consumer-leg channel-0 registry.
//! - [`client`]: `ChannelClient` — transport-agnostic
//! `from_connection` (ADR-043).
//! - [`self::env`]: `ChannelOperationEnv` extension trait (ADR-047 §4 —
@@ -43,5 +47,6 @@ pub mod operations;
pub mod policy;
pub mod pump;
pub mod reassembly;
pub mod relay;
pub mod source;
pub mod wire;
+346
View File
@@ -0,0 +1,346 @@
//! `ChannelRelay` — the in-tree channel relay component (ADR-042 as
//! amended by ADR-051; review 008 Unit 3a).
//!
//! A relay hub imports a producer leg's channel-open op (via
//! `from_call` discovery — the spec arrives reconstructed WITH the
//! `channel_open` marker, ADR-047 amendment 3) and re-exposes it on
//! its own consumer-leg channel-0 fork registry. The open op resolves
//! on the hub with a hub-allocated `channel_id`; the relay's
//! establisher (the translate hop, ADR-042 layer 1) opens the channel
//! on the producer leg and adopts the spoke-allocated id into the
//! producer-leg `ChannelManager`; the relay's `OpenHandler` (the
//! byte-forward hop, layer 2) pumps bytes between the two legs' data
//! planes with `pump_bidi` — the hub never parses chunk framing
//! (ADR-034/ADR-035: ids are per-connection, no rewrite exists).
//!
//! The consumer leg keeps the full open-op wrapper machinery (ACL,
//! per-identity cap/ledger, establishment bound, teardown-on-failure)
//! — the relay registers through
//! [`ChannelCore::register_openable_with_establisher`], never a
//! parallel authorization path.
//!
//! See ADR-042 (the relay contract), ADR-051 (the in-tree design:
//! §2 what the relay holds, §3 the reason mapping, §4 the
//! registration seam, §6 the rejection postures).
use std::sync::Arc;
use serde_json::{Map, Value};
use crate::core::auth::AuthContext;
use crate::protocol::connection::CallConnection;
use crate::protocol::wire::CallError;
use crate::registry::registration::OperationRegistry;
use crate::registry::spec::{OperationSpec, OperationType};
use super::manager::ChannelManager;
use super::operations::{
ChannelCore, Establishment, EstablishmentError, OpenEstablisher, OpenHandler,
CHANNEL_OPEN_FAILED, RESERVED_REPLY_KEY,
};
use super::pump::pump_bidi;
use super::reassembly::{MpscRecvStream, MpscSendStream};
/// The producer-leg surface the relay holds (ADR-051 §2): the
/// `CallConnection` for the translate hop's open call plus the
/// producer-leg `ChannelManager` for `adopt_channel` and teardown
/// visibility. Not a `ChannelClient` — the hub's `CallConnection` is
/// shared by import, relay establisher, and hub ops (three claimants),
/// so `take_call_connection`'s detach is wrong for a hub. A hub that
/// dials the spoke with `ChannelClient::from_connection`, imports via
/// `from_call`, then tears the client down keeps the relay alive on
/// these `Arc`s.
#[derive(Clone)]
pub struct ProducerLeg {
/// The producer leg's call connection — the hub's channel-0
/// connection to the spoke, the one `from_call` imported the open
/// op over.
pub call: Arc<CallConnection>,
/// The producer leg's channel manager — the relay's establisher
/// adopts the spoke-allocated channel id here.
pub manager: ChannelManager,
}
/// The plan payload the relay's establisher hands its pump handler:
/// the spoke-leg streams. The establisher and the handler agree on
/// this concrete type (the `ChannelPlan` downcast contract, ADR-049
/// amendment 2); alkcall never inspects it.
struct RelayPlan {
spoke_send: MpscSendStream,
spoke_recv: MpscRecvStream,
}
/// The relay component (ADR-051). Holds the producer-leg surface;
/// [`ChannelRelay::register_relay_openable`] registers a
/// from_call-imported marked spec on a consumer-leg channel-0 fork
/// registry with the translate + byte-forward machinery.
#[derive(Clone)]
pub struct ChannelRelay {
producer_leg: ProducerLeg,
}
impl ChannelRelay {
/// Construct a relay over a producer leg.
pub fn new(producer_leg: ProducerLeg) -> Self {
Self { producer_leg }
}
/// The producer-leg surface this relay translates onto.
pub fn producer_leg(&self) -> &ProducerLeg {
&self.producer_leg
}
/// Register a from_call-imported marked spec as a relay openable
/// on the consumer-leg channel-0 registry (ADR-051 §4 phase 2 —
/// the fork is the `consumer_registry` argument).
///
/// - `consumer_core` is the consumer leg's [`ChannelCore`] (its
/// channel-0 manager + lifecycle policy) — the full open-op
/// wrapper machinery (ACL, cap/ledger, establishment bound,
/// teardown) runs on the consumer leg exactly as for a locally
/// produced open op.
/// - `spec` must carry the `channel_open` marker (reconstructed
/// WITH the marker by discovery, ADR-047 amendment 3). A
/// `Pub`-typed marked spec is a loud assembly error
/// ([`RelayError::PubTypedOpen`] — the C-08 blocker, ADR-051
/// §6); an unmarked spec is likewise an error — branch on the
/// marker before calling.
/// - The translate hop calls the producer leg with the spec's own
/// op name; a hub that re-names the consumer-visible op composes
/// its own establisher via
/// [`ChannelCore::register_openable_with_establisher`] instead.
pub fn register_relay_openable(
&self,
consumer_core: &ChannelCore,
consumer_registry: &OperationRegistry,
spec: OperationSpec,
consumer_auth: AuthContext,
) -> Result<(), RelayError> {
if spec.channel_open.is_none() {
return Err(RelayError::UnmarkedSpec {
message: format!(
"spec `{}` has no channel_open marker — relay registration is \
for from_call-imported marked specs (ADR-051 §6)",
spec.name
),
});
}
if spec.op_type == OperationType::Pub {
return Err(RelayError::PubTypedOpen {
message: format!(
"spec `{}` is a Pub-typed channel-open op — the Pub open path is \
not implemented (C-08 blocker, `channel:pub_open_not_implemented` \
class; ADR-051 §6)",
spec.name
),
});
}
let producer_call = Arc::clone(&self.producer_leg.call);
let producer_manager = self.producer_leg.manager.clone();
let op_name = spec.name.clone();
let establisher: OpenEstablisher = Arc::new(move |input, per_call_auth| {
let producer_call = Arc::clone(&producer_call);
let producer_manager = producer_manager.clone();
let op_name = op_name.clone();
Box::pin(async move {
open_on_producer_leg(
&producer_call,
&producer_manager,
&op_name,
input,
&per_call_auth,
)
.await
})
});
let open_handler: OpenHandler = Arc::new(move |_input, plan, channel_conn, _auth| {
tokio::spawn(async move {
let Some(plan) = plan else {
return;
};
// The plan is uniquely held here (establisher →
// wrapper → handler, moved each hop), so `try_unwrap`
// succeeds; on the phantom-holder case the wrapper's
// no-accept telemetry fires and the spoke leg EOFs
// when the plan finally drops.
let Ok(relay_plan) = plan.downcast::<RelayPlan>() else {
tracing::debug!("relay: establisher plan of unexpected type");
return;
};
let Ok(relay_plan) = Arc::try_unwrap(relay_plan) else {
tracing::debug!("relay: establisher plan held elsewhere; skipping pump");
return;
};
let Ok(bidi) = channel_conn.accept_bi().await else {
return;
};
// The byte-forward hop (ADR-042 layer 2, ADR-050):
// two pumps joined inline (R-02). On completion the
// pump's drops EOF the spoke leg; the wrapper's
// handler-exit teardown covers the consumer leg.
let _ = pump_bidi(bidi, relay_plan.spoke_recv, relay_plan.spoke_send).await;
})
});
consumer_core
.register_openable_with_establisher(
spec,
Some(establisher),
open_handler,
consumer_registry,
consumer_auth,
None,
)
.map_err(|e| RelayError::Registration { message: e })
}
}
/// The translate hop (ADR-051 §1 layer 1, §2): call the producer leg's
/// open op with the forwarded payload, adopt the returned spoke
/// `channel_id` into the producer-leg manager, and return
/// `Establishment::new(plan)` whose plan carries the spoke-leg
/// streams. The spoke reply's `channel_id` is stripped (the consumer
/// reply carries the hub-allocated id); the reply's other fields ride
/// `with_reply_fields` (`bound` flows end-to-end, per-hop truthful).
async fn open_on_producer_leg(
producer_call: &Arc<CallConnection>,
producer_manager: &ChannelManager,
op_name: &str,
input: Value,
per_call_auth: &AuthContext,
) -> Result<Establishment, EstablishmentError> {
let payload = build_relay_payload(op_name, input, per_call_auth);
let response = producer_call.call_with_payload(payload).await;
let out = match response.result {
Ok(out) => out,
Err(spoke_error) => return Err(map_spoke_error(&spoke_error)),
};
let spoke_id = out
.get("channel_id")
.and_then(|v| v.as_u64())
.ok_or_else(|| EstablishmentError::HandlerError {
message: format!(
"spoke open reply for `{op_name}` missing `channel_id` — malformed responder"
),
})? as u32;
// Adopt the spoke-allocated id into the producer-leg manager so
// the spoke leg's mux/demux route this channel — the same shape
// `ChannelClient::open_channel` performs, minus the client. The
// producer leg is the connect side of the hub↔spoke channels
// connection, so the spoke's ids are even and disjoint from the
// hub's odd connect-side allocations.
let (spoke_send, spoke_recv) = producer_manager
.adopt_channel(spoke_id, "alk/relay", None)
.await
.map_err(|e| EstablishmentError::ResourceShortage {
message: format!(
"spoke channel {spoke_id} adopt failed on the hub's producer leg: {e}"
),
})?;
let mut reply_fields = Map::new();
if let Some(obj) = out.as_object() {
for (key, value) in obj {
if key != RESERVED_REPLY_KEY {
reply_fields.insert(key.clone(), value.clone());
}
}
}
let establishment = Establishment::new(Arc::new(RelayPlan {
spoke_send,
spoke_recv,
}) as super::operations::ChannelPlan);
Ok(if reply_fields.is_empty() {
establishment
} else {
establishment.with_reply_fields(reply_fields)
})
}
/// Map a spoke failure into the establisher vocabulary (ADR-051 §3 —
/// the reason table; the spoke's code and message are preserved in the
/// mapped variant's `message`, never discarded — a consumer debugging
/// a FORBIDDEN-through-relay sees the spoke's grant failure).
///
/// The `timeout` row is the one non-1:1 case: the consumer-visible
/// reason is `dial_failed` because establishment timeouts are
/// wrapper-generated (ADR-049 §3) and not an establisher vocabulary
/// word; the spoke's message carries the truth.
fn map_spoke_error(spoke_error: &CallError) -> EstablishmentError {
if spoke_error.code == CHANNEL_OPEN_FAILED {
let reason = spoke_error
.details
.as_ref()
.and_then(|d| d.get("reason"))
.and_then(|r| r.as_str())
.unwrap_or("");
let detail = spoke_error
.details
.as_ref()
.and_then(|d| d.get("message"))
.and_then(|m| m.as_str())
.unwrap_or(spoke_error.message.as_str());
let message = format!("spoke leg: {reason}: {detail}");
return match reason {
"dial_failed" | "timeout" => EstablishmentError::DialFailed { message },
"unknown_resource" => EstablishmentError::UnknownResource { message },
"resource_shortage" => EstablishmentError::ResourceShortage { message },
_ => EstablishmentError::HandlerError { message },
};
}
// `NOT_FOUND` (spoke predates the op), `FORBIDDEN` (hub lacks the
// spoke grant), and every other call-error class.
EstablishmentError::HandlerError {
message: format!("spoke leg: {} ({})", spoke_error.code, spoke_error.message),
}
}
/// Build the `call.requested` payload for the translate hop: the hub
/// as caller, the consumer as `forwarded_for` (ADR-026 §3, ADR-032 §3
/// — the same shape `build_forwarded_payload` produces for the plain
/// forwarding stubs). `forwarded_for` is metadata, never consulted by
/// any `AccessControl::check`; it is omitted when the consumer's call
/// carried no resolved identity.
fn build_relay_payload(op_name: &str, input: Value, per_call_auth: &AuthContext) -> Value {
let mut payload = Map::new();
payload.insert(
"operationId".to_string(),
Value::String(op_name.to_string()),
);
payload.insert("input".to_string(), input);
if let Some(originator) = &per_call_auth.identity {
if let Ok(value) = serde_json::to_value(originator) {
payload.insert("forwarded_for".to_string(), value);
}
}
Value::Object(payload)
}
/// The assembly-time errors `ChannelRelay::register_relay_openable`
/// produces (ADR-051 §6 — the rejection postures are loud, never
/// silent stubs).
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RelayError {
/// The spec carries no `channel_open` marker — the relay only
/// registers marked specs.
#[error("unmarked spec: {message}")]
UnmarkedSpec { message: String },
/// A `Pub`-typed marked spec — the Pub open path is not
/// implemented (C-08 blocker; `channel:pub_open_not_implemented`
/// class).
#[error("pub-typed open op: {message}")]
PubTypedOpen { message: String },
/// The consumer-leg registration itself failed (schema compile,
/// handler-kind mismatch).
#[error("registration failed: {message}")]
Registration { message: String },
}
#[cfg(test)]
#[path = "relay_tests.rs"]
mod tests;
+893
View File
@@ -0,0 +1,893 @@
//! Unit 3a gates (review 008; ADR-051 §"Verification gates").
//!
//! - The establisher populates `forwarded_for` from the consumer's
//! per-call identity and strips the spoke reply's `channel_id`.
//! - A spoke `channel:open_failed` maps to the consumer-leg
//! `channel:open_failed` with the mapped reason and the spoke's
//! message preserved; the consumer leg tears down (ledger
//! decremented); no spoke channel leaks (the adopt-then-fail window).
//! - `bound` (Unit 1's projection) survives the relay to the consumer
//! reply.
//! - Pub-typed and unmarked specs are rejected at registration.
//! - The full relay round trip over real channels connections
//! (consumer → hub → spoke), data both directions, the hub never
//! parsing the data plane.
use super::*;
use crate::channels::adapter::ChannelsAdapter;
use crate::channels::operations::ChannelCore;
use crate::channels::policy::NoCap;
use crate::core::auth::{AuthContext, Identity, IdentityProvider};
struct NoopIdProvider;
impl IdentityProvider for NoopIdProvider {
fn resolve_from_fingerprint(&self, _: &str) -> Option<Identity> {
None
}
fn resolve_from_token(&self, _: &crate::core::auth::AuthToken) -> Option<Identity> {
None
}
}
use crate::core::types::Connection;
use crate::protocol::connection::{split_single_stream, CallConnection};
use crate::protocol::dispatch::Dispatcher;
use crate::registry::registration::OperationRegistry;
use crate::registry::spec::{AccessControl, ChannelOpenSpec, Visibility};
use serde_json::json;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use tokio::io::AsyncReadExt;
const TEST_ADDR: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 4321);
const OP: &str = "channels/tunnel/direct";
const ALPN: &str = "alk/tunnel";
fn identity(id: &str, scopes: &[&str]) -> Identity {
Identity {
id: id.to_string(),
scopes: scopes.iter().map(|s| (*s).to_string()).collect(),
resources: Default::default(),
}
}
fn relay_spec(name: &str, op_type: OperationType, alpn: Option<String>) -> OperationSpec {
let mut spec = OperationSpec::new(
name,
op_type,
Visibility::External,
json!({ "type": "object" }),
json!({ "type": "object" }),
vec![],
AccessControl::default(),
None,
);
if let Some(alpn) = alpn {
spec = spec.with_channel_open(ChannelOpenSpec::new(alpn));
}
spec
}
// --- ADR-051 §6: the rejection postures ----------------------------------
#[tokio::test]
async fn unmarked_spec_is_rejected_at_registration() {
let producer_manager = manager_for_test();
let relay = ChannelRelay::new(ProducerLeg {
call: overlay_connection(),
manager: producer_manager,
});
let err = relay
.register_relay_openable(
&core_for_test(),
&OperationRegistry::new(),
relay_spec("plain/op", OperationType::Query, None),
AuthContext::anonymous(b"alk/call"),
)
.expect_err("unmarked spec must be rejected");
assert!(
matches!(err, RelayError::UnmarkedSpec { .. }),
"got {err:?}"
);
}
#[tokio::test]
async fn pub_typed_marked_spec_is_rejected_at_registration() {
let relay = ChannelRelay::new(ProducerLeg {
call: overlay_connection(),
manager: manager_for_test(),
});
let err = relay
.register_relay_openable(
&core_for_test(),
&OperationRegistry::new(),
relay_spec(
"channels/x/pub",
OperationType::Pub,
Some("alk/x".to_string()),
),
AuthContext::anonymous(b"alk/call"),
)
.expect_err("Pub-typed marked spec must be rejected");
assert!(
matches!(err, RelayError::PubTypedOpen { .. }),
"got {err:?}"
);
assert!(err.to_string().contains("pub_open_not_implemented"));
}
// --- ADR-051 §3: the reason mapping --------------------------------------
#[test]
fn spoke_open_failed_reasons_map_per_the_table() {
let dial = super::map_spoke_error(
&CallError::new("channel:open_failed", "x", false)
.with_details(json!({ "reason": "dial_failed", "message": "refused" })),
);
assert!(matches!(dial, EstablishmentError::DialFailed { .. }));
assert!(dial.message().contains("refused"), "{}", dial.message());
let timeout = super::map_spoke_error(
&CallError::new("channel:open_failed", "x", false)
.with_details(json!({ "reason": "timeout", "message": "spoke bound" })),
);
assert!(
matches!(timeout, EstablishmentError::DialFailed { .. }),
"timeout maps to dial_failed (ADR-051 §3)"
);
assert_eq!(
timeout.message(),
"spoke leg: timeout: spoke bound",
"the spoke's message is preserved"
);
let unknown = super::map_spoke_error(
&CallError::new("channel:open_failed", "x", false)
.with_details(json!({ "reason": "unknown_resource", "message": "no op" })),
);
assert!(matches!(
unknown,
EstablishmentError::UnknownResource { .. }
));
let shortage = super::map_spoke_error(
&CallError::new("channel:open_failed", "x", false)
.with_details(json!({ "reason": "resource_shortage", "message": "ports" })),
);
assert!(matches!(
shortage,
EstablishmentError::ResourceShortage { .. }
));
let handler = super::map_spoke_error(
&CallError::new("channel:open_failed", "x", false)
.with_details(json!({ "reason": "handler_error", "message": "boom" })),
);
assert!(matches!(handler, EstablishmentError::HandlerError { .. }));
}
#[test]
fn spoke_call_error_classes_map_to_handler_error_with_code_preserved() {
for code in ["NOT_FOUND", "FORBIDDEN", "TIMEOUT"] {
let mapped = super::map_spoke_error(&CallError::new(code, format!("{code} detail"), false));
assert!(
matches!(mapped, EstablishmentError::HandlerError { .. }),
"{code} maps to HandlerError"
);
assert!(
mapped.message().contains(code),
"the spoke's code is preserved in the message: {}",
mapped.message()
);
}
}
// --- ADR-026 §3: the forwarded payload -----------------------------------
#[test]
fn relay_payload_populates_forwarded_for_from_consumer_identity() {
let auth = AuthContext {
identity: Some(identity("alice", &["tunnel:direct"])),
..AuthContext::anonymous(b"alk/call")
};
let payload = super::build_relay_payload("channels/tunnel/direct", json!({ "t": "x" }), &auth);
assert_eq!(payload["operationId"], "channels/tunnel/direct");
assert_eq!(payload["input"], json!({ "t": "x" }));
assert_eq!(payload["forwarded_for"]["id"], "alice");
assert_eq!(payload["forwarded_for"]["scopes"][0], "tunnel:direct");
}
#[test]
fn relay_payload_omits_forwarded_for_when_identity_none() {
let auth = AuthContext::anonymous(b"alk/call");
let payload = super::build_relay_payload("channels/tunnel/direct", json!({}), &auth);
assert!(payload.get("forwarded_for").is_none());
}
// --- helpers --------------------------------------------------------------
fn overlay_connection() -> Arc<CallConnection> {
Arc::new(CallConnection::new_overlay_only(identity("spoke", &[])))
}
fn manager_for_test() -> ChannelManager {
let (_client, server) = tokio::io::duplex(1024);
let (_reader, writer) = tokio::io::split(server);
let (handle, runner) = super::super::mux::MuxRunner::new(Box::new(writer));
tokio::spawn(async move {
let _ = runner.run().await;
});
ChannelManager::with_defaults(handle, None)
}
fn core_for_test() -> ChannelCore {
ChannelCore::new(manager_for_test(), super::super::policy::default_policy())
}
// --- the real-wire relay round trip (ADR-051 gates 1/2/3) -----------------
//
// Topology: consumer —(alk/channels)— hub —(alk/channels)— spoke.
// The spoke serves `channels/tunnel/direct` (an establisher that
// contributes `bound` and an echo data-plane handler). The hub imports
// it with `from_call` and re-exposes the reconstructed marked spec on
// the consumer leg's channel-0 fork via
// `ChannelRelay::register_relay_openable`. The consumer opens through
// the hub: the open resolves with the hub-allocated `channel_id` (the
// spoke reply's id is stripped), the spoke's `bound` survives the
// relay, and data flows both directions without the hub parsing the
// data plane.
fn spoke_open_handler(data_tx: tokio::sync::mpsc::Sender<String>) -> OpenHandler {
Arc::new(move |_input, _plan, channel_conn, _auth| {
let data_tx = data_tx.clone();
tokio::spawn(async move {
let Ok(mut bidi) = channel_conn.accept_bi().await else {
return;
};
let mut buf = Vec::new();
if tokio::io::AsyncReadExt::read_to_end(&mut bidi, &mut buf)
.await
.is_err()
{
return;
}
let _ = data_tx
.send(String::from_utf8_lossy(&buf).to_string())
.await;
// Echo back upper-cased, then EOF.
use tokio::io::AsyncWriteExt;
let _ = bidi.write_all(buf.to_ascii_uppercase().as_slice()).await;
let _ = bidi.shutdown().await;
})
})
}
#[tokio::test]
async fn relay_round_trip_consumer_hub_spoke_with_bound_and_data() {
use crate::channels::operations::Establishment;
use crate::client::from_call;
use crate::client::FromCallConfig;
use tokio::io::AsyncWriteExt;
// ---- the spoke (producer leg) ----
let (data_tx, mut data_rx) = tokio::sync::mpsc::channel::<String>(4);
let spoke_open_handler = spoke_open_handler(data_tx);
let spoke_establisher: OpenEstablisher = Arc::new(|_input, _auth| {
Box::pin(async {
Ok(Establishment::default()
.with_reply_field("bound", json!({ "host": "203.0.113.9", "port": 42113 })))
})
});
let spoke_install: crate::channels::adapter::InstallChannelZero = {
let establisher = Arc::clone(&spoke_establisher);
Arc::new(move |manager, channel0_conn, auth| {
let establisher = Arc::clone(&establisher);
let open_handler = spoke_open_handler.clone();
tokio::spawn(async move {
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
return;
};
let (writer, reader) = split_single_stream(channel0_bidi);
let core = ChannelCore::new(manager, super::super::policy::default_policy());
let registry = Arc::new(OperationRegistry::new());
crate::registry::discovery::install_bootstrap_discovery(&registry)
.expect("spoke bootstrap discovery");
let spec = relay_spec(
"channels/tunnel/direct",
OperationType::Sub,
Some("alk/tunnel".to_string()),
);
core.register_openable_with_establisher(
spec,
Some(establisher),
open_handler,
&registry,
auth.clone(),
None,
)
.expect("spoke registers the direct open op");
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
Dispatcher::new(
registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
)
.serve_single_stream(call_connection, reader, writer)
.await;
})
})
};
let (hub_spoke_client_end, hub_spoke_server_end) = tokio::io::duplex(64 * 1024);
let hub_conn_to_spoke = Connection::from_bidi(
hub_spoke_client_end,
b"alk/channels".to_vec(),
Some(TEST_ADDR),
);
let spoke_conn = Connection::from_bidi(
hub_spoke_server_end,
b"alk/channels".to_vec(),
Some(TEST_ADDR),
);
let spoke_adapter = ChannelsAdapter::new(spoke_install, Arc::new(NoCap));
let _spoke_task = tokio::spawn(async move {
let auth = AuthContext::anonymous(b"alk/channels");
let _ =
crate::core::types::ProtocolHandler::handle(&spoke_adapter, spoke_conn, &auth).await;
});
// ---- the hub: dial the spoke, discover, stash ----
let hub_client_to_spoke =
crate::channels::client::ChannelClient::from_connection(hub_conn_to_spoke)
.await
.expect("hub's channel client to the spoke");
let producer_call = hub_client_to_spoke
.take_call_connection()
.await
.expect("hub takes its channel-0 CallConnection to the spoke");
let bundles = from_call(&producer_call, FromCallConfig::new())
.await
.expect("from_call discovers the spoke's ops");
let imported = bundles
.into_iter()
.find(|b| b.spec.name == OP)
.expect("the spoke's direct op is discovered");
assert!(
imported.spec.channel_open.is_some(),
"the flavor-form spec is reconstructed WITH the marker (ADR-047 amendment 3)"
);
assert_eq!(
imported
.spec
.channel_open
.as_ref()
.expect("marker")
.alpn
.as_ref(),
ALPN
);
// The hub keeps the producer-leg surface for the relay (ADR-051
// §2): the shared CallConnection + the client's manager. The
// client is dropped afterward — its calling surface detaches, the
// relay holds the Arcs.
let producer_leg = ProducerLeg {
call: producer_call,
manager: hub_client_to_spoke.manager().clone(),
};
drop(hub_client_to_spoke);
let relay = Arc::new(ChannelRelay::new(producer_leg));
let imported_spec = Arc::new(imported.spec);
// ---- the hub's consumer leg: the fork-registry install hook ----
// (ADR-051 §4 phase 2 — fork, generic ops, bootstrap discovery,
// the imported marked spec via the relay registration, dispatch.)
let consumer_install: crate::channels::adapter::InstallChannelZero =
Arc::new(move |consumer_manager, channel0_conn, auth| {
let relay = Arc::clone(&relay);
let imported_spec = Arc::clone(&imported_spec);
tokio::spawn(async move {
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
return;
};
let (writer, reader) = split_single_stream(channel0_bidi);
let registry = Arc::new(OperationRegistry::new());
crate::channels::operations::ChannelOperations::with_default_policy(
consumer_manager.clone(),
)
.register_on(&registry)
.expect("generic channel ops");
crate::registry::discovery::install_bootstrap_discovery(&registry)
.expect("bootstrap discovery");
let consumer_core = ChannelCore::new(consumer_manager.clone(), Arc::new(NoCap));
relay
.register_relay_openable(
&consumer_core,
&registry,
(*imported_spec).clone(),
auth.clone(),
)
.expect("relay openable registers on the fork");
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
Dispatcher::new(
registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
)
.run_loop_single_stream(call_connection, reader, writer)
.await;
})
});
let (consumer_end, hub_consumer_end) = tokio::io::duplex(64 * 1024);
let consumer_conn =
Connection::from_bidi(consumer_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let hub_consumer_side =
Connection::from_bidi(hub_consumer_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let consumer_adapter = ChannelsAdapter::new(consumer_install, Arc::new(NoCap));
let _hub_task = tokio::spawn(async move {
let auth = AuthContext::anonymous(b"alk/channels");
let _ = crate::core::types::ProtocolHandler::handle(
&consumer_adapter,
hub_consumer_side,
&auth,
)
.await;
});
// ---- the consumer opens through the hub ----
let consumer = crate::channels::client::ChannelClient::from_connection(consumer_conn)
.await
.expect("consumer's channel client");
let (consumer_id, reply, mut send, mut recv) = tokio::time::timeout(
std::time::Duration::from_secs(10),
consumer.open_channel_with_reply(OP, json!({ "target": "example.internal:443" }), ALPN),
)
.await
.expect("relay open timed out")
.expect("open through the relay");
// The consumer reply carries the hub-allocated id (the consumer
// leg's accept side allocates even ids), NOT the spoke's.
assert!(consumer_id > 0);
assert_eq!(reply["channel_id"], json!(consumer_id));
assert_eq!(
reply["bound"],
json!({ "host": "203.0.113.9", "port": 42113 }),
"the spoke establisher's `bound` survives the relay (per-hop truthful)"
);
// Data flows both directions; the hub never parses the data plane.
send.write_all(b"ping-payload").await.expect("write");
send.shutdown().await.expect("shutdown");
drop(send);
let seen = tokio::time::timeout(std::time::Duration::from_secs(5), data_rx.recv())
.await
.expect("timed out waiting for the spoke's data-plane bytes")
.expect("the spoke handler received the consumer's bytes through the relay");
assert_eq!(seen, "ping-payload");
let mut echoed = Vec::new();
tokio::time::timeout(
std::time::Duration::from_secs(5),
recv.read_to_end(&mut echoed),
)
.await
.expect("timed out waiting for the echo")
.expect("read echo");
assert_eq!(
echoed, b"PING-PAYLOAD",
"the spoke's echo flows back through the relay"
);
}
/// Gate: a spoke `channel:open_failed` maps to the consumer-leg
/// `channel:open_failed` with the mapped reason and the spoke's
/// message preserved; the consumer leg tears down (ledger
/// decremented) and no spoke channel leaks (the adopt-then-fail
/// window — the hub never adopts because the spoke open failed
/// before replying).
#[tokio::test]
async fn relay_spoke_open_failure_maps_reason_and_tears_down() {
use crate::channels::client::ChannelClient;
use crate::channels::operations::EstablishmentError;
use crate::channels::policy::PerIdentityChannelPolicy;
// The spoke serves the direct op with an establisher that always
// fails `dial_failed` (the target refused).
let spoke_establisher: OpenEstablisher = Arc::new(|_input, _auth| {
Box::pin(async {
Err(EstablishmentError::DialFailed {
message: "target refused the connection".to_string(),
})
})
});
let spoke_install: crate::channels::adapter::InstallChannelZero = {
let establisher = Arc::clone(&spoke_establisher);
Arc::new(move |manager, channel0_conn, auth| {
let establisher = Arc::clone(&establisher);
tokio::spawn(async move {
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
return;
};
let (writer, reader) = split_single_stream(channel0_bidi);
let policy = super::super::policy::default_policy();
let core = ChannelCore::new(manager.clone(), policy);
let registry = Arc::new(OperationRegistry::new());
crate::registry::discovery::install_bootstrap_discovery(&registry)
.expect("spoke bootstrap discovery");
let spec = relay_spec(
"channels/tunnel/direct",
OperationType::Sub,
Some("alk/tunnel".to_string()),
);
core.register_openable_with_establisher(
spec,
Some(establisher),
Arc::new(|_i, _p, _c, _a| tokio::spawn(async {})),
&registry,
auth.clone(),
None,
)
.expect("spoke registers the direct open op");
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
Dispatcher::new(
registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
)
.serve_single_stream(call_connection, reader, writer)
.await;
})
})
};
let (hub_spoke_client_end, hub_spoke_server_end) = tokio::io::duplex(64 * 1024);
let hub_conn_to_spoke = Connection::from_bidi(
hub_spoke_client_end,
b"alk/channels".to_vec(),
Some(TEST_ADDR),
);
let spoke_conn = Connection::from_bidi(
hub_spoke_server_end,
b"alk/channels".to_vec(),
Some(TEST_ADDR),
);
let spoke_adapter = ChannelsAdapter::new(spoke_install, Arc::new(NoCap));
let _spoke_task = tokio::spawn(async move {
let auth = AuthContext::anonymous(b"alk/channels");
let _ =
crate::core::types::ProtocolHandler::handle(&spoke_adapter, spoke_conn, &auth).await;
});
let hub_client_to_spoke = ChannelClient::from_connection(hub_conn_to_spoke)
.await
.expect("hub's channel client to the spoke");
let producer_call = hub_client_to_spoke
.take_call_connection()
.await
.expect("hub takes its channel-0 CallConnection");
let bundles = crate::client::from_call(&producer_call, crate::client::FromCallConfig::new())
.await
.expect("discovery");
let imported = bundles
.into_iter()
.find(|b| b.spec.name == OP)
.expect("direct op discovered");
let producer_leg = ProducerLeg {
call: producer_call,
manager: hub_client_to_spoke.manager().clone(),
};
let spoke_manager = producer_leg.manager.clone();
drop(hub_client_to_spoke);
let relay = Arc::new(ChannelRelay::new(producer_leg));
let imported_spec = Arc::new(imported.spec);
// The consumer leg runs a per-identity cap so the ledger
// decrement is observable.
let consumer_policy = Arc::new(PerIdentityChannelPolicy::new(8));
let consumer_policy_dyn: Arc<dyn crate::channels::policy::ChannelLifecyclePolicy> =
Arc::clone(&consumer_policy) as Arc<dyn crate::channels::policy::ChannelLifecyclePolicy>;
let consumer_install: crate::channels::adapter::InstallChannelZero =
Arc::new(move |consumer_manager, channel0_conn, auth| {
let relay = Arc::clone(&relay);
let imported_spec = Arc::clone(&imported_spec);
let policy = Arc::clone(&consumer_policy_dyn);
tokio::spawn(async move {
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
return;
};
let (writer, reader) = split_single_stream(channel0_bidi);
let registry = Arc::new(OperationRegistry::new());
crate::registry::discovery::install_bootstrap_discovery(&registry)
.expect("bootstrap discovery");
let consumer_core = ChannelCore::new(consumer_manager.clone(), policy);
relay
.register_relay_openable(
&consumer_core,
&registry,
(*imported_spec).clone(),
auth.clone(),
)
.expect("relay openable");
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
Dispatcher::new(
registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
)
.run_loop_single_stream(call_connection, reader, writer)
.await;
})
});
let (consumer_end, hub_consumer_end) = tokio::io::duplex(64 * 1024);
let consumer_conn =
Connection::from_bidi(consumer_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let hub_consumer_side =
Connection::from_bidi(hub_consumer_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let consumer_adapter = ChannelsAdapter::new(consumer_install, Arc::new(NoCap));
let _hub_task = tokio::spawn(async move {
let auth = AuthContext::anonymous(b"alk/channels");
let _ = crate::core::types::ProtocolHandler::handle(
&consumer_adapter,
hub_consumer_side,
&auth,
)
.await;
});
let consumer = ChannelClient::from_connection(consumer_conn)
.await
.expect("consumer's channel client");
let err = tokio::time::timeout(
std::time::Duration::from_secs(10),
consumer.open_channel(OP, json!({ "target": "refused.internal:1" }), ALPN),
)
.await
.expect("relay open timed out");
let call_error = match err {
Err(e) => e
.call_error()
.expect("CallFailed carries the CallError")
.clone(),
Ok(_) => panic!("the spoke failure must surface as an Err through the relay"),
};
assert_eq!(call_error.code, "channel:open_failed");
let details = call_error.details.expect("details");
assert_eq!(details["reason"], "dial_failed", "the mapped reason");
assert!(
details["message"]
.as_str()
.expect("message")
.contains("target refused the connection"),
"the spoke's message is preserved: {}",
details["message"]
);
// No spoke channel leaks: the hub never adopted (the spoke open
// failed before replying a channel_id), the consumer leg tore its
// just-allocated channel down.
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
assert!(
consumer
.manager()
.channel_ids()
.into_iter()
.all(|id| id == 0),
"no consumer-leg data channel exists after the failed relay open"
);
assert!(
spoke_manager.channel_ids().into_iter().all(|id| id == 0),
"no spoke channel leaked (the adopt-then-fail window)"
);
let anonymous = identity("anonymous", &[]);
assert_eq!(
consumer_policy.count_for(&anonymous),
0,
"the consumer leg's ledger decremented (teardown on establisher failure)"
);
}
/// Gate (ADR-051 gate 2's companion): a standard-shape
/// `channels/tty/sub` spec relays identically — no regression on the
/// existing derivation's shape.
#[tokio::test]
async fn relay_round_trip_pins_standard_shape_no_regression() {
use crate::channels::operations::Establishment;
use tokio::io::AsyncWriteExt;
let (data_tx, mut data_rx) = tokio::sync::mpsc::channel::<String>(4);
let spoke_open_handler = spoke_open_handler(data_tx);
let spoke_establisher: OpenEstablisher =
Arc::new(|_input, _auth| Box::pin(async { Ok(Establishment::default()) }));
let spoke_install: crate::channels::adapter::InstallChannelZero = {
let establisher = Arc::clone(&spoke_establisher);
Arc::new(move |manager, channel0_conn, auth| {
let establisher = Arc::clone(&establisher);
let open_handler = spoke_open_handler.clone();
tokio::spawn(async move {
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
return;
};
let (writer, reader) = split_single_stream(channel0_bidi);
let core = ChannelCore::new(manager, super::super::policy::default_policy());
let registry = Arc::new(OperationRegistry::new());
crate::registry::discovery::install_bootstrap_discovery(&registry)
.expect("spoke bootstrap discovery");
let spec = relay_spec(
"channels/tty/sub",
OperationType::Sub,
Some("alk/tty".to_string()),
);
core.register_openable_with_establisher(
spec,
Some(establisher),
open_handler,
&registry,
auth.clone(),
None,
)
.expect("spoke registers the tty open op");
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
Dispatcher::new(
registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
)
.serve_single_stream(call_connection, reader, writer)
.await;
})
})
};
let (hub_spoke_client_end, hub_spoke_server_end) = tokio::io::duplex(64 * 1024);
let hub_conn_to_spoke = Connection::from_bidi(
hub_spoke_client_end,
b"alk/channels".to_vec(),
Some(TEST_ADDR),
);
let spoke_conn = Connection::from_bidi(
hub_spoke_server_end,
b"alk/channels".to_vec(),
Some(TEST_ADDR),
);
let spoke_adapter = ChannelsAdapter::new(spoke_install, Arc::new(NoCap));
let _spoke_task = tokio::spawn(async move {
let auth = AuthContext::anonymous(b"alk/channels");
let _ =
crate::core::types::ProtocolHandler::handle(&spoke_adapter, spoke_conn, &auth).await;
});
let hub_client_to_spoke =
crate::channels::client::ChannelClient::from_connection(hub_conn_to_spoke)
.await
.expect("hub's channel client to the spoke");
let producer_call = hub_client_to_spoke
.take_call_connection()
.await
.expect("hub takes its channel-0 CallConnection");
let bundles = crate::client::from_call(&producer_call, crate::client::FromCallConfig::new())
.await
.expect("discovery");
let imported = bundles
.into_iter()
.find(|b| b.spec.name == "channels/tty/sub")
.expect("the tty op is discovered");
let producer_leg = ProducerLeg {
call: producer_call,
manager: hub_client_to_spoke.manager().clone(),
};
drop(hub_client_to_spoke);
let relay = Arc::new(ChannelRelay::new(producer_leg));
let imported_spec = Arc::new(imported.spec);
let consumer_install: crate::channels::adapter::InstallChannelZero =
Arc::new(move |consumer_manager, channel0_conn, auth| {
let relay = Arc::clone(&relay);
let imported_spec = Arc::clone(&imported_spec);
tokio::spawn(async move {
let Ok(channel0_bidi) = channel0_conn.accept_bi().await else {
return;
};
let (writer, reader) = split_single_stream(channel0_bidi);
let registry = Arc::new(OperationRegistry::new());
crate::registry::discovery::install_bootstrap_discovery(&registry)
.expect("bootstrap discovery");
let consumer_core = ChannelCore::new(consumer_manager.clone(), Arc::new(NoCap));
relay
.register_relay_openable(
&consumer_core,
&registry,
(*imported_spec).clone(),
auth.clone(),
)
.expect("relay openable");
let call_connection = Arc::new(CallConnection::new_single_stream(
channel0_conn,
Arc::clone(&writer),
));
Dispatcher::new(
registry,
Arc::new(NoopIdProvider) as Arc<dyn IdentityProvider>,
)
.run_loop_single_stream(call_connection, reader, writer)
.await;
})
});
let (consumer_end, hub_consumer_end) = tokio::io::duplex(64 * 1024);
let consumer_conn =
Connection::from_bidi(consumer_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let hub_consumer_side =
Connection::from_bidi(hub_consumer_end, b"alk/channels".to_vec(), Some(TEST_ADDR));
let consumer_adapter = ChannelsAdapter::new(consumer_install, Arc::new(NoCap));
let _hub_task = tokio::spawn(async move {
let auth = AuthContext::anonymous(b"alk/channels");
let _ = crate::core::types::ProtocolHandler::handle(
&consumer_adapter,
hub_consumer_side,
&auth,
)
.await;
});
let consumer = crate::channels::client::ChannelClient::from_connection(consumer_conn)
.await
.expect("consumer's channel client");
let (consumer_id, reply, mut send, mut recv) = tokio::time::timeout(
std::time::Duration::from_secs(10),
consumer.open_channel_with_reply("channels/tty/sub", json!({}), "alk/tty"),
)
.await
.expect("relay open timed out")
.expect("standard-shape open through the relay");
assert!(consumer_id > 0);
assert_eq!(
reply.as_object().expect("object").len(),
1,
"no establisher reply fields: the reply is exactly channel_id"
);
send.write_all(b"tty-bytes").await.expect("write");
send.shutdown().await.expect("shutdown");
drop(send);
let seen = tokio::time::timeout(std::time::Duration::from_secs(5), data_rx.recv())
.await
.expect("timed out")
.expect("the spoke handler received the bytes");
assert_eq!(seen, "tty-bytes");
let mut echoed = Vec::new();
tokio::time::timeout(
std::time::Duration::from_secs(5),
recv.read_to_end(&mut echoed),
)
.await
.expect("timed out")
.expect("read echo");
assert_eq!(echoed, b"TTY-BYTES");
}