feat(channels): implement the single-publish fixes — S-01..S-04 (reviews 010/011; ADR-047 am. 4, ADR-052, ADR-053, ADR-022 am.)

S-01 — the marked-pub open contract (ADR-047 amendment 4):
- ChannelCore::register_pub_openable + PubOpenHandler (the put path's
  handler returns its result for the merged single response; the
  wrapper awaits inline, teardown is EOF-driven, handler_task stays
  None); register_openable* refuses Pub-typed specs; the wrapper entry
  points refuse unmarked/non-Pub shapes
- the reserved input key `channel_id` (the initiator-allocated id):
  stripped pre-validation by resolve_sink_handler (the single source of
  truth, P-08), handed to the wrapper via OperationContext.metadata, a
  missing key rejected INVALID_INPUT; adopt parity enforced
  (ManagerError::InvalidAdopt) and adopt failures answer
  channel:open_failed / adopt_failed
- zero call.published on marked ops: the first publish chunk on a
  marked op is rejected loudly (channel:publish_chunks_rejected) in
  both dispatch loops; the handler is never fed
- relay put-translation replaces RelayError::PubTypedOpen: hub
  allocates odd on the producer leg, opens the spoke op in the
  background (put-acks-in-result), one-direction pump, response-mirror
  with the producer's reserved key stripped
- ChannelClient::open_pub_channel + PubOpenReply (the initiator-
  allocates put open capability)

S-02 — channel/control implemented (ADR-052): establisher-contributed
ChannelControl (Establishment::with_control), manager-owned registry,
teardown removals, channel:no_control_handle capability answer.

S-03 — channel/resources/subscribe implemented (ADR-053): the
ResourceEnumerators registry (shared per peer), snapshot-then-changes
enumerators, scan-once aggregation, the empty seed; the
always-failing one-shot error stream removed.

S-04 — op/register forward-through (ADR-022 amendment): announced
unmarked Sub/Pub register the real streaming/sink forwarding shapes;
marked announcements refused with MARKED_SPEC_UNSUPPORTED; marked
specs refuse the generic registration path and register_imported
(Result-returning now).

Guard (review 010 action 3): tests/stub_guard.rs greps src/ for
not_implemented; the four pinned stub tests are deleted and the
marked-spec registration refusals are pinned by tests.

Verified: cargo test (696 + guard), clippy --all-targets -D warnings,
fmt --check, fuzz corpus replay.
This commit is contained in:
glm-5.3-flash committed 2026-10-13 09:00:21 +00:00
1 parent df4d4d3a9e
commit 944c300c17
18 files changed
+2418 -326

No files matched your search

