feat: producer-listen — listen establisher (shape 2) + AcceptQueue contract

- AcceptFn: the injected accepted-connection source (the assembly
  layer owns the listener + accept loop; the protocol never binds —
  OQ-TN-04).
- AcceptQueue: the protocol-side queue contract (async push/pop/
  close; FIFO always-before-take ordering; close-while-waiting
  resolves None). Empty-queue posture is assembly-owned (a late
  accept is legitimate; resource_shortage is the closure's mapping;
  the wrapper's 10s establishment timeout is the backstop).
- listen_establisher: same open op, same params, same typed errors —
  registry namespace gate → accept() → Establishment::new(plan);
  the pump handler is untouched (plan-flow with a different source).
- register_tunnel_listen_openable: same spec/pump registration with
  the listen establisher (the honest shape vs a dial/accept enum:
  the establisher is the only difference; one establisher per op id
  per session registry — documented).
- Tests (tests/producer_listen.rs, 6): listen flow end-to-end, FIFO
  ordering across two opens (R-01 plan-flow, listen-flavored), empty
  queue -> resource_shortage, closed listener -> dial_failed,
  unknown resource -> unknown_resource, late-push wait-then-resolve.
- Harness: RegistrationMode enum + wire_listen.
- TargetHandle gains a structural Debug impl (test ergonomics).

Verified: cargo test green (44), clippy -D warnings (native + wasm32),
fmt clean, wasm32 check passes.
This commit is contained in:
2026-09-08 09:37:22 +00:00
parent fb2389bd62
commit 09d32d5aa6
6 changed files with 589 additions and 15 deletions
+49 -11
View File
@@ -92,9 +92,16 @@ pub struct Topology {
/// `serving_identity` is the explicit `ServingConfig.identity`
/// override (CF-005 remediation (a)) — `None` in the primary path.
/// `provider` resolves payload tokens (the hub-forwarding path).
/// The open-op registration mode: a dial establisher (shape 1) or a
/// listen establisher over an assembly-fed [`AcceptQueue`] (shape 2).
pub enum RegistrationMode {
Dial(alktunnels::producer::DialFn),
Listen(alktunnels::producer::AcceptFn),
}
pub async fn wire_with(
registry: ResourceRegistry,
dial: alktunnels::producer::DialFn,
mode: RegistrationMode,
transport_identity: Option<Identity>,
serving_identity: Option<Identity>,
provider: Arc<dyn IdentityProvider>,
@@ -185,15 +192,29 @@ pub async fn wire_with(
producer_client.manager().clone(),
alkcall::channels::policy::default_policy(),
);
register_tunnel_openable(
&core,
&registry,
&producer_op_registry,
AuthContext::anonymous(b"alk/tunnel"),
dial,
Some(Arc::clone(&identity_witness)),
)
.expect("register tunnel openable");
match mode {
RegistrationMode::Dial(dial) => {
register_tunnel_openable(
&core,
&registry,
&producer_op_registry,
AuthContext::anonymous(b"alk/tunnel"),
dial,
Some(Arc::clone(&identity_witness)),
)
.expect("register tunnel openable");
}
RegistrationMode::Listen(accept) => {
alktunnels::producer::register_tunnel_listen_openable(
&core,
&registry,
&producer_op_registry,
AuthContext::anonymous(b"alk/tunnel"),
accept,
)
.expect("register tunnel listen openable");
}
}
let producer_client = Arc::new(producer_client);
// --- consumer pieces (captured from the hook) -------------------------
@@ -215,7 +236,24 @@ pub async fn wire_with(
pub async fn wire(registry: ResourceRegistry, dial: alktunnels::producer::DialFn) -> Topology {
wire_with(
registry,
dial,
RegistrationMode::Dial(dial),
Some(consumer_identity()),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),
)
.await
}
/// The listen topology (shape 2): the producer's establisher pops
/// accepted handles from the assembly-fed [`AcceptQueue`] instead of
/// dialing. Same identity posture as [`wire`].
pub async fn wire_listen(
registry: ResourceRegistry,
accept: alktunnels::producer::AcceptFn,
) -> Topology {
wire_with(
registry,
RegistrationMode::Listen(accept),
Some(consumer_identity()),
None,
Arc::new(alkcall::core::auth::NoopIdentityProvider),