+214 -19
View File
@@ -35,11 +35,13 @@ use crate::registry::spec::{OperationSpec, OperationType};
use super::manager::ChannelManager;
use super::operations::{
ChannelCore, Establishment, EstablishmentError, OpenEstablisher, OpenHandler,
CHANNEL_OPEN_FAILED, RESERVED_REPLY_KEY,
ChannelCore, ChannelPlan, Establishment, EstablishmentError, OpenEstablisher, OpenHandler,
PubOpenHandler, CHANNEL_OPEN_FAILED, RESERVED_REPLY_KEY,
};
use super::pump::pump_bidi;
use super::reassembly::{MpscRecvStream, MpscSendStream};
use tokio::io::AsyncWriteExt;
/// The producer-leg surface the relay holds (ADR-051 §2): the
/// `CallConnection` for the translate hop's open call plus the
@@ -81,8 +83,17 @@ struct RelayPlan {
producer_manager: ChannelManager,
spoke_send: Option<MpscSendStream>,
spoke_recv: Option<MpscRecvStream>,
/// The put path (ADR-047 amendment 4 §4.7): the producer leg's
/// open-op response, awaited by the relay pub handler after the
/// consumer EOF (put-acks-in-result — the spoke's response cannot
/// exist before its byte stream completes, so the establisher
/// never awaits it).
pub_reply: Option<tokio::sync::oneshot::Receiver<CallConnectionResponse>>,
}
/// The producer leg's open-op response, carried on the put path's plan.
type CallConnectionResponse = crate::protocol::wire::ResponseEnvelope;
impl Drop for RelayPlan {
fn drop(&mut self) {
let _ = self.producer_manager.teardown_channel(self.spoke_id);
@@ -120,9 +131,10 @@ impl ChannelRelay {
/// 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
/// `Pub`-typed marked spec composes through the put-forwarding
/// translation (see [`Self::register_relay_pub_openable`]); an
/// unmarked spec is a
/// [`RelayError::UnmarkedSpec`] 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
@@ -145,14 +157,12 @@ impl ChannelRelay {
});
}
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
),
});
return self.register_relay_pub_openable(
consumer_core,
consumer_registry,
spec,
consumer_auth,
);
}
let producer_call = Arc::clone(&self.producer_leg.call);
@@ -227,6 +237,76 @@ impl ChannelRelay {
)
.map_err(|e| RelayError::Registration { message: e })
}
/// Register a from_call-imported **marked `Pub`** spec as a relay
/// openable — the put-forwarding translation (ADR-047 amendment 4
/// §4.7). The consumer leg runs the full marked-pub wrapper (adopt
/// → establish → pub handler awaited inline); this side's compose:
///
/// - the establisher allocates the hub's connect-side id on the
/// producer leg (`open_channel` — an odd id, disjoint from the
/// spoke's even allocations), sends the open op **in the
/// background** (the spoke's put response cannot exist before
/// its byte stream completes; an establisher that awaited it
/// deadlocks the leg), and plans the spoke-leg streams + the
/// response's oneshot; the plan's Drop guard reclaims the
/// producer-leg entry on every handler path, as ADR-051 §6 pins.
/// - the handler pumps the consumer-leg bytes → the spoke leg
/// (one direction, pub), EOFs the spoke side on consumer EOF,
/// then awaits the spoke's single response, strips the
/// producer's reserved reply key (the consumer reply carries the
/// hub-allocated consumer-leg id; a spoke `channel_id` merged
/// through would shadow it — the same reserved-key class the
/// wrapper refuses), and returns the remainder — the consumer-leg
/// wrapper merges it with the pinned order (ack fields are none
/// here; the put acks in the spoke's result, §4.5).
///
/// A spoke open-time establishment failure surfaces verbatim in
/// the final response (put-acks-in-result; the spoke's mapped
/// `channel:open_failed` rides the response, not the establisher).
fn register_relay_pub_openable(
&self,
consumer_core: &ChannelCore,
consumer_registry: &OperationRegistry,
spec: OperationSpec,
consumer_auth: AuthContext,
) -> Result<(), RelayError> {
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_pub_on_producer_leg(
&producer_call,
&producer_manager,
&op_name,
input,
&per_call_auth,
)
.await
})
});
let pub_open_handler: PubOpenHandler =
Arc::new(move |_input, plan, channel_conn, _auth| {
Box::pin(async move { pub_relay_forward(channel_conn, plan).await })
});
consumer_core
.register_pub_openable(
spec,
Some(establisher),
pub_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
@@ -288,6 +368,7 @@ async fn open_on_producer_leg(
producer_manager: producer_manager.clone(),
spoke_send: Some(spoke_send),
spoke_recv: Some(spoke_recv),
pub_reply: None,
}) as super::operations::ChannelPlan);
Ok(if reply_fields.is_empty() {
establishment
@@ -334,6 +415,123 @@ fn map_spoke_error(spoke_error: &CallError) -> EstablishmentError {
}
}
/// The put path's translate hop (ADR-047 amendment 4 §4.7): allocate
/// the hub's connect-side channel id on the producer leg, send the
/// spoke's open op **in the background** with the reserved input key
/// set, and plan the spoke-leg streams + the response oneshot. The
/// establisher returns promptly; the spoke's response arrives when the
/// spoke's put completes (or fails) — put-acks-in-result.
async fn open_pub_on_producer_leg(
producer_call: &Arc<CallConnection>,
producer_manager: &ChannelManager,
op_name: &str,
input: Value,
per_call_auth: &AuthContext,
) -> Result<Establishment, EstablishmentError> {
let (spoke_id, spoke_send, spoke_recv) = producer_manager
.open_channel(
"alk/relay",
// The opener ledger is inert on the hub's connect side
// (the connect side does not enforce the per-identity cap,
// ADR-041) — the record is observability only.
"hub-relay",
None,
)
.await
.map_err(|e| EstablishmentError::ResourceShortage {
message: format!("producer leg: channel allocation failed: {e}"),
})?;
let mut hop_input = input.clone();
if let Some(obj) = hop_input.as_object_mut() {
obj.insert(
crate::registry::spec::RESERVED_OPEN_INPUT_KEY.to_string(),
serde_json::json!(spoke_id),
);
}
let payload = build_relay_payload(op_name, hop_input, per_call_auth);
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
let producer_call_task = Arc::clone(producer_call);
tokio::spawn(async move {
let _ = reply_tx.send(producer_call_task.call_with_payload(payload).await);
});
Ok(Establishment::new(Arc::new(RelayPlan {
spoke_id,
producer_manager: producer_manager.clone(),
spoke_send: Some(spoke_send),
spoke_recv: Some(spoke_recv),
pub_reply: Some(reply_rx),
}) as ChannelPlan))
}
/// The relay pub handler (ADR-047 amendment 4 §4.7): pump the
/// consumer-leg bytes → the spoke leg (one direction, pub — the
/// shutdown-on-completion contract, ADR-050's), then await the spoke's
/// single response and strip its reserved reply key. The plan's Drop
/// guard reclaims the producer-leg entry on every path.
async fn pub_relay_forward(
channel_conn: crate::core::types::Connection,
plan: Option<ChannelPlan>,
) -> Result<Value, CallError> {
let Some(plan) = plan else {
return Err(CallError::internal(
"relay: pub establisher produced no plan",
));
};
let Ok(relay_plan_arc) = plan.downcast::<RelayPlan>() else {
return Err(CallError::internal(
"relay: establisher plan of unexpected type",
));
};
let Ok(mut relay_plan) = Arc::try_unwrap(relay_plan_arc) else {
return Err(CallError::internal(
"relay: establisher plan held elsewhere; skipping pump",
));
};
let Some(mut spoke_send) = relay_plan.spoke_send.take() else {
return Err(CallError::internal(
"relay: pub plan missing the spoke send half",
));
};
relay_plan.spoke_recv.take();
let Ok(bidi) = channel_conn.accept_bi().await else {
return Err(CallError::internal(
"relay: consumer leg data plane never opened",
));
};
let (mut read_half, _write_half) = tokio::io::split(bidi);
// The one-direction pump: consumer bytes → spoke; shutdown on
// consumer EOF (the pump contract, ADR-050).
let _copied = tokio::io::copy(&mut read_half, &mut spoke_send)
.await
.unwrap_or(0);
let _ = spoke_send.shutdown().await;
drop(spoke_send);
match relay_plan.pub_reply.take() {
Some(rx) => match rx.await {
Ok(response) => match response.result {
Ok(mut out) => {
if let Some(obj) = out.as_object_mut() {
obj.remove(RESERVED_REPLY_KEY);
}
Ok(out)
}
Err(e) => Err(e),
},
Err(_) => Err(CallError::internal(
"relay: producer-leg open resolved without a response",
)),
},
None => Err(CallError::internal(
"relay: pub plan missing the response receiver",
)),
}
}
/// 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
@@ -357,18 +555,15 @@ fn build_relay_payload(op_name: &str, input: Value, per_call_auth: &AuthContext)
/// The assembly-time errors `ChannelRelay::register_relay_openable`
/// produces (ADR-051 §6 — the rejection postures are loud, never
/// silent stubs).
/// silent stubs; the `PubTypedOpen` variant was removed by ADR-047
/// amendment 4 §4.7 — pub-typed marked specs translate now, never
/// reject-and-strand).
#[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}")]