diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f12b2eac8..3673d2605e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -405,6 +405,11 @@ Each of these is an owner-accepted difference from 1.5.5: additive, or strictly series by their labels; a histogram's or summary's lines stay together in their usual order (buckets ascending, then `_sum`, then `_count`). Every name, label and value is unchanged, and the order served is one 1.5.5 could already print. +- **The streaming plane serves the protected-resource metadata its challenge points at.** A caller + refused at a realtime door is pointed at + `/.well-known/oauth-protected-resource/v1/realtime`, and nothing served that path. It + now answers the RFC 9728 document: the plane's audience as `resource` and + `bearer_methods_supported: ["header"]`, the same document the MCP route serves. - **A wedged hook comes back.** A hook that wedged is no longer lost until restart: it is quarantined, backed off from 1 s doubling to 30 s, and then given one trial call on a fresh instance, and a successful trial returns it to service. 1.5.5 had no quarantine to exit. diff --git a/Cargo.lock b/Cargo.lock index fe900ce5d4..8286928acc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,7 +363,6 @@ dependencies = [ "busbar-transport-tcp-plugin", "busbar-transport-webrtc", "busbar-transport-ws", - "busbar-voice", "bytes", "core_affinity", "criterion", @@ -920,11 +919,12 @@ dependencies = [ name = "busbar-plane-streaming" version = "1.6.0" dependencies = [ - "async-trait", "busbar-contract", + "busbar-plugin-loader", "bytes", "serde", "serde_json", + "url", ] [[package]] @@ -1073,29 +1073,6 @@ dependencies = [ "tokio-tungstenite", ] -[[package]] -name = "busbar-voice" -version = "1.6.0" -dependencies = [ - "async-trait", - "axum", - "busbar-contract", - "busbar-kernel", - "busbar-plane-streaming", - "bytes", - "futures", - "http", - "http-body-util", - "metrics", - "serde", - "serde_json", - "serde_yaml_ng", - "tokio", - "tokio-tungstenite", - "tracing", - "url", -] - [[package]] name = "bytecount" version = "0.6.9" @@ -4362,6 +4339,17 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "voice-conformance-harness" +version = "0.0.0" +dependencies = [ + "busbar-contract", + "busbar-plane-streaming", + "busbar-plugin-loader", + "bytes", + "serde_json", +] + [[package]] name = "vsimd" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 289dea8d45..92e4f3fd71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "crates/busbar-core-oauth2", "crates/busbar-llm", "crates/busbar-a2a", - "crates/busbar-voice", "crates/busbar-contract", "crates/busbar-kernel", "crates/busbar-kernel-identity", @@ -26,6 +25,7 @@ members = [ "crates/busbar-transport-stdio", "crates/busbar-transport-webrtc", "testing/ws-conformance/subject", + "testing/voice-conformance/harness", "xtask", ] diff --git a/crates/busbar-kernel/Cargo.toml b/crates/busbar-kernel/Cargo.toml index 7686a771b0..044585928d 100644 --- a/crates/busbar-kernel/Cargo.toml +++ b/crates/busbar-kernel/Cargo.toml @@ -190,13 +190,12 @@ plane-mcp = ["egress-seam", "jsonrpc-ingress", "dispatch"] # other relocated `#[cfg(feature = "plane-a2a")]` sites now live in substrate, so this must turn the # substrate marker on with it. plane-a2a = ["egress-seam", "egress-stream", "card-signing", "jsonrpc-ingress", "relay"] -# THE NEUTRAL INBOUND WS-ACCEPT CAPABILITY — arms the core-side `mount_ws_arrivals` seam (router.rs) -# that mounts a plane's gauntlet-gated inbound-WebSocket arrivals. NAMES NO PLANE: any duplex plane -# (streaming today, a future telephony/stream plane tomorrow) turns on the same capability. Forwards to -# substrate's neutral `runtime` (which arms `axum/ws` + the `duplex_ws` acceptor and the `WsArrival` -# newtype/registry). OFF by default and pulled ONLY by the binary's `plane-streaming`, so the default/shipped -# money-path build carries no `axum/ws` edge (no tokio-tungstenite/sha1/base64) and this seam is -# compiled out entirely — byte-identical assembly, strong-form deletable. +# THE NEUTRAL INBOUND WS CAPABILITY — arms the core-side duplex session mount (router.rs, +# `mount_plane_session`) that answers a door plane's session upgrade. NAMES NO PLANE: any duplex +# plane turns on the same capability. Forwards to the neutral `runtime` (which arms `axum/ws` and the +# `duplex_ws` acceptor). OFF by default and pulled ONLY by the binary's `plane-streaming`, so the +# default/shipped money-path build carries no `axum/ws` edge (no tokio-tungstenite/sha1/base64) and +# this capability is compiled out entirely — byte-identical assembly, strong-form deletable. duplex-ws = ["runtime"] # The neutral capability markers. `dispatch`/`relay` gate the `#[cfg]`/dead-code-allowance sites in # this crate's engine source; `runtime` arms the full-duplex WS transport (client handshake + diff --git a/crates/busbar-kernel/src/config/config-schema.snapshot.json b/crates/busbar-kernel/src/config/config-schema.snapshot.json index 9330e88eb4..e84d5e35a8 100644 --- a/crates/busbar-kernel/src/config/config-schema.snapshot.json +++ b/crates/busbar-kernel/src/config/config-schema.snapshot.json @@ -433,6 +433,10 @@ "optional": true, "type": "ConfigMgmtCfg" }, + "decisions": { + "optional": true, + "type": "DeclaredSections" + }, "export": { "optional": true, "type": "ExportDefs" diff --git a/crates/busbar-kernel/src/egress/tests/duplex_ws_tests.rs b/crates/busbar-kernel/src/egress/tests/duplex_ws_tests.rs index bcb3b901d9..e5d0c5ff9b 100644 --- a/crates/busbar-kernel/src/egress/tests/duplex_ws_tests.rs +++ b/crates/busbar-kernel/src/egress/tests/duplex_ws_tests.rs @@ -578,7 +578,7 @@ async fn dial_refuses_unpinned_and_guard_failing_targets() { ); } -// ── THE INBOUND WS-ACCEPT ARRIVAL SEAM (WsArrival newtype + accept_gauntlet + registry) ────────── +// ── THE GAUNTLET-GATED WS ACCEPT (accept_gauntlet) ─────────────────────────────────────────────── use std::sync::atomic::{AtomicUsize, Ordering}; @@ -664,46 +664,6 @@ async fn accept_gauntlet_refuse_returns_refusal_and_spawns_zero_socket_tasks() { ); } -/// THE WS-ARRIVAL SPEC + PROCESS REGISTRY round-trip: a plane declares a `WsArrivalSpec` (the neutral, -/// single-compiled seam carrying the substrate-owned `WsArrival` newtype BY VALUE — never `Box`), the composition root installs it, and the core router drains it VERBATIM. Witnesses the R1 -/// seam shape (spec is constructible with a by-value accept fn) and the install/take registry. -#[test] -fn ws_arrival_spec_installs_and_drains_verbatim() { - use crate::ingress::duplex_ws::{ - install_ws_arrivals, take_ws_arrivals, WsArrival, WsArrivalSpec, - }; - use busbar_contract::abi::mechanism::route::RouteAuth; - - let spec = WsArrivalSpec { - path: "/v1/duplex/{id}".to_string(), - auth: RouteAuth::Key, - slot_key: "test-duplex-plane", - // The accept fn takes the newtype BY VALUE — the single-compiled `WsArrival`, never a box — and - // is ASYNC (returns a `WsAcceptFuture`) so a plane runs its pre-upgrade hooks before the upgrade. - accept: std::sync::Arc::new(|_a: WsArrival| { - Box::pin(async move { - axum::response::Response::builder() - .status(axum::http::StatusCode::NOT_IMPLEMENTED) - .body(axum::body::Body::empty()) - .expect("response builds") - }) as crate::ingress::duplex_ws::WsAcceptFuture - }), - }; - install_ws_arrivals(vec![spec]); - let drained = take_ws_arrivals(); - assert_eq!(drained.len(), 1, "the installed arrival drains verbatim"); - assert_eq!(drained[0].path, "/v1/duplex/{id}"); - assert_eq!(drained[0].slot_key, "test-duplex-plane"); - assert!(matches!(drained[0].auth, RouteAuth::Key)); - // Read-many: a second drain still yields the same installed set (the router may build twice). - assert_eq!( - take_ws_arrivals().len(), - 1, - "take is read-many, not destructive" - ); -} - // ── THE DIAL CREDENTIAL: on the upgrade request only, in no error ───────────────────────────────── use crate::egress::duplex_ws::{CredentialPlacement, DialCredential, DialTarget}; diff --git a/crates/busbar-kernel/src/ingress/duplex_ws.rs b/crates/busbar-kernel/src/ingress/duplex_ws.rs index 47d6d1b1a2..4755e96e71 100644 --- a/crates/busbar-kernel/src/ingress/duplex_ws.rs +++ b/crates/busbar-kernel/src/ingress/duplex_ws.rs @@ -255,104 +255,3 @@ where serve_messages(stream, sink, plane).await; }) } - -// ───────────────────────────────────────────────────────────────────────────────────────────── -// THE INBOUND WS-ACCEPT ARRIVAL SEAM — the neutral, substrate-owned vocabulary a plane DECLARES an -// inbound gauntlet-gated WS-accept route through, and the process registry the composition root -// installs those declarations into for the core router to drain. The plane names only this neutral -// seam; the CORE-side `mount_ws_arrivals` (behind `duplex-ws`) mounts a real WS-accept route per -// spec and hands the plane's accept fn a [`WsArrival`] by value. NONE of this is a `PlaneReqCtx` / -// `PlaneRouteSpec` field, so the always-compiled request path names no WS type (money-path byte- -// identity), and the upgrade is a substrate-owned newtype carried BY VALUE (never `Box`), so -// there is no dual-compile `TypeId` trap. -// ───────────────────────────────────────────────────────────────────────────────────────────── - -/// THE SUBSTRATE-OWNED NEWTYPE the WS upgrade rides on across the accept boundary — NEVER -/// `Box`. Single-compiled in substrate, so its type identity is the same in both dual- -/// compiled core instances (no downcast, no `TypeId` to diverge). Carries the upgrade BY VALUE plus -/// the verbatim per-request facts the plane's accept fn needs to build its `GauntletRequest` and open -/// its session — sourced from the SAME extractors the non-WS adapter reads, but assembled by the WS- -/// aware core mount. The only type-erased field is `slot`, the already-proven-safe plane-slot crossing -/// the non-WS `PlaneReqCtx::slot` already uses. -pub struct WsArrival { - /// The axum WS-upgrade extractor the route received. The ONLY way to consume it into a live socket - /// is [`serve_gauntlet`] / [`accept_gauntlet`]; a plane's accept fn never calls a bare `on_upgrade`. - pub upgrade: WebSocketUpgrade, - /// The middleware-resolved governance request context (`None` on a `RouteAuth::None` route). - pub gov: Option, - /// The middleware-resolved auth principal (`None` on a `RouteAuth::None` route). - pub principal: Option, - /// The resolved caller principal id, lifted from `gov` — the identity a plane binds session state to. - pub caller_principal: Option, - /// The request path this route was matched at. - pub path: String, - /// The full request URI (path + query). - pub uri: axum::http::Uri, - /// The request headers. - pub headers: axum::http::HeaderMap, - /// Any path-template captures (`{name}` → value), in match order. - pub path_params: Vec<(String, String)>, - /// The neutral engine host, minted core-side over the request's live engine snapshot. - pub host: Arc, - /// The plane's own per-generation runtime slot (the same `Arc` the plane's `build` produced), - /// captured by the core mount from the router's `plane_slots` and cloned into every arrival. - pub slot: Arc, -} - -/// The finished-`Response` FUTURE a [`WsAcceptFn`] returns — ASYNC (the mirror of the data-plane -/// `PlaneRouteFuture`), so a plane's accept fn can run its `async` operator hooks (gate/tap) BEFORE the -/// upgrade, exactly as the one-shot request path does, and still return the 101 (or a pre-upgrade -/// refusal) as its resolved value. -pub type WsAcceptFuture = std::pin::Pin + Send>>; - -/// ONE PLANE'S WS-ACCEPT HANDLER: a neutral fn that receives a [`WsArrival`] BY VALUE and returns a -/// finished axum [`Response`] (as a [`WsAcceptFuture`]). It MUST reach [`serve_gauntlet`] / -/// [`accept_gauntlet`] internally (the only path that consumes the upgrade into a live socket); it never -/// sees a bare `on_upgrade`. `Arc` so the core mount clones it into the per-request axum closure it -/// wires. ASYNC so the plane runs its pre-upgrade admission (operator hooks + the destination gauntlet) -/// before any socket binds — a hook/gauntlet refusal resolves to a pre-upgrade refusal Response. -pub type WsAcceptFn = Arc WsAcceptFuture + Send + Sync>; - -/// ONE WS-ACCEPT ARRIVAL a plane DECLARES: the exact path, the admission bar recorded VERBATIM in the -/// `CoreRouteTable` (identical shape to `PlaneRouteSpec::auth`, so the auth middleware enforces it -/// BEFORE the accept fn), the neutral accept fn, and the plane's registry `slot_key` so the core mount -/// resolves the live per-generation slot to carry on each [`WsArrival`] (exactly as the non-WS adapter -/// resolves it from `plane_slots`). Names only `axum`, `busbar_contract` and this crate — -/// no plane token, so adding a duplex plane is a new-crate-only diff. -#[derive(Clone)] -pub struct WsArrivalSpec { - /// The exact axum path pattern this WS-accept route is mounted at. - pub path: String, - /// The admission bar the core auth middleware enforces BEFORE the accept fn runs. - pub auth: busbar_contract::abi::mechanism::route::RouteAuth, - /// The plane's registry decl key — the core mount looks the live runtime slot up under it and, - /// when absent (the plane is unconfigured this generation), mounts nothing, exactly as the non-WS - /// route loop skips a plane with no slot. - pub slot_key: &'static str, - /// The neutral accept fn the core mount hands a [`WsArrival`], returning the finished response. - pub accept: WsAcceptFn, -} - -/// THE PROCESS-WIDE INSTALLED WS ARRIVALS — set once by the composition root -/// ([`install_ws_arrivals`]), read (cloned) by the core router at build ([`take_ws_arrivals`]). A -/// `OnceLock>`, mirroring `ingress::arrival::install_path_ingress`: the composition root -/// ASSEMBLES it from whichever duplex-plane crates are linked, install-once + read-many so a test that -/// builds several routers each mounts the same arrivals. -static INSTALLED_WS_ARRIVALS: std::sync::OnceLock> = std::sync::OnceLock::new(); - -/// INSTALL THE INBOUND WS-ACCEPT ARRIVALS — the composition root's one write, mirroring -/// [`crate::ingress::arrival::install_path_ingress`]. First-writer-wins (idempotent): a second call is -/// ignored rather than panicking, so a test harness that composes twice does not abort. Only the -/// composition root (or a test) calls this; a build with no duplex plane never does, and -/// [`take_ws_arrivals`] then yields nothing. -pub fn install_ws_arrivals(arrivals: Vec) { - let _ = INSTALLED_WS_ARRIVALS.set(arrivals); -} - -/// DRAIN (by clone) THE INSTALLED WS-ACCEPT ARRIVALS — the core router's one read at build. Returns an -/// empty vec when no duplex plane installed any, so the router mounts no WS-accept route (byte- -/// identical assembly). Cloning (not taking) keeps it read-many so repeated router builds are stable. -#[must_use] -pub fn take_ws_arrivals() -> Vec { - INSTALLED_WS_ARRIVALS.get().cloned().unwrap_or_default() -} diff --git a/crates/busbar-kernel/src/plane_host/mod.rs b/crates/busbar-kernel/src/plane_host/mod.rs index d5db438921..3d1ca69a0c 100644 --- a/crates/busbar-kernel/src/plane_host/mod.rs +++ b/crates/busbar-kernel/src/plane_host/mod.rs @@ -2920,8 +2920,8 @@ pub trait ContainerGateSink: PlaneSlots { } // ───────────────────────────────────────────────────────────────────────────────────────────── -// D3 WITNESS — the gauntlet siblings COEXIST and share ONE `admit_open` gate. (That `begin_session` -// actually CALLS `run_gauntlet_session` at its call site is pinned in busbar-voice's topology tests.) +// D3 WITNESS — the gauntlet siblings COEXIST and share ONE `admit_open` gate. (That a session +// open actually CALLS `run_gauntlet_session` at its call site is the serving door's to pin.) // ───────────────────────────────────────────────────────────────────────────────────────────── #[cfg(test)] #[path = "tests/gauntlet_session_tests.rs"] diff --git a/crates/busbar-kernel/src/plane_host/tests/mod_tests.rs b/crates/busbar-kernel/src/plane_host/tests/mod_tests.rs index 7b24be6b9a..8d13aff4c5 100644 --- a/crates/busbar-kernel/src/plane_host/tests/mod_tests.rs +++ b/crates/busbar-kernel/src/plane_host/tests/mod_tests.rs @@ -660,9 +660,10 @@ fn stale_host_ctx_through_a_real_vtable_slot_fails_closed() { /// 1. It admits ANY JSON object as the replacement. It does not know, and cannot check, the TYPE the /// receiving plane will decode into. For MCP and A2A the target is `serde_json::Value`, so bytes /// core commits always parse and their "output I cannot read back" arms are unreachable through -/// this host. For `busbar-voice` the target is `SessionConfig`, which rejects `{"voice": 7}` as a -/// type mismatch — so THAT plane's arm is genuinely reachable through core's real host, and its -/// refusal (`busbar_voice::mount::committed_session_config`) is a live fix, not a dormant one. +/// this host. For a session plane the target is a typed session configuration, which rejects +/// `{"voice": 7}` as a type mismatch — so THAT plane's arm is genuinely reachable through +/// core's real host, and its refusal (its door's `committed_session_config`) is a live fix, +/// not a dormant one. /// 2. The commit is a WHOLESALE REPLACEMENT, not a merge. A hook that names one field produces /// `arguments` carrying ONLY that field — every other key the caller (or the plane's own lock) /// had is gone from the committed bytes. A plane whose target type defaults its absent fields diff --git a/crates/busbar-kernel/src/router.rs b/crates/busbar-kernel/src/router.rs index 40bd492d8f..86e800cccd 100644 --- a/crates/busbar-kernel/src/router.rs +++ b/crates/busbar-kernel/src/router.rs @@ -493,14 +493,6 @@ pub(crate) fn base_data_router( }); } } - // THE PLANES' INBOUND WS-ACCEPT ARRIVALS, drained from the neutral substrate registry the - // composition root installed (`install_ws_arrivals`). Behind the neutral `duplex-ws` feature: the - // default/shipped money-path build compiles neither this line nor `mount_ws_arrivals`, so the - // router emits the same routes in the same order (byte-identical assembly) and `axum/ws` (hence - // tokio-tungstenite) stays out of the graph. Each spec's live slot is resolved from the SAME - // `plane_slots` the non-WS loop reads, so an unconfigured plane mounts nothing. - #[cfg(feature = "duplex-ws")] - let router = mount_ws_arrivals(router, plane_slots); // THE AUTHORIZATION SERVER'S ROUTES, or none of them. Same posture as the two planes above: a // deployment that is not an authorization server carries no `/authorize`, no `/token`, no // metadata document and nothing in the route table. Mounted through the seam @@ -749,100 +741,6 @@ fn mount_plane_session( ) } -/// Mount the planes' inbound WS-accept ARRIVALS onto the core router (the WS sibling of -/// [`mount_plane_route`]) — behind the neutral `duplex-ws` feature, so the default build compiles -/// none of this and names no WS type. Drains the neutral substrate registry -/// (`take_ws_arrivals`), and per spec calls the SAME [`crate::core_routes::CoreRouter::route`] the -/// non-WS adapter calls, with the spec's own `(path, GET, auth)` — so the `CoreRouteTable` row is -/// byte-identical in shape to a data route and the auth middleware enforces `auth` BEFORE the accept -/// fn, exactly as for `PlaneRouteSpec`. Method is GET: a WS upgrade is an HTTP GET. -/// -/// The handler is a CORE-owned axum closure whose ONLY new extractor is -/// [`axum::extract::ws::WebSocketUpgrade`] (named here and nowhere the default build compiles). It -/// assembles a substrate-owned [`busbar_kernel::ingress::duplex_ws::WsArrival`] from the SAME -/// sources `mount_plane_route` reads (minus the body — a WS GET carries none), carrying the upgrade -/// BY VALUE and the plane's live slot (resolved from `plane_slots` under the spec's `slot_key`, so an -/// unconfigured plane mounts nothing), and hands it to the plane's neutral accept fn — which reaches -/// `serve_gauntlet`/`accept_gauntlet` internally and never a bare `on_upgrade`. -#[cfg(feature = "duplex-ws")] -fn mount_ws_arrivals( - router: crate::core_routes::CoreRouter, - plane_slots: &std::collections::BTreeMap< - &'static str, - std::sync::Arc, - >, -) -> crate::core_routes::CoreRouter { - use busbar_contract::abi::mechanism::route::RouteMethod; - let mut router = router; - for spec in busbar_kernel::ingress::duplex_ws::take_ws_arrivals() { - let busbar_kernel::ingress::duplex_ws::WsArrivalSpec { - path, - auth, - slot_key, - accept, - } = spec; - // No live slot this generation ⇒ the plane is unconfigured ⇒ mount nothing, exactly as the - // non-WS route loop skips a plane with no slot. - let Some(slot) = plane_slots.get(slot_key) else { - continue; - }; - let slot = slot.clone(); - let ctx_path = path.clone(); - router = router.route(path, RouteMethod::Get, auth, { - move |upgrade: axum::extract::ws::WebSocketUpgrade, - axum::extract::State(handle): axum::extract::State< - std::sync::Arc, - >, - raw_params: axum::extract::RawPathParams, - uri: axum::http::Uri, - gov: Option< - axum::extract::Extension, - >, - principal: Option< - axum::extract::Extension, - >, - consumed: Option>, - mut headers: axum::http::HeaderMap| { - // As for a data route: the plane never sees the credential the gate consumed. - auth::ConsumedCredentials::strip_from(consumed.as_deref(), &mut headers); - let accept = accept.clone(); - let slot = slot.clone(); - let ctx_path = ctx_path.clone(); - let path_params: Vec<(String, String)> = raw_params - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - async move { - let gov = gov.map(|axum::extract::Extension(g)| g); - let caller_principal = gov - .as_ref() - .and_then(|g| g.key.as_ref().map(|k| k.id.clone())); - // Mint the neutral host seam over the request's live engine snapshot, exactly as - // `mount_plane_route` does — the accept fn reaches host capabilities through it. - let host = crate::plane_host::engine_host_from_handle(&handle); - let arrival = busbar_kernel::ingress::duplex_ws::WsArrival { - upgrade, - gov, - principal: principal.map(|axum::extract::Extension(p)| p), - caller_principal, - path: ctx_path, - uri, - headers, - path_params, - host, - slot, - }; - // The accept fn resolves to the finished response: the 101-switching upgrade on - // admit, or a pre-upgrade refusal (operator-hook or gauntlet) on refuse — no socket - // bound, no task spawned. It is async so the plane runs its hooks before the upgrade. - accept(arrival).await - } - } - }); - } - router -} - /// Apply the shared middleware stack — auth chain, request-body cap, 413 reshaping, server-timing — /// and bind the swappable `AppHandle` state. Identical for the single-listener router and each /// split-plane router, so both planes get the SAME auth + limit posture and both see config-apply diff --git a/crates/busbar-plane-streaming/Cargo.toml b/crates/busbar-plane-streaming/Cargo.toml index f22819d57e..8e1c986587 100644 --- a/crates/busbar-plane-streaming/Cargo.toml +++ b/crates/busbar-plane-streaming/Cargo.toml @@ -51,10 +51,9 @@ serde_json = { workspace = true } # THE MEDIA-FRAME CARRIER. `IrAudioFrame::media` and the tool IR's opaque payloads are `bytes::Bytes`; # this crate reads and builds them at the same seam rather than converting through an owned `Vec`. bytes = { workspace = true } -# THE SERVER-SIDE TOOL EXECUTOR PORT's async signature — an `async fn` in a trait object, written on the -# `#[async_trait]` desugaring it always was. A proc-macro that expands to `Box`ed futures and pulls no -# I/O into this crate's closure. -async-trait = { workspace = true } +# THE DOOR'S ONE READING OF THE PUBLIC BASE URL (`door::absolute`): the audience and the metadata URL +# replace the path of the deployment's public URL and drop its query and fragment, parsed once. +url = { workspace = true } [dev-dependencies] # The TEST-ONLY seal (#65). `busbar_contract::plugin::TestKernelSeal` is the one blessed @@ -64,3 +63,16 @@ busbar-contract = { path = "../busbar-contract", features = ["test-seal"] } # The conformance rig drives the plane end-to-end over real openai-realtime (the voice dialect) wire # bytes, so it builds the same JSON fixtures the underlying codec tests use. serde_json = { workspace = true } +# THE DOOR, BOTH WAYS (`tests/conformance.rs`, the plugin's own witness): the loader's plane kind +# loads the linked door and the dropped-in image the example below builds, and drives both through +# one script. Dev-only: absent from every release build. +busbar-plugin-loader = { path = "../plugin-loader" } + +# THE DROPPED-IN IMAGE ON THE ONE DOOR: built by `cargo test`. +[[example]] +name = "streaming_door" +crate-type = ["cdylib"] + +# THE VOICE CONFORMANCE HARNESS that drives the door above both ways (`testing/voice-conformance/`, +# ARCHITECT Q6) is its own testkit crate, `testing/voice-conformance/harness`: this crate's one +# user of the loader stays its own both-ways witness, `tests/conformance.rs`. diff --git a/crates/busbar-plane-streaming/examples/streaming_door.rs b/crates/busbar-plane-streaming/examples/streaming_door.rs new file mode 100644 index 0000000000..8018cc909c --- /dev/null +++ b/crates/busbar-plane-streaming/examples/streaming_door.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! The streaming plane's door, dropped in: the one exported symbol, forwarding to the same door a +//! compiled-in row holds. `tests/conformance.rs` loads it beside the linked door and drives both the +//! same way. + +busbar_contract::export_door!(busbar_plane_streaming::door::door); diff --git a/crates/busbar-plane-streaming/src/claims.rs b/crates/busbar-plane-streaming/src/claims.rs index 5dc99abedc..e1e9f70313 100644 --- a/crates/busbar-plane-streaming/src/claims.rs +++ b/crates/busbar-plane-streaming/src/claims.rs @@ -12,15 +12,13 @@ //! //! * **Twilio Media Streams rides the WebSocket carrier.** Its wire is one JSON event per WebSocket //! message (`connected`/`start`/`media`/`mark`/`dtmf`/`stop`): the carrier frames the messages and -//! the event envelope is this plane's dialect ([`crate::twilio`], [`crate::ulaw`]). The design +//! the event envelope is this plane's dialect ([`crate::codec::topology::twilio`]). The design //! lists five transports and no telephony one. Twilio //! cannot present a busbar bearer or API key, so its claim authenticates under the //! `webhook-signature` alternative alone: the `X-Twilio-Signature` HMAC over the request URL, //! verified by the auth kind's inbound verify, never by this plane. //! * **`webrtc`** — no codec surface for the RTP media plane exists anywhere in this crate's closure -//! (busbar-voice's WebRTC topology is `runtime`-gated and, per its own module documentation, is a -//! browser-sideband ferry over the same JSON event vocabulary rather than a distinct wire format — -//! but a plane cannot claim a transport it cannot decode frames from without lying about what it +//! (a plane cannot claim a transport it cannot decode frames from without lying about what it //! reads). It is unclaimed: a claim is declared independently, and the boot's own overlap check //! is what proves the claims stay disjoint when it is added. diff --git a/crates/busbar-plane-streaming/src/codec/ir/codec/gemini/mod.rs b/crates/busbar-plane-streaming/src/codec/ir/codec/gemini/mod.rs index 00fcbe62e6..7e5e8815c8 100644 --- a/crates/busbar-plane-streaming/src/codec/ir/codec/gemini/mod.rs +++ b/crates/busbar-plane-streaming/src/codec/ir/codec/gemini/mod.rs @@ -4,7 +4,7 @@ //! THE SECOND DIALECT — Google **Gemini Live** (`BidiGenerateContent`), mapped to/from the SAME //! shared voice IR that [`super::OpenAiRealtimeCodec`] targets. Design `BUSBAR-1.6.0.md` #18/#45. //! -//! This is the codec that turns busbar-voice into a real voice *translator*: a Gemini-Live wire event +//! This is the codec that turns the streaming plane into a real voice *translator*: a Gemini-Live wire event //! is decoded into the plane-owned IR ([`IrClientEvent`] / [`IrServerEvent`]), and the OpenAI codec //! (or this one) re-frames that IR onto its own wire. Earning a cross-dialect superset IR is exactly //! what a SECOND dialect does — the A2A discipline (a plane earns a superset at its second wire format, diff --git a/crates/busbar-plane-streaming/src/codec/ir/config.rs b/crates/busbar-plane-streaming/src/codec/ir/config.rs index 52a7cb4759..eff13d01c0 100644 --- a/crates/busbar-plane-streaming/src/codec/ir/config.rs +++ b/crates/busbar-plane-streaming/src/codec/ir/config.rs @@ -154,7 +154,8 @@ pub struct SessionConfig { )] pub turn_detection: Option>, /// The tool set, carried VERBATIM as opaque JSON (the plane locks the set but never reshapes a - /// definition — the plane's tool moat (`BUSBAR-1.6.0.md` #18/#45) normalizes call CORRELATION, not the argument/definition bytes). + /// definition — the tool layer (`BUSBAR-1.6.0.md` #18/#45) normalizes call CORRELATION, not the + /// argument/definition bytes). #[serde(default, skip_serializing_if = "Vec::is_empty")] pub tools: Vec, /// Tool-choice policy (`"auto"` / `"none"` / `"required"` / a forced-call object), opaque. diff --git a/crates/busbar-plane-streaming/src/codec/ir/event.rs b/crates/busbar-plane-streaming/src/codec/ir/event.rs index be6c3eb351..e4cf199084 100644 --- a/crates/busbar-plane-streaming/src/codec/ir/event.rs +++ b/crates/busbar-plane-streaming/src/codec/ir/event.rs @@ -21,7 +21,7 @@ pub enum IrClientEvent { AudioFrame(IrAudioFrame), /// A session-control / config event the client sent (reconciled against the locked config). Control(IrDuplexControl), - /// A server-side tool RESULT the plane authored back toward the upstream (`CallResult`). + /// The caller's tool RESULT, carried toward the upstream (`CallResult`). Tool(IrDuplexTool), } diff --git a/crates/busbar-plane-streaming/src/codec/ir/mod.rs b/crates/busbar-plane-streaming/src/codec/ir/mod.rs index 02093f797f..5df3a6d47f 100644 --- a/crates/busbar-plane-streaming/src/codec/ir/mod.rs +++ b/crates/busbar-plane-streaming/src/codec/ir/mod.rs @@ -3,7 +3,7 @@ //! THE PLANE-4 DUPLEX / SESSION IR — the plane's OWN vocabulary. //! -//! These are the nouns that live ONLY in `busbar-voice` +//! These are the nouns that live ONLY in this plane //! (`docs/design/BUSBAR-1.6.0.md` #18/#45): the four-layer duplex/session IR and its //! reader/writer pair. Per `BUSBAR-1.6.0.md` #18/#45 "pass-through is still an IR" — the layers differ in HOW MUCH the IR //! reshapes the wire, from full normalization (tool-call) to identity (media): diff --git a/crates/busbar-plane-streaming/src/codec/ir/tool.rs b/crates/busbar-plane-streaming/src/codec/ir/tool.rs index 3cf49adade..674432c120 100644 --- a/crates/busbar-plane-streaming/src/codec/ir/tool.rs +++ b/crates/busbar-plane-streaming/src/codec/ir/tool.rs @@ -1,11 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! LAYER 1 — TOOL-CALL (full normalization; the moat). Design `BUSBAR-1.6.0.md` #18/#45. +//! LAYER 1 — TOOL-CALL (correlation normalization). Design `BUSBAR-1.6.0.md` #18/#45. //! -//! The one layer where the IR genuinely reshapes the wire, and the whole reason a governed plane -//! beats a dumb WS pipe: tools execute server-side, under governance, and the browser is never trusted -//! to author them. +//! The one layer where the IR reshapes the wire: a call's correlation, so one dialect's call can be +//! answered in another's terms. A tool call is part of the model's response and goes back to the +//! caller unchanged except for dialect translation; the caller runs the tool and busbar executes +//! nothing (Law 11; QUESTIONS Q98). use bytes::Bytes; @@ -28,8 +29,8 @@ pub struct CallRef(pub u64); /// /// The Realtime tool loop this normalizes: the model announces a call /// (`response.output_item.added` with a `function_call` item) then streams -/// `response.function_call_arguments.delta` → `…done`; busbar executes the tool server-side and -/// returns `conversation.item.create{ function_call_output }` then `response.create`. A tool-call turn +/// `response.function_call_arguments.delta` → `…done`; the caller runs the tool and returns +/// `conversation.item.create{ function_call_output }` then `response.create`. A tool-call turn /// often produces NO audio until the result is fed back. #[derive(Debug, Clone, PartialEq, Eq)] pub enum IrDuplexTool { @@ -58,8 +59,8 @@ pub enum IrDuplexTool { /// The raw wire id the dialect correlates on. call_id: String, }, - /// busbar's SERVER-SIDE RESULT (client→server) — authored by the plane after governance, never by - /// the browser. Written back to the upstream as `function_call_output` + `response.create`. + /// The caller's RESULT (client→server), carried upstream as `function_call_output` + + /// `response.create` once the node's open-call table took it. busbar never authors one. CallResult { /// The call this result answers. call_ref: CallRef, diff --git a/crates/busbar-plane-streaming/src/codec/mod.rs b/crates/busbar-plane-streaming/src/codec/mod.rs index dbc3ad9913..a7a28b0432 100644 --- a/crates/busbar-plane-streaming/src/codec/mod.rs +++ b/crates/busbar-plane-streaming/src/codec/mod.rs @@ -3,8 +3,9 @@ //! THE VOICE DUPLEX CODECS — the pure half of the voice protocol plugin. //! -//! `busbar-voice` held two things behind one name: these codecs — the plane-4 duplex/session -//! intermediate representation (media frames, control, events, tools, session config, usage), the +//! The retired `busbar-voice` crate held two things behind one name: these codecs — the plane-4 +//! duplex/session intermediate representation (media frames, control, events, tools, session +//! config, usage), the //! shared duplex reader/writer over it, the Gemini Live dialect and the Twilio Media Streams //! grammar — and the runtime that carries them over a live socket (the axum mount, the WebSocket //! accept, the tokio session tasks, the telephony dial, the HTTPS token minter). The plane crate @@ -14,10 +15,9 @@ //! earlier spelling and was DELETED into `busbar-plane-streaming` at `fbead1a31`, #18/#83.) //! //! So the codecs live here, naming only the plugin contract (`busbar-contract`, for the base64 media -//! transcode, the billing carrier and its reserved unit names) plus serde and `bytes`. -//! `busbar-voice` depends on this crate and re-exports every module that moved under -//! its old path, so `busbar_voice::ir::…` and `busbar_voice::topology::twilio::…` resolve exactly -//! what they always did. The split is a MOVE: no item changed shape crossing it. +//! transcode, the billing carrier and its reserved unit names) plus serde and `bytes`. The split +//! was a MOVE: no item changed shape crossing it. The runtime half was deleted at FLIP-STREAMING, +//! once this plane's door served every route. /// THE REGISTRY KEY THE STREAMING PLANE IS KNOWN BY — the string the composition root flips /// onto the unified kernel loop's session admit @@ -25,9 +25,7 @@ /// gauntlet reports from its `GauntletPlane::capability_key`. /// /// Named ONCE, here, on the pure side of the split, because the plane's `capability_key` and the -/// composition-root FLIP must reference the SAME literal or a swap could drift onto two. It agrees -/// with `busbar-voice`'s `PLANE_DECLARATION.key` (`"streaming"`). `busbar-voice` re-exports it as -/// `busbar_voice::PLANE_KEY`, the one stable path the `busbar` binary names. +/// composition-root FLIP must reference the SAME literal or a swap could drift onto two. pub const PLANE_KEY: &str = "streaming"; pub mod ir; diff --git a/crates/busbar-plane-streaming/src/codec/topology/twilio.rs b/crates/busbar-plane-streaming/src/codec/topology/twilio.rs index 2c3f674980..15627508ca 100644 --- a/crates/busbar-plane-streaming/src/codec/topology/twilio.rs +++ b/crates/busbar-plane-streaming/src/codec/topology/twilio.rs @@ -30,6 +30,8 @@ use serde::Serialize; // nothing bought for it, and this is the primitive that turns bytes on a phone call into audio. use busbar_contract::media::{base64_decode, base64_encode}; +use crate::codec::ir::codec::WireEvent; + /// Twilio's negotiated µ-law encoding string on the `start` event's media format. pub const TWILIO_MULAW_ENCODING: &str = "audio/x-mulaw"; /// The only sample rate the 8 kHz passthrough carrier accepts. @@ -256,14 +258,77 @@ impl TwilioEnvelope { /// field order is fixed so the serialization is deterministic and byte-stable. #[must_use] pub fn encode_media(stream_sid: &str, mulaw: &[u8]) -> Vec { - let out = OutboundMedia { - event: "media", - stream_sid, - media: OutboundPayload { - payload: base64_encode(mulaw), - }, - }; - serde_json::to_vec(&out).expect("the outbound media envelope always serializes") + let mut out = Vec::new(); + Self::encode_media_into(&mut out, stream_sid, mulaw); + out + } + + /// The same envelope, rendered into a buffer the caller already holds. + /// + /// This is the shape the downlink writes in. The envelope is fixed (three members, in this + /// order) and everything about it except the identifier and the payload is known at compile + /// time. Building a document, a string for the payload, and then serializing would spend three + /// allocations per audio frame, and a call carries fifty frames a second in each direction. + /// The buffer is CLEARED, not appended to, so a caller may hand the same one back frame after + /// frame and pay for its growth once. An identifier the serializer would have to escape goes + /// through the serializer, which is the authority on those bytes. + pub fn encode_media_into(out: &mut Vec, stream_sid: &str, mulaw: &[u8]) { + out.clear(); + if !is_bare_json_string(stream_sid) { + let doc = OutboundMedia { + event: "media", + stream_sid, + media: OutboundPayload { + payload: base64_encode(mulaw), + }, + }; + out.extend_from_slice( + &serde_json::to_vec(&doc).expect("the outbound media envelope always serializes"), + ); + return; + } + const HEAD: &[u8] = br#"{"event":"media","streamSid":""#; + const MIDDLE: &[u8] = br#"","media":{"payload":""#; + const TAIL: &[u8] = br#""}}"#; + out.reserve( + HEAD.len() + stream_sid.len() + MIDDLE.len() + mulaw.len().div_ceil(3) * 4 + TAIL.len(), + ); + out.extend_from_slice(HEAD); + out.extend_from_slice(stream_sid.as_bytes()); + out.extend_from_slice(MIDDLE); + base64_encode_into(mulaw, out); + out.extend_from_slice(TAIL); + } +} + +/// Whether a string is its own JSON body: printable ASCII with neither character a JSON string +/// cannot carry raw. Everything else goes to the serializer. +fn is_bare_json_string(s: &str) -> bool { + s.bytes() + .all(|b| (0x20..0x7f).contains(&b) && b != b'"' && b != b'\\') +} + +/// Standard base64 (RFC 4648) with padding, appended to a buffer the caller holds: the same bytes +/// `busbar_contract::media::base64_encode` produces, without the string it would allocate. +fn base64_encode_into(data: &[u8], out: &mut Vec) { + const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for chunk in data.chunks(3) { + let b0 = u32::from(chunk[0]); + let b1 = u32::from(*chunk.get(1).unwrap_or(&0)); + let b2 = u32::from(*chunk.get(2).unwrap_or(&0)); + let n = (b0 << 16) | (b1 << 8) | b2; + out.push(ALPHABET[((n >> 18) & 63) as usize]); + out.push(ALPHABET[((n >> 12) & 63) as usize]); + out.push(if chunk.len() > 1 { + ALPHABET[((n >> 6) & 63) as usize] + } else { + b'=' + }); + out.push(if chunk.len() > 2 { + ALPHABET[(n & 63) as usize] + } else { + b'=' + }); } } @@ -386,6 +451,87 @@ fn xml_escape(s: &str) -> String { out } +/// THE TELEPHONY CARRIER'S ENVELOPE around a session: the caller speaks Twilio Media Streams, the +/// far end the realtime dialect locked to `g711_ulaw` both ways, so the audio passes through +/// unchanged and only the envelope is rewritten. +#[derive(Debug, Default)] +pub struct TwilioBridge { + stream_sid: Option, +} + +/// What one caller frame on the telephony leg is. +#[derive(Debug, PartialEq, Eq)] +pub enum CallerStep { + /// Caller audio, as the far end's dialect frames an uplink append. + Audio(WireEvent), + /// The call ended. + Stop, + /// The call is refused (no stream id, another media format, a frame not in Twilio's shape, a + /// payload that is not clean base64): the session ends. + Refused, + /// Nothing for the far end (the handshake, a start, a mark, a keypress, an event this reader + /// does not model, or media for another stream). + Nothing, +} + +impl TwilioBridge { + /// Read one caller frame. A start with no stream id, or in another media format, is refused; so + /// is a frame that is not Twilio's shape or carries a payload that is not clean base64 (data + /// resuming after padding is never billed as a longer payload). An event this reader does not + /// model is dropped. + pub fn from_caller(&mut self, frame: &[u8]) -> CallerStep { + match TwilioEnvelope::decode(frame) { + Ok(TwilioEvent::Start(start)) => { + if start.stream_sid.is_empty() || assert_g711_ulaw(&start.media_format).is_err() { + return CallerStep::Refused; + } + self.stream_sid = Some(start.stream_sid); + CallerStep::Nothing + } + Ok(TwilioEvent::Media { + stream_sid, + payload, + }) => { + if stream_sid.is_empty() || self.stream_sid.as_deref() != Some(stream_sid.as_str()) + { + return CallerStep::Nothing; + } + let append = serde_json::json!({ + "type": "input_audio_buffer.append", + "audio": base64_encode(&payload), + }); + CallerStep::Audio(WireEvent(bytes::Bytes::from( + serde_json::to_vec(&append).unwrap_or_default(), + ))) + } + Ok(TwilioEvent::Stop) => CallerStep::Stop, + Ok(_) | Err(TwilioError::UnknownEvent(_)) => CallerStep::Nothing, + Err(_) => CallerStep::Refused, + } + } + + /// Rewrite one frame bound for the caller into the carrier's envelope: model audio becomes a + /// `media` frame on the call's stream, and a barge-in clears the audio the carrier has queued. + /// `None` for a frame the carrier has no event for. + #[must_use] + pub fn to_caller(&self, frame: &[u8]) -> Option> { + let sid = self.stream_sid.as_deref()?; + let v: serde_json::Value = serde_json::from_slice(frame).ok()?; + match v.get("type").and_then(serde_json::Value::as_str)? { + "response.output_audio.delta" | "response.audio.delta" => { + let audio = base64_decode(v.get("delta")?.as_str()?)?; + Some(TwilioEnvelope::encode_media(sid, &audio)) + } + "input_audio_buffer.speech_started" => serde_json::to_vec(&serde_json::json!({ + "event": "clear", + "streamSid": sid, + })) + .ok(), + _ => None, + } + } +} + #[cfg(test)] #[path = "tests/twilio_tests.rs"] mod twilio_tests; diff --git a/crates/busbar-plane-streaming/src/door.rs b/crates/busbar-plane-streaming/src/door.rs new file mode 100644 index 0000000000..7d9a018024 --- /dev/null +++ b/crates/busbar-plane-streaming/src/door.rs @@ -0,0 +1,1322 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE STREAMING PLANE'S DOOR: the one function a compiled-in row holds and a dropped-in image +//! exports (`examples/streaming_door.rs`), and everything it states and answers. +//! +//! The composition root links [`door`] on its `plane-door` axis (the `plane-streaming` row, +//! `BUSBAR-1.6.0.md` Part 3, section 12) and binds it through the loader's one load beside the +//! dropped-in plane doors. +//! +//! ## What the plane states once +//! +//! The Statement tail ([`TAIL`]): the `streams:` section it declares (and the `providers:` section +//! a document must carry when the plane is linked), its dialects, the `session` grant kind, the six +//! billable classes a session counts in, the per-session fee, its connection needs per direction +//! and the config path its egress target is read from. +//! +//! ## What each generation publishes +//! +//! A snapshot per generation: the doors a session is opened through ([`ROUTES`]), and the audience +//! a caller's token must carry with the metadata URL a refused caller is pointed at, both one +//! reading of the deployment's public base URL. A deployment with no public URL fronts nothing: its +//! snapshot claims nothing and binds no audience. +//! +//! ## What the settings blob is +//! +//! The plane's own `streams:` section as JSON, the kernel's reserved keys already read and stripped. +//! [`read_settings`] judges it with the plane's grammar ([`crate::config::StreamsCfg`], unknown keys +//! refused); an empty blob is the default section. +//! +//! ## What each unit's pieces are answered +//! +//! A one-request door (the mint, the SDP offer) answers its pieces through a [`RequestUnit`] the +//! instance keeps by the kernel's unit key, built at the unit's first piece over the newest live +//! generation's session params. Its answer's bytes are paid into +//! the reply buffer across `more = 1` re-calls ([`crate::piece`]). +//! +//! A piece that names a stream is a live session's, served by the door's [`Sessions`] +//! ([`crate::session_door`]): the driver's duplex vocabulary (K6), one frame per answer, the +//! session's unsolicited output named on the instance's driver ticket, and the session's cumulative +//! units on every answer. A request unit on a session door is refused. + +use std::collections::BTreeMap; +use std::mem::size_of; +use std::ptr; +use std::sync::Mutex; + +use crate::piece::{self, Owed}; +use busbar_contract::abi::hook::REQUEST_STREAM; +use busbar_contract::abi::host::conn::connector::{ + Need, DIRECTION_INBOUND, DIRECTION_OUTBOUND, EGRESS_PROVIDER, KEEP_NAMED, +}; +use busbar_contract::abi::mechanism::call::{ + AbiStr, Blob, InHead, OutHead, Outcome, Span, BLOB_ABSENT, +}; +use busbar_contract::abi::mechanism::door::{ + KindTailHead, Section, Statement, SECTION_CONSUMED, SECTION_DECLARING, +}; +use busbar_contract::abi::mechanism::lifecycle::{ + GenIn, RefreshIn, ReleaseIn, TickIn, TickOut, ValidateIn, +}; +use busbar_contract::abi::mechanism::ticket::Ticket; +use busbar_contract::abi::plane::{ + class_of, reason_of, ArriveIn, ArriveOut, BillableClass, DialectAuth, OnPieceIn, OnPieceOut, + OpClass, OutField, PlaneCancelIn, PlaneCancelOut, PlaneDriveIn, PlaneDriveOut, PlaneOpenIn, + PlaneOpenOut, PlaneRefreshOut, PlaneSnapshot, PlaneTail, ProjectIn, ProjectOut, RefusalClass, + RefusalIn, RefusalOut, ServeIn, ServeOut, UnitCount, CANCEL_FAILED, CLAIM_EXACT, CLAIM_OPEN, + EMIT_DONE, EMIT_TO_FAR_END, FROM_CALLER, FROM_FAR_END, FROM_KERNEL, INGRESS_DUPLEX_SESSION, + INGRESS_REQUEST_RESPONSE, PIECE_HAS_STATUS, PIECE_LAST, PIECE_OUT_TEXT, ROUTE_DIRECT, + SHAPE_PIECEWISE, SPAN_ABSENT, UNITS_ESTIMATED, UNITS_REPORTED, +}; +use busbar_contract::abi::sdk::door::{abi_str, statement}; +use busbar_contract::abi::sdk::publish::{ClaimSpec, SnapshotSpec}; +use busbar_contract::abi::sdk::{Generations, HostBuf, Instance, Lent, Out, Safe, SafeSlot, Wake}; +use busbar_contract::plane::{PER_SESSION, TOKEN_FAMILY}; + +use crate::claims::{Dialect, HTTP_TRANSPORT, WS_TRANSPORT}; +use crate::config::StreamsCfg; +use crate::driven::{Door, Steps, FIELD_CONTENT_LENGTH}; +use crate::meta; +use crate::provider::{GEMINI_LIVE, OPENAI_REALTIME}; +use crate::request_unit::{self, Answer, Piece, RequestUnit}; +use crate::session_door::{committed_session_config, Live, Sessions, Side, CEILING_TICK_NS}; + +/// The name the plane's Statement carries. +pub const NAME: &str = crate::codec::PLANE_KEY; +/// The plane's config section. +pub const SECTION: &str = "streams"; +/// The grant kind that admits traffic on this plane: one live session. +pub const SCOPE: &str = "session"; +/// The plane's human label. +pub const LABEL: &str = "Streaming"; +/// What one live session on this plane is called. +pub const SUBJECT_NOUN: &str = "streaming session"; +/// The singular noun for one session, in admin responses. +pub const ADMIN_NOUN: &str = "streaming-session"; +/// The record resource kind a session is audited under. +pub const AUDIT_KIND: &str = "streaming_session"; +/// The one value a key's `session` grant names to open a live session here: the pool every session +/// is served on. +pub const SESSION_POOL: &str = "streaming-server"; +/// The section the plane reads but does not own: the upstream destinations (core's grammar, as the +/// llm door consumes it). +pub const PROVIDERS_SECTION: &str = "providers"; +/// Where, inside `streams:`, the upstream a session dials is named. +pub const EGRESS_TARGET: &str = "session.model"; +/// The inbound auth style of a keyed door. +pub const KEY_AUTH: &str = "bearer"; +/// The inbound auth style of the telephony door: the carrier's signed request. +pub const SIGNATURE_AUTH: &str = "webhook-signature"; + +/// The mount every door of the plane sits under. +pub const MOUNT_PATH: &str = "/v1/realtime"; +/// The metadata URL a refused caller is pointed at (bound, never claimed: 1.5.5 parity). +pub const METADATA_PATH: &str = "/.well-known/oauth-protected-resource/v1/realtime"; + +/// An absent string. +const NONE: AbiStr = AbiStr { + ptr: ptr::null(), + len: 0, +}; + +const SECTIONS: &[Section] = &[ + Section { + name: abi_str(SECTION), + flags: SECTION_DECLARING, + _reserved: 0, + }, + Section { + name: abi_str(PROVIDERS_SECTION), + flags: SECTION_CONSUMED, + _reserved: 0, + }, +]; + +/// The dialects, in the order the per-call `dialect` index names them; then the PROVIDER +/// protocols a member's provider states, as the far-end spellings of the two realtime dialects +/// (ARCHITECT Q-L5B-NEEDS: the provider protocol -> realtime dialect mapping is the plane's own): +/// `openai` is spoken as OpenAI Realtime, `gemini` as Gemini Live. No arrival names them. +pub const DIALECTS_BY_NAME: &[&str] = &[ + OPENAI_REALTIME, + GEMINI_LIVE, + Dialect::TwilioMediaStreams.name(), + OPENAI_PROVIDER, + GEMINI_PROVIDER, +]; + +/// The provider protocol whose realtime dialect is OpenAI Realtime. +pub const OPENAI_PROVIDER: &str = "openai"; +/// The provider protocol whose realtime dialect is Gemini Live. +pub const GEMINI_PROVIDER: &str = "gemini"; + +const DIALECTS: &[AbiStr] = &[ + abi_str(DIALECTS_BY_NAME[0]), + abi_str(DIALECTS_BY_NAME[1]), + abi_str(DIALECTS_BY_NAME[2]), + abi_str(DIALECTS_BY_NAME[3]), + abi_str(DIALECTS_BY_NAME[4]), +]; + +/// The OpenAI Realtime far end's credential style (`Authorization: Bearer`). +pub const REALTIME_STYLE: &str = "bearer"; +/// The Gemini Live far end's credential style (the `x-goog-api-key` header). +pub const LIVE_STYLE: &str = "x-goog-api-key"; + +const fn dialect_auth(dialect: u32, style: &'static str) -> DialectAuth { + DialectAuth { + dialect, + _reserved: 0, + style: abi_str(style), + params: Blob::ABSENT, + } +} + +/// The upstream dialects' default auth styles: each realtime dialect's, and its provider +/// protocol's spelling's. +const DIALECT_AUTH: &[DialectAuth] = &[ + dialect_auth(0, REALTIME_STYLE), + dialect_auth(1, LIVE_STYLE), + dialect_auth(3, REALTIME_STYLE), + dialect_auth(4, LIVE_STYLE), +]; + +const SCOPE_KINDS: &[AbiStr] = &[abi_str(SCOPE)]; + +/// The one operation every door serves: a session open (or the one request that brokers one). +const OP_CLASSES: &[OpClass] = &[OpClass { + op: abi_str(meta::OP_SESSION_OPEN.as_str()), + name: abi_str(LABEL), +}]; + +const fn class(class: &'static str, family: &'static str) -> BillableClass { + BillableClass { + class: abi_str(class), + family: abi_str(family), + } +} + +/// The six classes a session counts in (`cached_tokens` is attribution only, never billed), then +/// the session's fee unit: `per_session`, reported `1` once the far end first answers the session +/// and never before, so a session whose open failed refunds its fee (ARCHITECT Q-L5-FEE (A), Q17-6; +/// the fee unit is a billable class reported as a count of 0 or 1 and never ledgered, Q17-5 (c)). +const BILLABLE_CLASSES: &[BillableClass] = &[ + class(meta::CLASS_AUDIO_TOKENS_IN.as_str(), TOKEN_FAMILY), + class(meta::CLASS_AUDIO_TOKENS_OUT.as_str(), TOKEN_FAMILY), + class(meta::CLASS_TEXT_TOKENS_IN.as_str(), TOKEN_FAMILY), + class(meta::CLASS_TEXT_TOKENS_OUT.as_str(), TOKEN_FAMILY), + class(meta::CLASS_AUDIO_SECONDS_IN.as_str(), "duration"), + class(meta::CLASS_TOOL_CALLS.as_str(), "count"), + class(PER_SESSION, "count"), +]; + +const FEE_UNITS: &[AbiStr] = &[abi_str(PER_SESSION)]; + +const EGRESS_TARGETS: &[AbiStr] = &[abi_str(EGRESS_TARGET)]; + +const fn need(direction: u32, transport: &'static str, auth: AbiStr, target: AbiStr) -> Need { + Need { + direction, + egress_class: 0, + transport: abi_str(transport), + auth, + target_from: target, + trust_from: NONE, + details: Blob { + ptr: ptr::null(), + len: 0, + fmt: BLOB_ABSENT, + flags: 0, + }, + keep_response_headers: ptr::null(), + keep_response_headers_len: 0, + timeout_ms: 0, + keep_mode: KEEP_NAMED, + _reserved: 0, + deny_response_headers: ptr::null(), + deny_response_headers_len: 0, + } +} + +/// One outbound need: to a configured provider, its dials judged in the provider egress class, its +/// address the member's own (the `base_url` of the catalog model `streams.session.model` names, sealed +/// by the kernel's egress), never a config path of the plane's: a model name is no dial target. +const fn outbound(transport: &'static str, style: &'static str) -> Need { + Need { + egress_class: EGRESS_PROVIDER, + ..need(DIRECTION_OUTBOUND, transport, abi_str(style), NONE) + } +} + +/// The plane's connection needs: its keyed doors and its telephony door inbound; outbound, to the +/// upstream `streams.session.model` names, OpenAI Realtime's socket and its one-shot passes (the +/// mint, the SDP offer) under its bearer style, and Gemini Live's socket under its key header +/// (ARCHITECT Q-L5B-NEEDS: every outbound need declares its auth; the kernel binds each a member's +/// style names, one per (transport, auth), and a far request names the one it rides). +const NEEDS: &[Need] = &[ + need(DIRECTION_INBOUND, WS_TRANSPORT, abi_str(KEY_AUTH), NONE), + need(DIRECTION_INBOUND, HTTP_TRANSPORT, abi_str(KEY_AUTH), NONE), + need( + DIRECTION_INBOUND, + WS_TRANSPORT, + abi_str(SIGNATURE_AUTH), + NONE, + ), + outbound(WS_TRANSPORT, REALTIME_STYLE), + outbound(HTTP_TRANSPORT, REALTIME_STYLE), + outbound(WS_TRANSPORT, LIVE_STYLE), +]; + +/// The need a far request rides, as `OnPieceOut::need` names it (its index in [`NEEDS`] plus one): +/// OpenAI Realtime's socket. +pub const RIDES_REALTIME_SOCKET: u32 = 4; +/// OpenAI Realtime's one-shot passes (the mint, the SDP offer). +pub const RIDES_REALTIME_PASS: u32 = 5; +/// Gemini Live's socket. +pub const RIDES_LIVE_SOCKET: u32 = 6; + +/// THE STATEMENT TAIL. +pub const TAIL: &PlaneTail = &PlaneTail { + head: KindTailHead { + size: size_of::() as u32, + _reserved: 0, + }, + flags: 0, + ingress: INGRESS_REQUEST_RESPONSE | INGRESS_DUPLEX_SESSION, + dispatch_shape: SHAPE_PIECEWISE, + _reserved: 0, + scope: abi_str(SCOPE), + label: abi_str(LABEL), + subject_noun: abi_str(SUBJECT_NOUN), + admin_noun: abi_str(ADMIN_NOUN), + audit_kind: abi_str(AUDIT_KIND), + signing_domain: NONE, + signing_kid_prefix: NONE, + cli_help: NONE, + dialects: DIALECTS.as_ptr(), + dialects_len: DIALECTS.len(), + dialect_auth: DIALECT_AUTH.as_ptr(), + dialect_auth_len: DIALECT_AUTH.len(), + scope_kinds: SCOPE_KINDS.as_ptr(), + scope_kinds_len: SCOPE_KINDS.len(), + op_classes: OP_CLASSES.as_ptr(), + op_classes_len: OP_CLASSES.len(), + billable_classes: BILLABLE_CLASSES.as_ptr(), + billable_classes_len: BILLABLE_CLASSES.len(), + route_cost: ptr::null(), + route_cost_len: 0, + fee_units: FEE_UNITS.as_ptr(), + fee_units_len: FEE_UNITS.len(), + record_kinds: ptr::null(), + record_kinds_len: 0, + egress_targets: EGRESS_TARGETS.as_ptr(), + egress_targets_len: EGRESS_TARGETS.len(), + record_chains: ptr::null(), + record_chains_len: 0, + trust_keys: ptr::null(), + trust_keys_len: 0, + refusal_statuses: ptr::null(), + refusal_statuses_len: 0, + caller_credential_refusal: NONE, + admin_routes: ptr::null(), + admin_routes_len: 0, + admin_openapi: Blob::ABSENT, + // No stream ceiling (ARCHITECT STREAM-CEILING, 2026-10-07): a live session runs as long as its + // caller keeps it open, so the streaming plane states none; a streamed answer's send runs under + // the deployment's ceiling. + stream_ceiling_secs: 0, +}; + +/// The inbound auth style of a door read without a credential (no door of the plane is, today). +pub const NO_AUTH: &str = "none"; + +/// One door a live session is opened through: one line of this plane on a listener's guest list +/// (`BUSBAR-1.6.0.md` section 6, "Auth points and guest lists", step 3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Route { + /// The method set: every door takes one method. + pub verb: &'static str, + /// The path: exact, or a pattern where `{…}` is one path segment. No door is a prefix, and none + /// carries a field-presence predicate: no two doors share a path. + pub target: &'static str, + /// The transport the line's bytes are read by. + pub carrier: &'static str, + /// `true` for an upgrade line: the listener's transport runs the line's auth at `Head` on the + /// upgrade request, then the connection is handed to [`Route::carrier`]. + pub upgrade: bool, + /// The default inbound auth style: applied only where the operator's config gives the line no + /// auth. + pub auth: &'static str, + /// The dialect the line's units speak, by its index in the tail's dialects. + pub dialect: u32, + /// The dialect a refusal on this line is rendered in, by the same index. + pub refusal_dialect: u32, +} + +impl Route { + /// Read without a credential. + #[must_use] + pub const fn open(&self) -> bool { + let a = self.auth.as_bytes(); + let n = NO_AUTH.as_bytes(); + if a.len() != n.len() { + return false; + } + let mut i = 0; + while i < a.len() { + if a[i] != n[i] { + return false; + } + i += 1; + } + true + } + + /// `true` for an exact path, `false` for a pattern. + #[must_use] + pub const fn exact(&self) -> bool { + let b = self.target.as_bytes(); + let mut j = 0; + while j < b.len() { + if b[j] == b'{' { + return false; + } + j += 1; + } + true + } +} + +/// The doors, in the order a snapshot claims them: the ephemeral-secret mint and the SDP offer (one +/// request each), the browser's sideband socket, the Gemini Live socket and the telephony socket. +/// The three sockets are upgrade lines. The protected-resource metadata path a refused caller is +/// pointed at is NOT claimed (1.5.5 parity, ARCHITECT 2026-10-04: no accepted difference): the +/// data listener's fallback answers it as it did. +pub const ROUTES: &[Route] = &[ + Route { + verb: "POST", + target: "/v1/realtime/client_secrets", + carrier: HTTP_TRANSPORT, + upgrade: false, + auth: KEY_AUTH, + dialect: 0, + refusal_dialect: 0, + }, + Route { + verb: "POST", + target: "/v1/realtime/calls", + carrier: HTTP_TRANSPORT, + upgrade: false, + auth: KEY_AUTH, + dialect: 0, + refusal_dialect: 0, + }, + Route { + verb: "GET", + target: "/v1/realtime/sideband/{call_id}", + carrier: WS_TRANSPORT, + upgrade: true, + auth: KEY_AUTH, + dialect: 0, + refusal_dialect: 0, + }, + Route { + verb: "GET", + target: "/v1/realtime/gemini/{call_id}", + carrier: WS_TRANSPORT, + upgrade: true, + auth: KEY_AUTH, + dialect: 1, + refusal_dialect: 1, + }, + Route { + verb: "GET", + target: "/twilio/{call_id}", + carrier: WS_TRANSPORT, + upgrade: true, + auth: SIGNATURE_AUTH, + dialect: 2, + refusal_dialect: 2, + }, +]; + +impl Route { + /// The door as a generation snapshot claims it. + #[must_use] + pub fn claim(&self) -> ClaimSpec { + let flags = + if self.exact() { CLAIM_EXACT } else { 0 } | if self.open() { CLAIM_OPEN } else { 0 }; + ClaimSpec { + refusal_dialect: self.refusal_dialect as u16, + ..ClaimSpec::new(self.verb, self.target, self.carrier, flags) + } + } +} + +/// Judge a settings blob with the plane's grammar; an empty blob is the default section. +/// +/// # Errors +/// The grammar's reason when the blob is not a `streams:` section. +pub fn read_settings(settings: &[u8]) -> Result { + if settings.is_empty() { + return Ok(StreamsCfg::default()); + } + serde_json::from_slice::(settings).map_err(|e| e.to_string()) +} + +/// THE SECTION A DEALT `validate` BLOB CARRIES. The boot's stage-3g deal hands a declared-verb +/// plugin `{:
}` (`config_validate::deal`), where `open` and `refresh` are handed the +/// section itself; the one verb this plane declares is [`SECTION`], so a blob that is exactly +/// `{streams: …}` is unwrapped to its section and any other blob is the section already. +#[must_use] +pub fn dealt_section(settings: &[u8]) -> std::borrow::Cow<'_, [u8]> { + match serde_json::from_slice::>(settings) { + Ok(map) if map.len() == 1 => match map.get(SECTION) { + Some(section) => serde_json::to_vec(section).map_or( + std::borrow::Cow::Borrowed(settings), + std::borrow::Cow::Owned, + ), + None => std::borrow::Cow::Borrowed(settings), + }, + _ => std::borrow::Cow::Borrowed(settings), + } +} + +/// ONE READING of the public base URL: parse it and REPLACE the path wholesale, dropping query and +/// fragment, so a public URL with a path of its own cannot produce a second spelling. +#[must_use] +pub fn absolute(public_url: &str, path: &str) -> Option { + let mut u = url::Url::parse(public_url).ok()?; + u.set_path(path); + u.set_query(None); + u.set_fragment(None); + Some(u.to_string()) +} + +/// One generation's answer: the strings its snapshot names, read once from the public base URL. +pub struct Generation { + generation: u64, + audience: String, + resource_metadata: String, +} + +impl Generation { + /// The generation `generation` of a plane whose deployment states `public_url`. + #[must_use] + pub fn build(generation: u64, public_url: Option<&str>) -> Self { + let (audience, resource_metadata) = public_url + .and_then(|p| Some((absolute(p, MOUNT_PATH)?, absolute(p, METADATA_PATH)?))) + .unwrap_or_default(); + Generation { + generation, + audience, + resource_metadata, + } + } + + /// Its generation. + #[must_use] + pub fn generation(&self) -> u64 { + self.generation + } + + /// The audience a caller's token must carry; empty = no receiving side. + #[must_use] + pub fn audience(&self) -> &str { + &self.audience + } + + /// The metadata URL a refused caller is pointed at; empty = no receiving side. + #[must_use] + pub fn resource_metadata(&self) -> &str { + &self.resource_metadata + } + + /// Its snapshot as the plane publishes it: every door claimed and the audience and metadata + /// URL bound, or, with no receiving side, nothing claimed. + #[must_use] + pub fn snapshot(&self) -> SnapshotSpec { + if self.audience.is_empty() { + return SnapshotSpec::default(); + } + SnapshotSpec { + claims: ROUTES.iter().map(Route::claim).collect(), + audience: Some(self.audience.clone()), + resource_metadata: Some(self.resource_metadata.clone()), + ..SnapshotSpec::default() + } + } +} + +/// The plane's refusal code for an arrival on a claim it does not publish: no door of the plane +/// serves it (a 404). +pub const REFUSAL_NO_DOOR: u32 = 1; + +/// The most units an instance keeps state for at once; past it, the oldest is dropped first. +pub const MAX_UNITS: usize = 4096; + +/// One unit on a one-request door, kept from its first piece to the caller's answer. +struct Held { + /// The plane's answers to the unit's pieces. + unit: RequestUnit, + /// What the unit's answer still owes the reply buffer. + owed: Owed, + /// An answer that did not fit its field or arena buffer: the re-call carries the same piece, + /// which is answered from here rather than read twice. + short: Option, +} + +/// One open plane instance. +pub struct Plane { + /// The deployment's public base URL, as `open` read it; a refresh keeps it. + public_url: Option, + /// The live generations' settings and answers, oldest first (control lane only). + generations: Mutex>, + /// The published snapshots, held by the SDK until their generation's `retire`. + snapshots: Generations, + /// The request units in flight, by the kernel's unit key. + units: Mutex>, + /// The live sessions, by the kernel's stream. + sessions: Mutex, + /// The host's wake, as `open` handed it; `None` = no session output can be named. + wake: Option, + /// The instance's driver ticket, as its last `tick` was handed it, and that tick's clock + /// reading in milliseconds. + driver: Mutex<(Ticket, u64)>, +} + +impl Plane { + /// A unit arriving on claim `claim`, under the newest live generation's session params, naming + /// its caller by `caller_ref`. `None` for a claim the plane never published, and for a door + /// that opens a live session. + fn held(&self, claim: u32, caller_ref: Option) -> Option { + let door = Door::of(claim)?; + let generations = lock(&self.generations); + let (cfg, _) = generations.last()?; + let unit = RequestUnit::open(door, cfg.session.clone(), caller_ref)?; + Some(Held { + unit, + owed: Owed::default(), + short: None, + }) + } + + /// The newest live generation's section; `None` before the first publish. + fn newest(&self) -> Option { + lock(&self.generations).last().map(|(cfg, _)| cfg.clone()) + } + + /// Whether any live generation configures a session wall-clock ceiling. + fn any_ceiling(&self) -> bool { + lock(&self.generations) + .iter() + .any(|(cfg, _)| cfg.session_max_secs.is_some()) + } + + /// Name the instance's driver ticket: a session owes output of its own. + fn wake_driver(&self) { + let (ticket, _) = *lock(&self.driver); + if let (Some(wake), false) = (self.wake, ticket == Ticket::NONE) { + wake.wake(ticket); + } + } + + /// Publish generation `generation` into the `out` field `pick` names. + fn publish( + &self, + out: &mut Out<'_, T>, + pick: impl FnOnce(&T) -> &*const PlaneSnapshot, + generation: u64, + cfg: StreamsCfg, + ) { + let g = Generation::build(generation, self.public_url.as_deref()); + out.publish(pick, &self.snapshots, generation, &g.snapshot()); + lock(&self.generations).push((cfg, g)); + } +} + +fn lock(m: &Mutex) -> std::sync::MutexGuard<'_, T> { + m.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// One slot body on the SDK's safe surface over the instance's [`Plane`]. +macro_rules! slot { + ($name:ident, $in:ty, $out:ty, |$inst:pat_param, $input:pat_param, $o:ident| $body:block) => { + struct $name; + impl SafeSlot for $name { + type In = $in; + type Out = $out; + type State = Plane; + fn call( + $inst: Instance<'_, Plane>, + $input: Lent<'_, $in>, + #[allow(unused_mut)] mut $o: Out<'_, $out>, + ) -> Outcome { + $body + } + } + }; +} + +slot!(Validate, ValidateIn, OutHead, |_, input, out| { + match read_settings(&dealt_section(input.field(|i| &i.settings).bytes())) { + Ok(_) => Outcome::Ready, + Err(words) => out.fail(busbar_contract::abi::sdk::life::Refusal::refused(words)), + } +}); + +slot!(Open, PlaneOpenIn, PlaneOpenOut, |instance, input, out| { + let Ok(cfg) = read_settings(input.field(|i| &i.open.settings).bytes()) else { + return Outcome::Refused; + }; + let public_url = input + .field(|i| &i.public_url) + .as_str() + .ok() + .filter(|s| !s.is_empty()) + .map(str::to_owned); + let wake = input.field(|i| &i.open).host().and_then(|h| Wake::of(&h)); + let p = Plane { + public_url, + generations: Mutex::new(Vec::new()), + snapshots: Generations::new(), + units: Mutex::new(BTreeMap::new()), + sessions: Mutex::new(Sessions::default()), + wake, + driver: Mutex::new((Ticket::NONE, 0)), + }; + p.publish(&mut out, |o| &o.snapshot, input.open.generation, cfg); + instance.open(p); + Outcome::Ready +}); + +slot!( + Refresh, + RefreshIn, + PlaneRefreshOut, + |instance, input, out| { + let Some(p) = instance.get() else { + return Outcome::Failed; + }; + let Ok(cfg) = read_settings(input.field(|i| &i.settings).bytes()) else { + return Outcome::Refused; + }; + p.publish(&mut out, |o| &o.snapshot, input.generation, cfg); + Outcome::Ready + } +); + +slot!(Retire, GenIn, OutHead, |instance, input, _out| { + if let Some(p) = instance.get() { + lock(&p.generations).retain(|(_, g)| g.generation() != input.generation); + p.snapshots.retire(input.generation); + } + Outcome::Ready +}); + +// The tick: the instance's driver ticket is noted, every live session's clock and ceiling read on +// the tick clock, and a session that now owes its end names the driver ticket. While a live +// generation configures a ceiling the door asks to be ticked every `CEILING_TICK_NS`. +slot!(Tick, TickIn, TickOut, |instance, input, out| { + let given = input.get(); + let next = match instance.get() { + Some(p) => { + *lock(&p.driver) = (given.head.ticket, given.now_ns / 1_000_000); + if lock(&p.sessions).tick(given.now_ns) { + p.wake_driver(); + } + if p.any_ceiling() { + given.now_ns.saturating_add(CEILING_TICK_NS) + } else { + 0 + } + } + None => 0, + }; + out.set(|o| &o.next_tick_ns, next); + Outcome::Ready +}); + +// `drive`: the sessions with output of their own, by stream, as many as the host's buffer takes. +slot!( + Drive, + PlaneDriveIn, + PlaneDriveOut, + |instance, input, out| { + let Some(p) = instance.get() else { + return Outcome::Ready; + }; + let sessions = lock(&p.sessions); + let mut buf = input.sessions_buf(); + for stream in sessions.ready(usize::MAX) { + buf.push(stream); + } + if !buf.fits() { + out.set(|o| &o.sessions_needed, buf.needed() as u32); + return Outcome::Failed; + } + out.set(|o| &o.sessions_written, buf.written() as u32); + Outcome::Ready + } +); + +// `cancel`: an op on a session's ticket ends the session (each side tells the other); a request +// unit's cancel is declined. It writes no record. +slot!( + Cancel, + PlaneCancelIn, + PlaneCancelOut, + |instance, input, out| { + if let Some(p) = instance.get() { + lock(&p.sessions).cancel(input.get().cancel.ticket); + } + out.set(|o| &o.cancel.disposition, CANCEL_FAILED); + Outcome::Ready + } +); + +slot!(Release, ReleaseIn, OutHead, |_, _, _out| { Outcome::Ready }); + +slot!(Close, InHead, OutHead, |_, _, _out| { Outcome::Ready }); + +// A claimed arrival is classified: its door's dialect, the session open, a known principal. No +// admission estimate: a session's units are what the far end reports. A door that reaches the far end +// names its route: the DIRECT entry `streams.session.model` (ARCHITECT Q-L5B-ROUTE: the composition +// folds that top-level catalog model into the plane's route table, so the kernel's door steps resolve +// it as any section entry). The metadata door reaches none and names none. +slot!(Arrive, ArriveIn, ArriveOut, |instance, input, out| { + let Some(a) = crate::driven::arrive(input.claim) else { + out.set(|o| &o.refusal, REFUSAL_NO_DOOR); + out.set(|o| &o.refusal_status, 404); + return Outcome::Refused; + }; + let mut units = input.units_buf(); + for (class, amount) in a.door.admit() { + units.push(UnitCount { + class, + source: UNITS_ESTIMATED, + amount, + }); + } + if !units.fits() { + out.set(|o| &o.units_needed, units.needed() as u32); + return Outcome::Failed; + } + out.set(|o| &o.units_written, units.written() as u32); + out.set(|o| &o.op_class, a.op_class); + out.set(|o| &o.dialect, a.dialect); + out.set(|o| &o.principal_need, a.door.authenticate()); + let model = (!a.door.is_open()) + .then(|| instance.get().and_then(Plane::newest)) + .flatten() + .and_then(|cfg| cfg.session.model); + if let Some(model) = model { + out.route(ROUTE_DIRECT, &model); + } + Outcome::Ready +}); + +/// The piece `input` carries, in the plane's own vocabulary; `None` for a source the kind does not +/// name. `head` holds the far end's kept response fields, read on its first piece. +fn piece_of<'h, 'a: 'h>( + input: Lent<'a, OnPieceIn>, + head: &'h mut Vec<(&'a [u8], &'a [u8])>, +) -> Option> { + let given = input.get(); + let has_status = given.flags & PIECE_HAS_STATUS != 0; + let from = match given.from { + FROM_CALLER => request_unit::From::Caller, + FROM_FAR_END => request_unit::From::FarEnd, + FROM_KERNEL => request_unit::From::Kernel(given.attempt_no), + _ => return None, + }; + if has_status { + head.extend( + input + .head_fields() + .iter() + .map(|f| (f.field(|f| &f.name).bytes(), f.field(|f| &f.value).bytes())), + ); + } + Some(Piece { + from, + bytes: input.field(|i| &i.bytes).bytes(), + status: has_status.then(|| u16::try_from(given.status_code).unwrap_or(0)), + last: given.flags & PIECE_LAST != 0, + head: head.as_slice(), + }) +} + +/// Write the head fields `pairs` (name, value) into the host's field buffer over its arena. +fn push_fields( + fields: &mut HostBuf<'_, OutField>, + arena: &mut HostBuf<'_, u8>, + pairs: &[(&'static str, String)], +) { + for (name, value) in pairs { + fields.push(OutField { + name: arena.span(name.as_bytes()), + value: arena.span(value.as_bytes()), + }); + } +} + +/// Write the plane's `answer` for `held` into the host's buffers. Answers the outcome and whether +/// the unit is done. +fn answer_piece( + held: &mut Held, + answer: Answer, + input: Lent<'_, OnPieceIn>, + out: &mut Out<'_, OnPieceOut>, +) -> (Outcome, bool) { + match answer { + Answer::Nothing => (Outcome::Ready, false), + Answer::Refused => (Outcome::Refused, true), + Answer::ToFarEnd(bytes) => { + held.owed.owe(&bytes, EMIT_TO_FAR_END, input, out); + (Outcome::Ready, false) + } + Answer::Attempt(attempt) => { + let (mut fields, units, mut arena) = + (input.fields_buf(), input.units_buf(), input.arena_buf()); + let verb = arena.span(attempt.verb.as_bytes()); + let target = arena.span(attempt.target.as_bytes()); + push_fields(&mut fields, &mut arena, &attempt.fields); + if piece::settle(out, &fields, &units, &arena) { + held.short = Some(Answer::Attempt(attempt)); + return (Outcome::Failed, false); + } + out.set(|o| &o.verb, verb); + out.set(|o| &o.target, target); + out.set(|o| &o.need, RIDES_REALTIME_PASS); + held.owed.owe(&attempt.body, EMIT_TO_FAR_END, input, out); + (Outcome::Ready, false) + } + Answer::ToCaller(reply) => { + let (mut fields, units, mut arena) = + (input.fields_buf(), input.units_buf(), input.arena_buf()); + // A one-request door's answer is WHOLE: its head states its length, so the caller is + // answered under it, never chunked (1.6.0-pre's bytes; the serve path sends a whole + // answer under the length its head states). + let mut head = reply.fields.clone(); + head.push((FIELD_CONTENT_LENGTH, reply.body.len().to_string())); + push_fields(&mut fields, &mut arena, &head); + if piece::settle(out, &fields, &units, &arena) { + held.short = Some(Answer::ToCaller(reply)); + return (Outcome::Failed, false); + } + out.set(|o| &o.reply_status, u32::from(reply.status)); + let done = held.owed.owe(&reply.body, EMIT_DONE, input, out); + (Outcome::Ready, done) + } + } +} + +/// A live session's piece (`stream != 0`), answered by its [`crate::session_door::Live`]: the +/// caller's and the far end's pieces go through the session, a collection (`FROM_KERNEL`, no +/// attempt) answers its next queued frame, and the turn's ATTEMPT carries nothing of its own (the +/// turn's request is the caller side's answer). Every answer reports the session's cumulative +/// units and writes at most one frame; a session still owing output names the driver ticket. +fn session_piece(p: &Plane, input: Lent<'_, OnPieceIn>, out: &mut Out<'_, OnPieceOut>) -> Outcome { + let given = input.get(); + let Some(door) = Door::of(given.claim).filter(|d| d.is_session()) else { + return Outcome::Refused; + }; + let Some(cfg) = p.newest() else { + return Outcome::Refused; + }; + let now_ms = lock(&p.driver).1; + let mut sessions = lock(&p.sessions); + // Only the caller's piece opens a session: a far-end piece or a collection for a stream the + // door holds nothing for (an ended or cancelled session) is refused, never a fresh session. + let live = if given.from == FROM_CALLER { + sessions.get_or_open(given.stream, door, &cfg) + } else { + sessions.get(given.stream) + }; + if live.is_none() { + return Outcome::Refused; + } + sessions.crossed(given.stream, given.head.ticket); + let Some(live) = sessions.get(given.stream) else { + return Outcome::Refused; + }; + let near = given.from != FROM_FAR_END; + // A re-call after a `more = 1` answer pays what that side's frame still owes. + let owed = if near { &mut live.near } else { &mut live.far }; + if piece::is_recall(input) && owed.pending() { + owed.pay(input, out); + return Outcome::Ready; + } + let short = if near { + live.short_near.take() + } else { + live.short_far.take() + }; + let emit = match short { + Some(emit) => emit, + None => { + let caller_last = given.from == FROM_CALLER && given.flags & PIECE_LAST != 0; + let bytes = input.field(|i| &i.bytes).bytes(); + match given.from { + FROM_CALLER => live.from_caller(bytes, caller_last), + FROM_FAR_END => live.from_far_end(bytes, now_ms), + FROM_KERNEL => {} + _ => return Outcome::Refused, + } + // The caller's last piece ends the session: what it still owed either side has no + // one to go to. A turn's ATTEMPT carries nothing of its own. + if caller_last { + crate::session_door::Emit { + frame: None, + done: true, + } + } else if given.from == FROM_KERNEL && given.attempt_no > 0 { + crate::session_door::Emit::default() + } else { + live.next(near) + } + } + }; + let (fields, mut units, mut arena) = (input.fields_buf(), input.units_buf(), input.arena_buf()); + for (class, amount) in live.units() { + units.push(UnitCount { + class, + source: UNITS_REPORTED, + amount, + }); + } + let target = match emit.frame { + Some((Side::FarEnd, _)) => door.route(&cfg.session, None, &[]).ok().flatten(), + _ => None, + }; + let verb = target.as_ref().map(|a| arena.span(a.verb.as_bytes())); + let path = target.as_ref().map(|a| arena.span(a.target.as_bytes())); + if piece::settle(out, &fields, &units, &arena) { + // Short as a whole: the re-call carries the same piece, answered from here. + if near { + live.short_near = Some(emit); + } else { + live.short_far = Some(emit); + } + return Outcome::Failed; + } + let done = match emit.frame { + Some((Side::FarEnd, frame)) => { + if let (Some(verb), Some(path)) = (verb, path) { + out.set(|o| &o.verb, verb); + out.set(|o| &o.target, path); + } + out.set( + |o| &o.need, + if door == Door::Gemini { + RIDES_LIVE_SOCKET + } else { + RIDES_REALTIME_SOCKET + }, + ); + // A realtime dialect's frame is a JSON text message on its socket. + live.near + .owe(&frame, EMIT_TO_FAR_END | PIECE_OUT_TEXT, input, out); + false + } + Some((Side::Caller, frame)) => { + let owed = if near { &mut live.near } else { &mut live.far }; + owed.owe(&frame, PIECE_OUT_TEXT, input, out); + false + } + None if emit.done => { + out.set(|o| &o.flags, EMIT_DONE); + true + } + None => false, + }; + let owes = live.ready(); + if done { + sessions.close(given.stream); + } else if owes { + drop(sessions); + p.wake_driver(); + } + Outcome::Ready +} + +// `on_piece`: a one-request door's pieces (the mint, the SDP offer), each +// answered by the unit's [`RequestUnit`]: the ATTEMPT's request with its body, the caller's body to +// the far end, and the caller's answer from the far end's. A finished or refused unit is forgotten. +// A piece that names a stream is a live session's ([`session_piece`]). +slot!(OnPiece, OnPieceIn, OnPieceOut, |instance, input, out| { + let Some(p) = instance.get() else { + return Outcome::Failed; + }; + let given = input.get(); + if given.stream != 0 { + return session_piece(p, input, &mut out); + } + let mut units = lock(&p.units); + if piece::is_recall(input) { + if let Some(held) = units.get_mut(&given.unit) { + if held.owed.pending() { + if held.owed.pay(input, &mut out) { + units.remove(&given.unit); + } + return Outcome::Ready; + } + } + } + if !units.contains_key(&given.unit) { + let caller_ref = input + .field(|i| &i.caller_ref) + .as_str() + .ok() + .filter(|r| !r.is_empty()) + .map(str::to_owned); + let Some(held) = p.held(given.claim, caller_ref) else { + return Outcome::Refused; + }; + while units.len() >= MAX_UNITS { + if units.pop_first().is_none() { + break; + } + } + units.insert(given.unit, held); + } + let Some(held) = units.get_mut(&given.unit) else { + return Outcome::Failed; + }; + let answer = match held.short.take() { + Some(answer) => answer, + None => { + let mut head = Vec::new(); + let Some(piece) = piece_of(input, &mut head) else { + return Outcome::Refused; + }; + held.unit.on_piece(piece) + } + }; + let (outcome, done) = answer_piece(held, answer, input, &mut out); + if done { + units.remove(&given.unit); + } + outcome +}); + +// The refusal render, the admin serve and the hook projection are declined: a declined unit is +// charged nothing. + +/// A refusal in the realtime dialects' error shape (the OpenAI REST error envelope, which both +/// the session doors and the one-request doors answer a refused open in): the kernel's status read +/// as the error's type, its text as the message. +#[must_use] +pub fn refusal_body(status: u16, text: &str) -> Vec { + error_envelope(status, text, None) +} + +/// The message the realtime dialects answer a bad or missing credential with: the vendor's own copy, +/// as 1.5.5's residual answered a refused credential on these targets. +pub const AUTH_FAILURE_MESSAGE: &str = "Incorrect API key provided."; + +/// The error code the realtime dialects answer a bad or missing credential with, beside +/// [`AUTH_FAILURE_MESSAGE`]. +pub const AUTH_FAILURE_CODE: &str = "invalid_api_key"; + +/// A refusal the kernel hands the door, rendered by its reason's class (the one classification, +/// `busbar_contract::abi::plane`): an authentication refusal reads in the dialects' own vendor terms +/// ([`AUTH_FAILURE_MESSAGE`], [`AUTH_FAILURE_CODE`]), never the kernel's word, which would be a +/// busbar tell; every other refusal is [`refusal_body`], the kernel's status and text. A refusal with +/// no unit (the kernel authenticates before `arrive`) is the 1.5.5 residual's answer for the target. +#[must_use] +pub fn refusal_render(status: u16, reason: u32, text: &str) -> Vec { + let authentication = + reason_of(reason).is_some_and(|r| class_of(r) == RefusalClass::Unauthenticated); + if authentication { + error_envelope(status, AUTH_FAILURE_MESSAGE, Some(AUTH_FAILURE_CODE)) + } else { + refusal_body(status, text) + } +} + +/// The OpenAI REST error envelope: `status` read as the error's type, `message` and `code` as given. +fn error_envelope(status: u16, message: &str, code: Option<&str>) -> Vec { + let kind = match status { + 401 => "authentication_error", + 403 => "permission_error", + 404 => "not_found_error", + 429 => "rate_limit_error", + 400..=499 => "invalid_request_error", + _ => "server_error", + }; + serde_json::to_vec(&serde_json::json!({ + "error": {"message": message, "type": kind, "param": null, "code": code} + })) + .unwrap_or_default() +} + +/// The response field naming a refusal body's document type. +const FIELD_CONTENT_TYPE: &str = "content-type"; +/// A refusal body's document type. +const CONTENT_TYPE_JSON: &str = "application/json"; + +// `refusal`: the kernel's status and text (a hook's veto, an admission's refusal) in the dialects' +// error shape, as JSON, before any upgrade. +slot!(Refusal, RefusalIn, RefusalOut, |_, input, out| { + let given = input.get(); + let status = u16::try_from(given.status).unwrap_or(0); + let text = input.field(|i| &i.text).as_str().unwrap_or_default(); + let body = refusal_render(status, given.reason, text); + let (mut reply, mut fields, mut arena) = + (input.reply_buf(), input.fields_buf(), input.arena_buf()); + reply.extend(&body); + fields.push(OutField { + name: arena.span(FIELD_CONTENT_TYPE.as_bytes()), + value: arena.span(CONTENT_TYPE_JSON.as_bytes()), + }); + let short = !(reply.fits() && fields.fits() && arena.fits()); + let (rw, rnd) = reply.settle(short); + let (fw, fnd) = fields.settle(short); + let (aw, and) = arena.settle(short); + out.set(|o| &o.reply_written, rw as u64); + out.set(|o| &o.reply_needed, rnd as u64); + out.set(|o| &o.fields_written, fw as u32); + out.set(|o| &o.fields_needed, fnd as u32); + out.set(|o| &o.arena_written, aw as u64); + out.set(|o| &o.arena_needed, and as u64); + if short { + Outcome::Failed + } else { + Outcome::Ready + } +}); + +slot!(Serve, ServeIn, ServeOut, |_, _, _out| { Outcome::Refused }); + +slot!(Hydrate, GenIn, OutHead, |_, _, _out| { Outcome::Ready }); + +slot!(Start, GenIn, OutHead, |_, _, _out| { Outcome::Ready }); + +// `project` (ARCHITECT Q-L5B-PROJECT; K5): the hook view of a unit's open, once, when a hook is +// bound. A session door projects the session-open params it locks (the telephony door's µ-law, else +// the section's session) as the view's body, the session-open the hooks screen; a hook's rewrite is +// patched over them ([`committed_session_config`]), kept for the session the unit opens, and THAT is +// projected and answered. A rewrite that does not read back refuses the open. A one-request door +// projects its view with no body, and takes no rewrite. +slot!(Project, ProjectIn, ProjectOut, |instance, input, out| { + let Some(p) = instance.get() else { + return Outcome::Failed; + }; + let given = input.get(); + let Some(door) = Door::of(given.claim) else { + return Outcome::Refused; + }; + let Some(cfg) = p.newest() else { + return Outcome::Refused; + }; + let rewrite = input.field(|i| &i.rewrite).bytes(); + let (body, rewritten) = if door.is_session() { + let locked = Live::locked(door, &cfg); + let params = if rewrite.is_empty() { + locked + } else { + match committed_session_config(&locked, rewrite) { + Ok(params) => { + lock(&p.sessions).rewrite(given.unit, params.clone()); + params + } + Err(_) => return Outcome::Refused, + } + }; + let Ok(body) = serde_json::to_vec(¶ms) else { + return Outcome::Refused; + }; + (Some(body), !rewrite.is_empty()) + } else if rewrite.is_empty() { + (None, false) + } else { + return Outcome::Refused; + }; + let (signals, mut arena, turns) = + (input.signals_buf(), input.arena_buf(), input.messages_buf()); + let dialect = DIALECTS_BY_NAME + .get(door.dialect() as usize) + .map(|d| arena.span(d.as_bytes())); + let pool = cfg + .session + .model + .as_deref() + .map(|m| arena.span(m.as_bytes())); + let body = body.as_deref().map(|b| arena.span(b)); + let short = !(signals.fits() && arena.fits() && turns.fits()); + let (sw, snd) = signals.settle(short); + let (aw, and) = arena.settle(short); + let (tw, tnd) = turns.settle(short); + out.set(|o| &o.signals_needed, snd as u32); + out.set(|o| &o.arena_written, aw as u64); + out.set(|o| &o.arena_needed, and as u64); + out.set(|o| &o.messages_needed, tnd as u32); + let absent = Span { + offset: SPAN_ABSENT, + len: 0, + }; + out.set(|o| &o.body, absent); + out.set(|o| &o.rewritten, absent); + if short { + // The driver re-calls once with the buffers this named, the same rewrite with it: patching + // it again over the locked params writes the same body. + return Outcome::Failed; + } + if let Some(pool) = pool { + out.host_str(|o| &o.view.pool, &arena, pool); + } + if let Some(dialect) = dialect { + out.host_str(|o| &o.view.ingress_dialect, &arena, dialect); + } + out.set( + |o| &o.view.flags, + if door.is_session() { REQUEST_STREAM } else { 0 }, + ); + out.host_rows(|o| &o.view.signals, &signals); + out.set(|o| &o.view.signals_len, sw); + out.set(|o| &o.prompt.message_count, tw as u64); + out.host_rows(|o| &o.prompt.messages, &turns); + out.set(|o| &o.prompt.messages_len, tw); + if let Some(body) = body { + out.set(|o| &o.body, body); + if rewritten { + out.set(|o| &o.rewritten, body); + } + } + Outcome::Ready +}); + +busbar_contract::plugin_door! { + ops: busbar_contract::abi::plane::Ops, + statement: Statement { + kind_tail: ptr::from_ref(TAIL).cast::(), + sections: SECTIONS.as_ptr(), + sections_len: SECTIONS.len(), + needs: NEEDS.as_ptr(), + needs_len: NEEDS.len(), + ..statement(NAME, env!("CARGO_PKG_VERSION"), 64) + }, + lifecycle: { + validate: Safe, open: Safe, refresh: Safe, retire: Safe, + tick: Safe, drive: Safe, cancel: Safe, release: Safe, + close: Safe, + }, + kind_ops: { + arrive: Safe, on_piece: Safe, refusal: Safe, serve: Safe, + hydrate: Safe, start: Safe, project: Safe, + }, +} + +#[cfg(test)] +#[path = "tests/ladder_tests.rs"] +mod ladder_tests; + +const _: () = { + // The ride constants name the needs they say they name. + assert!(NEEDS[RIDES_REALTIME_SOCKET as usize - 1].direction == DIRECTION_OUTBOUND); + assert!(NEEDS[RIDES_REALTIME_PASS as usize - 1].direction == DIRECTION_OUTBOUND); + assert!(NEEDS[RIDES_LIVE_SOCKET as usize - 1].direction == DIRECTION_OUTBOUND); + assert!(DIALECTS.len() == DIALECTS_BY_NAME.len()); +}; diff --git a/crates/busbar-plane-streaming/src/driven.rs b/crates/busbar-plane-streaming/src/driven.rs new file mode 100644 index 0000000000..b46ac1d8fd --- /dev/null +++ b/crates/busbar-plane-streaming/src/driven.rs @@ -0,0 +1,417 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE PLANE'S ANSWERS TO THE KERNEL PLANE DRIVER for its two one-request doors (`BUSBAR-1.6.0.md` +//! Part 3, section 12): the ephemeral-secret mint and the SDP offer. Each answer is plain data, in +//! the plane's own vocabulary; writing it into the host's buffers is the door's job. +//! +//! | driver crossing | here | +//! |---|---| +//! | `arrive` | [`arrive`]: which door the claim is, its dialect and its principal need | +//! | `on_piece`, ATTEMPT | [`mint_attempt`], [`sdp_attempt`]: the request bound for the far end | +//! | `on_piece`, from the far end | [`mint_reply`], [`sdp_reply`]: what the caller is answered | +//! +//! The caller's texts are the served plane's, byte for byte, where the far end answered. A far end +//! that could not be reached is the kernel's route terminal and is not answered here. + +use crate::broker::{ + clamped_ttl_secs, mint_request_body, minted_answer, read_minted, rtc_call_id_of, CALLS_PATH, + CLIENT_SECRETS_PATH, SAFETY_IDENTIFIER_HEADER, SDP_CONTENT_TYPE, +}; +use crate::codec::ir::config::SessionConfig; + +/// The media type of a JSON body. +pub const JSON_CONTENT_TYPE: &str = "application/json"; +/// The head field naming a body's media type. +pub const FIELD_CONTENT_TYPE: &str = "content-type"; +/// The head field naming where a created call lives. +pub const FIELD_LOCATION: &str = "location"; +/// The head field a whole answer states its length in. +pub const FIELD_CONTENT_LENGTH: &str = "content-length"; + +/// The door a claim is, by its index in the snapshot's claims (`crate::door::ROUTES`). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Door { + /// `POST /v1/realtime/client_secrets`: one ephemeral secret for a browser session. + Mint, + /// `POST /v1/realtime/calls`: one SDP offer, relayed to the provider. + Sdp, + /// The browser's sideband socket. + Sideband, + /// The Gemini Live socket. + Gemini, + /// The telephony socket. + Twilio, +} + +impl Door { + /// The door of claim `claim`; `None` for a claim the plane never published. + #[must_use] + pub const fn of(claim: u32) -> Option { + match claim { + 0 => Some(Door::Mint), + 1 => Some(Door::Sdp), + 2 => Some(Door::Sideband), + 3 => Some(Door::Gemini), + 4 => Some(Door::Twilio), + _ => None, + } + } + + /// The door's line on the guest list: its claim (`crate::door::ROUTES`), in claim order. + #[must_use] + pub const fn line(self) -> crate::door::Route { + crate::door::ROUTES[self as usize] + } + + /// The door's dialect, by its index in the tail's dialects: its line's. + #[must_use] + pub const fn dialect(self) -> u32 { + self.line().dialect + } + + /// `true` for the door read without a credential. + #[must_use] + pub const fn is_open(self) -> bool { + self.line().open() + } + + /// `true` for a door that opens a live session rather than answering one request. + #[must_use] + pub const fn is_session(self) -> bool { + matches!(self, Door::Sideband | Door::Gemini | Door::Twilio) + } +} + +/// What `arrive` answers for a claimed request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Arrival { + /// The door. + pub door: Door, + /// Index into the tail's op classes: the session open, every door's one operation. + pub op_class: u32, + /// Index into the tail's dialects. + pub dialect: u32, +} + +/// `arrive` for claim `claim`: every keyed door opens a session (or brokers one) for a known +/// principal. +#[must_use] +pub const fn arrive(claim: u32) -> Option { + match Door::of(claim) { + Some(door) => Some(Arrival { + door, + op_class: 0, + dialect: door.dialect(), + }), + None => None, + } +} + +// ── the plane's answer at each loop step, in the order the kernel's loop runs them ───────────── + +/// THE PLANE'S ANSWER AT EACH LOOP STEP for a unit on one of its doors: what the kernel's loop asks +/// the plane at authenticate, verify, approve, admit, route, meter and audit. The door answers the +/// driver through these; nothing here reaches the kernel any other way. +pub trait Steps { + /// AUTHENTICATE: whom a unit on this door needs. + fn authenticate(&self) -> u32; + /// VERIFY: where, inside the plane's section, the destination a session dials is named. + fn verify(&self) -> &'static str; + /// APPROVE: the grant a key must hold, as (kind, value). + fn approve(&self) -> (&'static str, &'static str); + /// ADMIT: the units a unit is expected to cost before the far end reports any. + fn admit(&self) -> Vec<(u32, u64)>; + /// ROUTE: the request an attempt sends the far end; `None` when the door sends nothing. + /// + /// # Errors + /// The request body's serializer text. + fn route( + &self, + config: &SessionConfig, + caller_ref: Option<&str>, + body: &[u8], + ) -> Result, String>; + /// METER: a session's units so far, per billable class index, the zero classes left out. + fn meter(&self, units: &crate::session_unit::CumulativeUnits) -> Vec<(u32, u64)>; + /// AUDIT: the action a unit on this door is audited under. + fn audit(&self) -> &'static str; +} + +impl Steps for Door { + /// Every keyed door needs a principal; a door read without a credential (none today) answers + /// anyone. + fn authenticate(&self) -> u32 { + if self.is_open() { + busbar_contract::abi::plane::PRINCIPAL_NONE + } else { + busbar_contract::abi::plane::PRINCIPAL_REQUIRED + } + } + + /// The kernel resolves `streams.session.model` through the root models and judges it. + fn verify(&self) -> &'static str { + crate::door::EGRESS_TARGET + } + + /// The `session` kind, for the one pool every session is served on. + fn approve(&self) -> (&'static str, &'static str) { + (crate::door::SCOPE, crate::door::SESSION_POOL) + } + + /// None: a session is admitted at open and pays for what the far end reports (and its + /// per-session fee, which the kernel counts); the one-request doors report nothing. + fn admit(&self) -> Vec<(u32, u64)> { + match self { + Door::Mint | Door::Sdp | Door::Sideband | Door::Gemini | Door::Twilio => Vec::new(), + } + } + + /// The session doors dial the dialect's realtime socket (the credential is the kernel's, never + /// in the target); the mint and SDP doors send their one request. + fn route( + &self, + config: &SessionConfig, + caller_ref: Option<&str>, + body: &[u8], + ) -> Result, String> { + Ok(match self { + Door::Mint => Some(mint_attempt(config, caller_ref, None)?), + Door::Sdp => Some(sdp_attempt(body)), + Door::Sideband | Door::Twilio => Some(Attempt { + verb: "GET", + target: REALTIME_SOCKET_PATH, + fields: Vec::new(), + body: Vec::new(), + }), + Door::Gemini => Some(Attempt { + verb: "GET", + target: GEMINI_SOCKET_PATH, + fields: Vec::new(), + body: Vec::new(), + }), + }) + } + + /// Counts only; the kernel prices nothing the plane names. + fn meter(&self, units: &crate::session_unit::CumulativeUnits) -> Vec<(u32, u64)> { + units + .0 + .iter() + .enumerate() + .filter(|(_, n)| **n != 0) + .map(|(i, n)| (i as u32, *n)) + .collect() + } + + /// A session open. + fn audit(&self) -> &'static str { + SESSION_OPEN_ACTION + } +} + +/// The action a session open is audited under. +pub const SESSION_OPEN_ACTION: &str = "streaming.session.open"; + +/// The realtime socket an OpenAI Realtime session dials, under the member's base URL. +pub const REALTIME_SOCKET_PATH: &str = "/v1/realtime"; + +/// The socket a Gemini Live session dials, under the member's base URL. +pub const GEMINI_SOCKET_PATH: &str = + "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent"; + +/// A request bound for the far end: verb and target explicit, then its head fields and body. The +/// kernel adds the credential and sends it; the target is under the member's base URL. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Attempt { + /// The verb. + pub verb: &'static str, + /// The target, under the member's base URL. + pub target: &'static str, + /// The head fields, in order. + pub fields: Vec<(&'static str, String)>, + /// The body. + pub body: Vec, +} + +/// The mint's ATTEMPT: the locked session params the browser's session is bound to, the secret's +/// lifetime (the requested one held inside its bounds), and the caller named for attribution. +/// +/// # Errors +/// The body's serializer text. +pub fn mint_attempt( + config: &SessionConfig, + caller: Option<&str>, + requested_ttl_secs: Option, +) -> Result { + let mut fields = vec![(FIELD_CONTENT_TYPE, JSON_CONTENT_TYPE.to_string())]; + if let Some(r) = caller.filter(|r| !r.is_empty()) { + fields.push((SAFETY_IDENTIFIER_HEADER, r.to_string())); + } + Ok(Attempt { + verb: "POST", + target: CLIENT_SECRETS_PATH, + fields, + body: mint_request_body(clamped_ttl_secs(requested_ttl_secs), config)?, + }) +} + +/// The SDP offer's ATTEMPT: the caller's offer, relayed as it arrived. +#[must_use] +pub fn sdp_attempt(offer: &[u8]) -> Attempt { + Attempt { + verb: "POST", + target: CALLS_PATH, + fields: vec![(FIELD_CONTENT_TYPE, SDP_CONTENT_TYPE.to_string())], + body: offer.to_vec(), + } +} + +/// What the caller is answered. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Reply { + /// The status. + pub status: u16, + /// The head fields, in order. + pub fields: Vec<(&'static str, String)>, + /// The body. + pub body: Vec, + /// For an SDP answer that names its call: the provider's `rtc_` call id, for the session record. + pub rtc_call_id: Option, +} + +/// The status the served plane answers a failed mint with. +pub const MINT_FAILED_STATUS: u16 = 502; + +/// The served plane's 502 for a mint that failed for `reason`. +#[must_use] +pub fn mint_failed(reason: &str) -> Reply { + Reply { + status: MINT_FAILED_STATUS, + fields: Vec::new(), + body: format!( + "streaming ephemeral-secret mint failed: ephemeral token mint failed: {reason}" + ) + .into_bytes(), + rtc_call_id: None, + } +} + +/// The caller's answer to the far end's mint answer (`status`, `body`): the secret and its expiry on +/// a success that carries one, else the served plane's 502 and its text. +#[must_use] +pub fn mint_reply(status: u16, body: &[u8]) -> Reply { + if !(200..300).contains(&status) { + return mint_failed(&format!( + "client-secret endpoint returned {}", + status_text(status) + )); + } + match read_minted(body) { + Ok(minted) => Reply { + status: 200, + fields: vec![(FIELD_CONTENT_TYPE, JSON_CONTENT_TYPE.to_string())], + body: minted_answer(&minted), + rtc_call_id: None, + }, + Err(reason) => mint_failed(&reason), + } +} + +/// The caller's answer to the far end's SDP answer: its status, its body as an SDP answer, and its +/// `Location` when it named one; the call id in that `Location` goes to the session record. +#[must_use] +pub fn sdp_reply(status: u16, location: Option<&str>, body: &[u8]) -> Reply { + let mut fields = vec![(FIELD_CONTENT_TYPE, SDP_CONTENT_TYPE.to_string())]; + if let Some(loc) = location { + fields.push((FIELD_LOCATION, loc.to_string())); + } + Reply { + status, + fields, + body: body.to_vec(), + rtc_call_id: location.and_then(rtc_call_id_of), + } +} + +/// A status as the served plane printed it: the number and its canonical reason phrase, or the +/// number alone for one with none. +#[must_use] +pub fn status_text(status: u16) -> String { + match reason_phrase(status) { + Some(reason) => format!("{status} {reason}"), + None => status.to_string(), + } +} + +/// The canonical reason phrase of a registered status (RFC 9110 and its registry). +#[must_use] +pub const fn reason_phrase(status: u16) -> Option<&'static str> { + Some(match status { + 100 => "Continue", + 101 => "Switching Protocols", + 102 => "Processing", + 200 => "OK", + 201 => "Created", + 202 => "Accepted", + 203 => "Non Authoritative Information", + 204 => "No Content", + 205 => "Reset Content", + 206 => "Partial Content", + 207 => "Multi-Status", + 208 => "Already Reported", + 226 => "IM Used", + 300 => "Multiple Choices", + 301 => "Moved Permanently", + 302 => "Found", + 303 => "See Other", + 304 => "Not Modified", + 305 => "Use Proxy", + 307 => "Temporary Redirect", + 308 => "Permanent Redirect", + 400 => "Bad Request", + 401 => "Unauthorized", + 402 => "Payment Required", + 403 => "Forbidden", + 404 => "Not Found", + 405 => "Method Not Allowed", + 406 => "Not Acceptable", + 407 => "Proxy Authentication Required", + 408 => "Request Timeout", + 409 => "Conflict", + 410 => "Gone", + 411 => "Length Required", + 412 => "Precondition Failed", + 413 => "Payload Too Large", + 414 => "URI Too Long", + 415 => "Unsupported Media Type", + 416 => "Range Not Satisfiable", + 417 => "Expectation Failed", + 418 => "I'm a teapot", + 421 => "Misdirected Request", + 422 => "Unprocessable Entity", + 423 => "Locked", + 424 => "Failed Dependency", + 426 => "Upgrade Required", + 428 => "Precondition Required", + 429 => "Too Many Requests", + 431 => "Request Header Fields Too Large", + 451 => "Unavailable For Legal Reasons", + 500 => "Internal Server Error", + 501 => "Not Implemented", + 502 => "Bad Gateway", + 503 => "Service Unavailable", + 504 => "Gateway Timeout", + 505 => "HTTP Version Not Supported", + 506 => "Variant Also Negotiates", + 507 => "Insufficient Storage", + 508 => "Loop Detected", + 510 => "Not Extended", + 511 => "Network Authentication Required", + _ => return None, + }) +} + +#[cfg(test)] +#[path = "tests/driven_tests.rs"] +mod tests; diff --git a/crates/busbar-plane-streaming/src/governed.rs b/crates/busbar-plane-streaming/src/governed.rs index 9d2ac98b40..c575779056 100644 --- a/crates/busbar-plane-streaming/src/governed.rs +++ b/crates/busbar-plane-streaming/src/governed.rs @@ -1,27 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (C) 2026 Busbar Inc and contributors -//! THE GOVERNED-CALL PORT — how a client-served tool call's reply reaches the node's own table. +//! THE GOVERNED-CALL PORT — how a tool call's reply reaches the node's own table. //! -//! Two kinds of tool call cross a live session, and they end in different places. +//! A tool call is part of the model's response: the session relays it to the caller as-is, the +//! caller runs the tool, and busbar executes nothing (Law 11; QUESTIONS Q98). The answer can only +//! come from the caller, so the call's leg is a *wait*, and a wait belongs to the unit the kernel +//! opened for the call — not to the session. The node holds that table ([`crate::open_calls`]): it +//! enters the wait when the session relays the call's close, wakes it when a reply names the call, +//! and sweeps the ones nobody answered. The session's whole job is to be the thing that *tells* it, +//! and this port is that telling, dependency-inverted because a plane crate that named the +//! composition root would be the I/O half deciding which unit an answer belongs to. //! -//! A call for a tool **this node serves** is the tool moat the design is built around -//! (the tool-moat section of `BUSBAR-1.6.0.md` #18/#45): the runtime accumulates the streamed arguments, executes the -//! tool in-process through `busbar_voice::runtime::ToolExecutor`, and authors the `function_call_output` -//! itself. The client never sees it and could not forge it. Nothing about that path changes here. -//! -//! A call for a tool the node **does not** serve is the other half: the answer can only come from the -//! client, so the call's leg is a *wait*, and a wait belongs to the unit the kernel opened for the -//! call — not to the runtime. The root holds that table (`OpenToolCalls` on the voice node): it -//! enters the wait when the runtime plans the leg, wakes it when a reply names the call, and sweeps -//! the ones nobody answered. The runtime's whole job is to be the thing that *tells* it — and this port -//! is that telling, dependency-inverted the same way `busbar_voice::runtime::ToolExecutor` is, because a -//! plane crate that named the composition root would be the I/O half deciding which unit an answer -//! belongs to. -//! -//! A node with no governed table bound keeps the pre-1.6.0 behaviour exactly: every call is served -//! in-process, and a client-authored result is carried upstream verbatim. The governed path is what a -//! composition root opts a session into. +//! A session with no table bound carries a caller-authored result upstream verbatim. /// Why a client's tool reply woke nothing. /// diff --git a/crates/busbar-plane-streaming/src/lib.rs b/crates/busbar-plane-streaming/src/lib.rs index dcca40cf59..849170fa5c 100644 --- a/crates/busbar-plane-streaming/src/lib.rs +++ b/crates/busbar-plane-streaming/src/lib.rs @@ -3,24 +3,12 @@ //! //! ## What this crate is //! -//! An ADAPTER, in the same sense `busbar-plane-llm` is one: every method of the plane kind here is a -//! thin wrapper over a codec in this crate's own `codec` module — -//! the OpenAI Realtime and Gemini Live dialect readers/writers and the four-layer duplex/session IR -//! they meet in (`docs/design/BUSBAR-1.6.0.md` #18/#45, the four-layer IR section). No wire format -//! for those two dialects is written twice. -//! -//! Two things this crate DOES write itself, because nothing upstream provides them and the task this -//! crate exists for names them explicitly: -//! -//! * A minimal Twilio Media Streams JSON reader/writer ([`twilio`]) — `codec` has no dialect codec -//! for Twilio's own wire, and the one Twilio-shaped module in it ([`codec::topology::twilio`]) is -//! for the legacy engine's own use, not this crate's — this crate never reaches it (see the -//! crate-root dependency note below). So this crate's Twilio reader/writer is written from the -//! wire shape alone, independently, and is NOT a copy of that module. -//! * A standard G.711 µ-law ↔ PCM16 transform ([`ulaw`]) — `codec` only carries the byte-rate -//! bookkeeping for the format (`codec::ir::media::AudoFormat`), not an actual sample transcoder; -//! its own doc comments call the transcode an unimplemented "seam...armed only when a lane -//! declares it." This crate is the lane that declares it. +//! The plane's door ([`door`]) and everything it answers with: the Statement tail and the +//! per-generation snapshot, the driver answers for the one-request doors ([`driven`]), and one live +//! session as the driver serves it ([`session_unit`] over the frame pump [`session_pump`]). The +//! dialect readers and writers (OpenAI Realtime, Gemini Live) and the four-layer duplex/session IR +//! they meet in are this crate's own `codec` module (`docs/design/BUSBAR-1.6.0.md` #18/#45); the +//! Twilio Media Streams envelope is [`codec::topology::twilio`]. No wire format is written twice. //! //! ## What this crate is not //! @@ -32,30 +20,19 @@ //! //! ## The dependency seam, stated honestly //! -//! THIS CRATE DOES NOT DEPEND ON `busbar-voice`, AND THE EDGE RUNS THE OTHER WAY. Measured with -//! `cargo tree -p busbar-plane-streaming --edges normal --depth 1`, this crate's whole direct -//! closure is `busbar-contract` + `serde` + `serde_json` + `bytes` + `async-trait` (a proc-macro) — -//! the codecs are this crate's own `codec` module, not a named dependency. The legacy engine is the -//! crate that names THIS one, not the reverse — so there is no `default-features = false` on a line -//! naming it here, because there is no such line at all. -//! -//! `busbar-voice`'s own plane machinery (`PLANE_DECL`, `mount`, `runtime`, `topology`) is built -//! against a different, older plane architecture (`busbar_kernel::plane::registry::PlaneDecl`, -//! the same shape `busbar-mcp`/`busbar-a2a` use) and is gated behind busbar-voice's `runtime` cargo -//! feature. None of that machinery, and none of the async runtime it would pull in (`tokio`, -//! `futures`, `hyper`), is reachable from here — the closure above measures zero of them. +//! Measured with `cargo tree -p busbar-plane-streaming --edges normal --depth 1`, this crate's whole +//! direct closure is `busbar-contract` + `serde` + `serde_json` + `bytes` + `url` — the codecs are +//! this crate's own `codec` module, not a named dependency, and no async runtime (`tokio`, +//! `futures`, `hyper`) is reachable from here. //! //! What this crate DOES use is [`codec::ir`] — the plane-4 duplex/session intermediate //! representation and both dialect codecs — which is pure, sync, and free of any async surface. //! //! ## What it holds across calls //! -//! Nothing of its own. The plane value itself ([`StreamingPlane`]) is immutable and carries only its -//! configured upstream list. What a session needs across frames — the codec's per-connection state, -//! the negotiated dialect, the counters this crate derives itself (`audio_seconds_in`, `tool_calls`), -//! and the one pending IR event a two-step ingress/egress or decode/encode pair needs to hand across -//! — lives in the kernel-held [`busbar_contract::plane::PlaneSessionState`], via -//! [`session::VoiceSessionState`]. +//! The plane value ([`StreamingPlane`]) is immutable and carries only its configured upstream list. +//! A door instance holds its live generations; a live session's state (the codec's decode state, the +//! open turn's counters, the session's cumulative units and record) is a [`session_unit::SessionUnit`]. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -68,20 +45,23 @@ pub mod claims; pub mod codec; pub mod config; pub mod diagnostics; +pub mod door; +pub mod driven; pub mod governed; pub mod meta; -pub mod oneshot; pub mod open_calls; -pub mod plane; +/// What an `on_piece` answer owes the reply buffer across re-calls, and the one settle of its buffers. +mod piece; pub mod provider; pub mod register; +pub mod request_unit; pub mod session; +pub mod session_door; pub mod session_params; pub mod session_pump; pub mod session_row; -pub mod tools; -pub mod twilio; -pub mod ulaw; +pub mod session_unit; +pub mod sessions; #[cfg(test)] mod tests; diff --git a/crates/busbar-plane-streaming/src/oneshot.rs b/crates/busbar-plane-streaming/src/oneshot.rs deleted file mode 100644 index c7e72ab7de..0000000000 --- a/crates/busbar-plane-streaming/src/oneshot.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! The one-shot transcribe/TTS surface — a provisional wire shape, documented as such. -//! -//! `busbar-voice`'s own IR is duplex/session-only: per its crate-root documentation, Plane 4 is "the -//! duplex / live-voice plane," and nothing in `crate::codec::ir` models a request with no session at -//! all. So there is no dedicated wire format anywhere in this crate's dependency closure to decode a -//! one-shot transcribe or text-to-speech request against — this is one of the places the task this -//! crate exists for names as a genuine gap rather than something to guess past silently. -//! -//! What this module does instead: it treats a one-shot request as [`busbar_contract::plane::Ingress::OneShot`] -//! — a single `IrClientEvent`/`IrServerEvent`-shaped exchange with no session state, in the same -//! event vocabulary the duplex dialects use, per this crate's documented plane-level decision. The -//! wire shape it decodes for the request body is the widely-used convention every OpenAI-Realtime- -//! adjacent provider's REST transcription/speech endpoints share (a JSON body carrying an `input` -//! or `model` field for TTS; an opaque audio body for transcription) — PROVISIONAL, stated as such: -//! it is not derived from any codec this crate depends on, and a future pass that finds a more -//! precise shape (e.g. multipart transcription uploads) should replace it without ceremony. - -use serde_json::Value; - -/// The text a one-shot text-to-speech request asks to be spoken, if the body is the provisional -/// JSON shape (`{"input": "...", ...}`) and the field is a string. -/// -/// Used as this plane's estimate of the `text_tokens` class for a `tts` unit — there is no upstream -/// usage report to read the true count from before the audio comes back, so the input text length is -/// the honest, documented estimate rather than a fabricated number. -#[must_use] -pub fn tts_input_text(body: &[u8]) -> Option { - let value: Value = serde_json::from_slice(body).ok()?; - value - .get("input") - .and_then(Value::as_str) - .map(str::to_string) -} - -/// The voice a one-shot text-to-speech request asks for, if named in the provisional JSON shape. -#[must_use] -pub fn tts_voice(body: &[u8]) -> Option { - let value: Value = serde_json::from_slice(body).ok()?; - value - .get("voice") - .and_then(Value::as_str) - .map(str::to_string) -} diff --git a/crates/busbar-plane-streaming/src/open_calls.rs b/crates/busbar-plane-streaming/src/open_calls.rs index 0eb6861f1a..ecedd99e8e 100644 --- a/crates/busbar-plane-streaming/src/open_calls.rs +++ b/crates/busbar-plane-streaming/src/open_calls.rs @@ -4,9 +4,9 @@ //! THE NODE'S OPEN-CALL TABLE — every client-served tool call this node's sessions have open, and the //! one port the session runtime reaches it through. //! -//! Two kinds of tool call cross a live session. A call for a tool this node serves is executed -//! in-process and the client never authors its result. A call for a tool the node does NOT serve has -//! only one possible answerer, so its leg is a wait: the runtime plans it +//! A tool call crossing a live session is relayed to the caller, who runs the tool (Law 11): busbar +//! executes nothing. The caller is its only possible answerer, so its leg is a wait: the session +//! plans it //! ([`GovernedCalls::planned`]), the client's reply names the call it answers //! ([`GovernedCalls::replied`]), and the tick beside the pump ends the ones nobody answered //! ([`GovernedCalls::expired`]). Which answer wakes which wait is decided here, once, by @@ -27,14 +27,28 @@ use busbar_contract::ids::{CorrelationRef, CorrelationValue, UnitKey}; use busbar_contract::reply::{AwaitingReplies, NotWaiting}; use busbar_contract::Millis; +/// The fact key a tool call's reply wait correlates on. +/// +/// Public because it is half of a correlation and the other half is entered somewhere else: the leg +/// this plane plans names this key, and whatever holds the waiting table has to name the same one +/// for a reply to match. Two spellings of one key is exactly the drift that makes a wait unmatchable +/// while both sides look right on their own. +pub const FACT_TOOL_CORRELATION: &str = crate::meta::FACT_CALL_ID; + +/// How long a tool call's reply leg waits, in seconds. +/// +/// Declared here, beside the leg that names it, so the table that has to end an unanswered call at +/// this deadline reads the plane's own figure rather than restating it. +pub const TOOL_REPLY_DEADLINE_SECS: u32 = 30; + /// The leg a provider-pushed tool call plans: deliver it to the client, and wait for the answer. /// /// The key and the deadline are the plane's own declarations, read from it rather than restated. A /// second spelling of either here would be a wait entered under one key and answered under another, /// with both files looking correct on their own. pub const TOOL_REPLY_LEG: ClientMode = ClientMode::AwaitReply { - correlation_key: crate::plane::FACT_TOOL_CORRELATION, - deadline_secs: crate::plane::TOOL_REPLY_DEADLINE_SECS, + correlation_key: FACT_TOOL_CORRELATION, + deadline_secs: TOOL_REPLY_DEADLINE_SECS, }; /// Why a client's tool reply woke nothing. @@ -256,7 +270,7 @@ impl NodeCalls { /// the key the plane's leg names — the one pair `planned` enters and `replied` answers. fn tool_call(call_id: &str) -> CorrelationRef<'_> { CorrelationRef { - fact_key: crate::plane::FACT_TOOL_CORRELATION, + fact_key: FACT_TOOL_CORRELATION, value: CorrelationValue::Str(call_id), } } diff --git a/crates/busbar-plane-streaming/src/piece.rs b/crates/busbar-plane-streaming/src/piece.rs new file mode 100644 index 0000000000..f72fa6a714 --- /dev/null +++ b/crates/busbar-plane-streaming/src/piece.rs @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THIS PLANE'S PIECE ANSWER: what an `on_piece` answer owes the host's reply +//! buffer, and the one settle of the answer's field, unit and arena buffers. +//! +//! `BUSBAR-1.6.0.md` Part 3, section 12, "The route pump": a full reply buffer answers `more = 1`; +//! the kernel flushes it, waits for the caller's side and calls again with the same `from` and zero +//! bytes ([`is_recall`]). An answer that does not fit its field, unit or arena buffer is short as a +//! whole ([`settle`]): the slot answers FAILED, and the kernel re-calls with the same piece and room. +//! +//! The door keeps an [`Owed`] per unit and pays it across the re-calls, so the backpressure rule +//! (`EMIT_DONE` only with the last byte) is written once in this plane. Each plane door carries +//! this logic today; one SDK home for all of them is 1.6.0-QUESTIONS.md PIECE-HOME. + +use busbar_contract::abi::plane::{ + OnPieceIn, OnPieceOut, OutField, UnitCount, EMIT_DONE, EMIT_TO_FAR_END, PIECE_OUT_TEXT, +}; +use busbar_contract::abi::sdk::{HostBuf, Lent, Out}; + +/// Whether `input` is the kernel's re-call after a `more = 1` answer: no bytes, no flags and no +/// attempt. +#[must_use] +pub fn is_recall(input: Lent<'_, OnPieceIn>) -> bool { + let given = input.get(); + given.flags == 0 && given.attempt_no == 0 && input.field(|i| &i.bytes).bytes().is_empty() +} + +/// The bytes a unit's answer still owes the host's reply buffer, and the `EMIT_*` bits they carry. +#[derive(Debug, Default)] +pub struct Owed { + bytes: Vec, + at: usize, + flags: u32, +} + +impl Owed { + /// Whether any byte is still owed. + #[must_use] + pub fn pending(&self) -> bool { + self.at < self.bytes.len() + } + + /// Owe `bytes` under the `EMIT_*` `flags`, and pay what fits now. Answers whether the unit is + /// done: every byte written, with `EMIT_DONE` among `flags`. + pub fn owe( + &mut self, + bytes: &[u8], + flags: u32, + input: Lent<'_, OnPieceIn>, + out: &mut Out<'_, OnPieceOut>, + ) -> bool { + self.bytes.clear(); + self.bytes.extend_from_slice(bytes); + self.at = 0; + self.flags = flags; + self.pay(input, out) + } + + /// Write what is owed into the reply buffer, as much as fits: `more = 1` while any is left, and + /// the owed flags on every write, `EMIT_DONE` held back until the last byte, and so is a + /// far-bound message's `PIECE_OUT_TEXT` (the kernel gathers a far-bound message whole, so its + /// last piece names it text). Answers whether the unit is done. + pub fn pay(&mut self, input: Lent<'_, OnPieceIn>, out: &mut Out<'_, OnPieceOut>) -> bool { + let n = input.reply_buf().stream(&self.bytes[self.at..]); + self.at += n; + let more = self.pending(); + out.set(|o| &o.emitted, n as u64); + out.set(|o| &o.more, u32::from(more)); + out.set( + |o| &o.flags, + if more && self.flags & EMIT_TO_FAR_END != 0 { + self.flags & !(EMIT_DONE | PIECE_OUT_TEXT) + } else if more { + self.flags & !EMIT_DONE + } else { + self.flags + }, + ); + if more { + return false; + } + let done = self.flags & EMIT_DONE != 0; + *self = Owed::default(); + done + } +} + +/// Settle an answer's field, unit and arena buffers: `(written, needed)` of each, short as a whole +/// when any one is short. Answers whether the answer is short. +pub fn settle( + out: &mut Out<'_, OnPieceOut>, + fields: &HostBuf<'_, OutField>, + units: &HostBuf<'_, UnitCount>, + arena: &HostBuf<'_, u8>, +) -> bool { + let short = !(fields.fits() && units.fits() && arena.fits()); + let (fw, fnd) = fields.settle(short); + let (uw, und) = units.settle(short); + let (aw, and) = arena.settle(short); + out.set(|o| &o.fields_written, fw as u32); + out.set(|o| &o.fields_needed, fnd as u32); + out.set(|o| &o.units_written, uw as u32); + out.set(|o| &o.units_needed, und as u32); + out.set(|o| &o.arena_written, aw as u64); + out.set(|o| &o.arena_needed, and as u64); + short +} diff --git a/crates/busbar-plane-streaming/src/plane.rs b/crates/busbar-plane-streaming/src/plane.rs deleted file mode 100644 index 97562e6fde..0000000000 --- a/crates/busbar-plane-streaming/src/plane.rs +++ /dev/null @@ -1,1459 +0,0 @@ -//! The `Plane`/`SessionPlane` implementation. -//! -//! Every method here is a thin adapter over `crate::codec::ir`'s shared duplex codec -//! ([`OpenAiRealtimeCodec`], [`GeminiLiveCodec`]), this crate's own Twilio reader/writer -//! ([`crate::twilio`]) and its own µ-law transform ([`crate::ulaw`]). None of the three inputs the -//! design brief names is skipped: a turn is the unit (opened on the first audio frame of a session, -//! closed on the upstream's `response.done` usage report or an upstream error); a provider tool call -//! decodes to `Progress::OneShot`; the interrupt fact and the pacing fact are both written where the -//! crate doc comment on [`crate::meta`] says they are. -//! -//! # Assumptions and simplifications, stated once -//! -//! Three of the entries below are no longer this module's own reading. They were raised as findings, -//! carried to the architecture's inventory row for this plane, and **ratified there** — so what -//! follows restates a decision rather than declaring one, and a change to any of the three is a -//! change to that row first and to this file second. They are: only the first decoded IR event per -//! wire frame is acted on; uplink audio on the two WS dialects is metered for `audio_seconds_in` at -//! the rate its frame's wire states, else PCM16 (audit HIGH 9 moved this from "assumed PCM16"); and -//! model-emitted text in a duplex turn prices under `text_tokens_out`, an -//! output class, never the input one. -//! -//! - **HTTP/WS path arrives as a transport fact**, under the kernel's own reserved key -//! (`busbar_contract::transport::facts::PATH`), used to resolve which one-shot operation or which -//! duplex dialect a session's Unit 0 is. This used to be a guess, and both transports now declare -//! the key they publish it under. -//! - **Every decoded IR event of a wire frame is folded, in order.** Both `read_up`/`read_down` -//! return `Vec<..Event>` (one wire message can map to 0..n IR events); `decode_response` walks all -//! of them, letting each update the session's accumulation state, and returns the one meaningful -//! outcome. A tool call is the frame that genuinely decodes to more than one event — Gemini's -//! `toolCall` is `CallOpen`→`CallArgs`→`CallClose` in one frame, and OpenAI's -//! `…arguments.done` frame carries the complete arguments and the close together — so acting on -//! only the first dropped the `CallClose` that dispatches the call. Every other reference-dialect -//! frame emits at most one non-state event, so the fold is identical to first-only for them. -//! - **Uplink audio on the two WS dialects is metered at the format its frame's own wire states.** -//! A Gemini Live blob states its rate in its `mimeType` (16 kHz, the rate Gemini requires on its -//! uplink, or 24 kHz), and the reader carries it on the frame ([`IrAudioFrame::format`]); a frame -//! whose wire states none (a Realtime `input_audio_buffer.append`) is metered at the Realtime -//! default, PCM16 at 24 kHz. Counting a 16 kHz uplink at the 24 kHz rate billed two-thirds of the -//! audio the caller spoke (audit HIGH 9; THE DESIGN section 7: a plane that under-reports is that -//! plugin's bug). Twilio's own uplink is unambiguous (G.711 µ-law, priced from the raw payload -//! before this plane's `encode_ingress_frame` transforms it). -//! - **A provider tool call is dispatched with its ACCUMULATED ARGUMENTS, on `CallClose`.** -//! `CallOpen` announces the call (and increments [`crate::session::TurnCounters::tool_calls`]) but -//! mints nothing on its own — a tool call dispatched with no arguments is a different call. The -//! streamed `CallArgs` fragments are accumulated on the session's own codec state -//! ([`crate::codec::ir::codec::DecodeState::push_call_args`], which appends a fragment and -//! replaces on a whole object, so the OpenAI done-repeat and the Gemini atomic call both land the -//! right bytes), and `CallClose` mints the `tool_call` `OneShot` carrying the tool name and those -//! arguments as its body, correlated by the wire call id. That is when the call becomes visible, -//! priced and audited as its own unit at its `tool_call` operation class. -//! - **`encode_response` is a passthrough of bytes `decode_response` already rendered**, mirroring -//! `busbar-plane-admin`'s pattern. `decode_response` reads the open turn's own client dialect off -//! `Ctx::session()`'s declared `dialect` session fact (the one fact this plane's `SESSION_FACTS` -//! declares) and renders the client-shaped bytes immediately. The downlink half of -//! [`crate::session::Pending`] is gone with the reason it existed: a step after decode can now -//! read what decode determined off the unit's own draft facts. -//! - **Verify's upstream pick for a fresh session is a documented default, not a policy.** A session -//! whose own arriving dialect is one of the two duplex-upstream dialects dials the SAME dialect's -//! configured upstream when one exists; otherwise (Twilio, or no matching upstream configured) it -//! dials this plane's FIRST configured upstream. Picking among several qualifying upstreams is the -//! trust unit's and the ranking hooks' business in every other plane in this workspace; this default -//! exists only so a unit test has something deterministic to assert against. -//! - **A provider tool call's destination is `Client(AwaitReply)`.** The design names two permitted -//! shapes (`Client(AwaitReply)` or `NestedPlane(mcp)`); wiring the second requires the `mcp` plane's -//! own operation classes, which are outside this crate's dependency graph. `Client(AwaitReply)` is -//! implemented; a future pass that wants server-side tool EXECUTION (rather than delivering the call -//! to whatever session member answers it) is the one that should add the `NestedPlane` path. - -use busbar_contract::bounded::{FactValue, Facts, Ir, ScratchBytes}; -use busbar_contract::dest::{ - ClientMode, DestinationFacts, EgressBody, RoutePlan, VerifiedDestination, -}; -use busbar_contract::ids::{AdminVerbId, CorrelationRef, CorrelationValue, OpClassId, SchemeKey}; -use busbar_contract::kinds::{ContentFacts, CredentialLocator, PlaneFacts}; -use busbar_contract::plane::{ - Ingress, Plane, PlaneSessionState, Progress, Response, SessionPlane, UnitDraft, -}; -use busbar_contract::unit::{ - AuditFacts, Ctx, FinishClass, Refusal, Unit, UnitEnd, UsageLocator, UsageLocators, -}; -use busbar_contract::wire::{Decode, DiscardCode, Encode, Frame, FrameCursor, TransportEnvelope}; - -use crate::codec::ir::control::IrDuplexControl; -use crate::codec::ir::event::{IrClientEvent, IrServerEvent}; -use crate::codec::ir::media::{AudioFormat, IrAudioFrame, IrAudioRef, UpDown}; -use crate::codec::ir::tool::IrDuplexTool; -use crate::codec::ir::{ - DecodeState, DuplexReader, DuplexWriter, GeminiLiveCodec, OpenAiRealtimeCodec, WireRef, -}; - -use crate::claims::{self, Dialect}; -use crate::meta; -use crate::session::{Pending, VoiceSessionState}; -use crate::{twilio, ulaw, StreamingPlane}; - -/// The transport fact key the request path is published under. -/// -/// The kernel's own reserved key, named rather than assumed. It used to be this crate's guess, and -/// the module header said so; the header's first assumption is retired with it. -const FACT_PATH: &str = busbar_contract::transport::facts::PATH; - -/// The fact key a tool call's provider-origin `OneShot` correlates on. -/// -/// Public because it is half of a correlation and the other half is entered somewhere else: the leg -/// this plane plans names this key, and whatever holds the waiting table has to name the same one -/// for a reply to match. Two spellings of one key is exactly the drift that makes a wait unmatchable -/// while both sides look right on their own. -pub const FACT_TOOL_CORRELATION: &str = meta::FACT_CALL_ID; - -/// How long a tool call's reply leg waits, in seconds. -/// -/// Declared here, beside the leg that names it, so the table that has to end an unanswered call at -/// this deadline reads the plane's own figure rather than restating it. -pub const TOOL_REPLY_DEADLINE_SECS: u32 = 30; - -/// Both duplex dialects' reader, boxed so the same call site works for either without a generic -/// parameter leaking into every method signature. Cheap: both codecs are zero-sized. -fn reader_for(dialect: Dialect) -> Box { - match dialect { - Dialect::GeminiLive => Box::new(GeminiLiveCodec), - _ => Box::new(OpenAiRealtimeCodec), - } -} - -/// See [`reader_for`]. -fn writer_for(dialect: Dialect) -> Box { - match dialect { - Dialect::GeminiLive => Box::new(GeminiLiveCodec), - _ => Box::new(OpenAiRealtimeCodec), - } -} - -/// The client→server IR event ONE client wire frame carries, read in the client's own dialect. -/// -/// Twilio's carrier `media` payload is widened from µ-law to PCM16 HERE, the documented transform -/// seam (see [`encode_ingress_frame`]'s own note). `None` when the frame carries no event to relay -/// (a lifecycle frame, or one this reader does not model). -/// -/// Metering is DELIBERATELY not done here: the frame that OPENS a turn is metered on its draft at -/// decode (`open_or_relay`'s `FACT_AUDIO_MS_IN`), and every later frame at the relay seam in -/// [`encode_ingress_frame`]; this seam only shapes bytes, so neither path's count leaks into it. -/// -/// [`encode_ingress_frame`]: StreamingPlane::encode_ingress_frame -fn client_event_from_wire( - client_dialect: Dialect, - wire: &[u8], - state: &mut VoiceSessionState, -) -> Result, Encode> { - match client_dialect { - Dialect::TwilioMediaStreams => { - let event = match twilio::decode(wire) { - Ok(event) => event, - Err(twilio::TwilioError::UnknownEvent(_)) => return Ok(None), - Err(_) => return Err(Encode::Unrepresentable), - }; - match event { - twilio::TwilioEvent::Media { payload, .. } => { - let pcm = ulaw::decode_frame(&payload); - Ok(Some(IrClientEvent::AudioFrame(IrAudioFrame { - dir: UpDown::Up, - seq: state.codec.next_up_seq(), - media: bytes::Bytes::from(pcm), - origin: IrAudioRef::default(), - // Metered from the carrier payload (µ-law) before the transform widens it. - format: None, - }))) - } - _ => Ok(None), - } - } - _ => { - let reader = reader_for(client_dialect); - Ok(reader - .read_up_ref(WireRef(wire), &mut state.codec) - .into_iter() - .next()) - } - } -} - -/// Re-frame ONE client→server event onto the upstream dialect's wire and park the result in the -/// arena — the single dialect-writer path every relayed client frame travels, whether it OPENS the -/// turn (Unit 0's egress body, via [`encode_egress`]) or is a LATER frame of it (via -/// [`encode_ingress_frame`]). `None` when the upstream dialect has no verb for the concept (the -/// cross-dialect drop rows): nothing is relayed, the same answer a fully-handled lifecycle frame -/// gives. -/// -/// [`encode_egress`]: StreamingPlane::encode_egress -/// [`encode_ingress_frame`]: StreamingPlane::encode_ingress_frame -fn write_up_to_arena<'u>( - upstream_dialect: Dialect, - event: IrClientEvent, - state: &mut VoiceSessionState, - ctx: &Ctx<'u>, -) -> Result>, Encode> { - let writer = writer_for(upstream_dialect); - let Some(out) = writer.write_up(event, &mut state.codec) else { - return Ok(None); - }; - ctx.arena() - .alloc_bytes(&out.0) - .map(Some) - .map_err(|_| Encode::ScratchExhausted) -} - -impl StreamingPlane { - /// The upstream a fresh session's Unit 0 dials, given the dialect it arrived on. See the module - /// doc comment's "verify's upstream pick" note. - fn default_upstream(&self, arriving: Dialect) -> Option<&'static crate::Upstream> { - if arriving.is_duplex_upstream() { - if let Some(u) = self.upstream_for_dialect(arriving) { - return Some(u); - } - } - self.upstreams().first() - } -} - -impl Plane for StreamingPlane { - fn decode_ingress<'u>( - &self, - frames: &mut FrameCursor<'u>, - st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Decode> { - // Dispatch on the SESSION's own bound dialect, not the literal transport key: a claim's - // selector (`claims::dialect_for`) is what names the dialect, and more than one dialect can - // share a transport key in principle (the two WS dialects already do). A one-shot HTTP - // operation carries no session state at all, which is how the two shapes are told apart here. - match st { - None => decode_one_shot(frames, ctx), - Some(halfbox) => { - let state = halfbox - .get_mut::() - .ok_or(Decode::MissingDeclaredFact)?; - let dialect = state.dialect.ok_or(Decode::MissingDeclaredFact)?; - match dialect { - Dialect::TwilioMediaStreams => decode_twilio_frame(frames, state, ctx), - Dialect::OpenaiRealtime | Dialect::GeminiLive => { - decode_ws_frame(frames, state, dialect, ctx) - } - Dialect::OneShotTranscribe | Dialect::OneShotTts => { - Err(Decode::UnsupportedOperation) - } - } - } - } - } - - fn encode_egress<'u>( - &self, - u: &Unit<'u>, - dest: &VerifiedDestination, - st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Encode> { - // Unit 0 of a DUPLEX session: the first frame IS the egress body (the session-shapes section - // of the architecture doc — "Unit 0's EgressBody is the first upstream frame"). Its body - // carries the client's opening wire frame verbatim (stored by `open_or_relay`), and it must - // reach the provider in the UPSTREAM's own dialect — the SAME shaping `encode_ingress_frame` - // gives every LATER frame of the turn. Rendering it through the upstream writer HERE is what - // makes a session-opening `session.update` (voice/instructions/tools/turn_detection) actually - // apply, and a first audio frame arrive as a dialect-shaped `input_audio_buffer.append` - // rather than raw PCM; a byte pass-through of the raw opening frame silently applied none of - // it. A one-shot transcribe/tts unit has no session state: its body was already decoded in - // the upstream's HTTP request shape, so it passes straight through unchanged. - let session = st.and_then(|halfbox| halfbox.get_mut::()); - let body = match session { - Some(state) => { - let client_dialect = client_dialect_from_session(ctx) - .or(state.dialect) - .ok_or(Encode::Poisoned)?; - let upstream_dialect = upstream_dialect_for(self, dest); - // The opening frame is re-read off the unit's OWN body rather than the session stash: - // the body is this unit's authoritative frame, whereas `state.pending` is a per-half - // scratch slot the next decoded frame overwrites — reading it here would risk shaping - // a later frame's event into Unit 0. Metering is not repeated: the opening frame's - // audio was counted on its draft at decode. - match client_event_from_wire(client_dialect, u.body().body(), state)? { - Some(event) => write_up_to_arena(upstream_dialect, event, state, ctx)? - // The upstream dialect has no verb for this opening concept: nothing is - // relayed, the same answer `encode_ingress_frame` gives a dropped later frame. - .unwrap_or_else(|| ScratchBytes::new(&[])), - None => ScratchBytes::new(&[]), - } - } - None => ctx - .arena() - .alloc_bytes(u.body().body()) - .map_err(|_| Encode::ScratchExhausted)?, - }; - Ok(EgressBody { - envelope: TransportEnvelope::default(), - body, - auth: SchemeKey::new(claims::SCHEME), - }) - } - - fn encode_ingress_frame<'u>( - &self, - _u: &Unit<'u>, - f: &Frame, - dest: &VerifiedDestination, - st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result>, Encode> { - let state = st - .ok_or(Encode::Poisoned)? - .get_mut::() - .ok_or(Encode::Poisoned)?; - // The client's own dialect off the session fact first, exactly as `decode_response` asks - // it: this seam is scoped to the DESTINATION, so the half it is handed is not guaranteed - // to be the one the client's arriving frames bound a dialect on. - let client_dialect = client_dialect_from_session(ctx) - .or(state.dialect) - .ok_or(Encode::Poisoned)?; - let upstream_dialect = upstream_dialect_for(self, dest); - - let client_event = match client_dialect { - Dialect::TwilioMediaStreams => { - // The documented seam: the µ-law -> PCM16 transform happens HERE, never at decode. - let event = match twilio::decode(f.bytes.as_slice()) { - Ok(event) => event, - // The same forward-compatibility rule the decode step takes: an event this - // reader does not model carries no audio to relay, and is not a reason to end - // the call. - Err(twilio::TwilioError::UnknownEvent(_)) => return Ok(None), - Err(_) => return Err(Encode::Unrepresentable), - }; - match event { - twilio::TwilioEvent::Media { payload, .. } => { - // Priced from the raw carrier payload, before the transform below widens - // it: what the caller spoke is µ-law on this dialect's wire. - state.turn.admit_audio(AudioFormat::G711Ulaw, payload.len()); - let pcm = ulaw::decode_frame(&payload); - IrClientEvent::AudioFrame(IrAudioFrame { - dir: UpDown::Up, - seq: state.codec.next_up_seq(), - media: bytes::Bytes::from(pcm), - // A carrier media frame names no conversation item. - origin: IrAudioRef::default(), - // Counted above from the carrier payload, at G.711 µ-law. - format: None, - }) - } - // Lifecycle events (`connected`/`start`/`mark`/`stop`) carry no audio and are - // fully handled at decode; nothing is relayed onward for them. - _ => return Ok(None), - } - } - _ => match state.pending.take() { - Some(Pending::Ingress(ev)) => ev, - // Nothing stashed. Either decode answered the frame fully (a control event with - // nothing to relay) or the stash was left on another half of the session, which is - // not a reason to drop a caller's audio on the floor: the frame is re-read here. - // The reader is the one the stash existed to avoid calling twice, so it is called - // once either way. - None => { - let reader = reader_for(client_dialect); - let events = reader.read_up_ref(WireRef(f.bytes.as_slice()), &mut state.codec); - match events.into_iter().next() { - Some(ev) => ev, - None => return Ok(None), - } - } - }, - }; - - // The uplink meter is taken HERE, at the seam that relays the frame to the provider, and - // not at decode: decode runs against the client half of the session and this step and the - // response step run against the destination's, so a figure counted at decode was read back - // from a half it was never written to and every second the caller spoke metered at zero. - // Counting where the audio actually leaves also means audio this node never relayed is - // audio nobody is charged for. - // Twilio's own uplink was counted from its carrier payload above, before the transform. - if client_dialect != Dialect::TwilioMediaStreams { - if let IrClientEvent::AudioFrame(f) = &client_event { - // See the module doc comment: the format the frame's wire states, else PCM16. - state - .turn - .admit_audio(f.format.unwrap_or(AudioFormat::Pcm16), f.media.len()); - } - } - - // The upstream dialect may have NO verb for this concept (the cross-dialect drop rows): then - // nothing is relayed — the same answer a lifecycle frame handled fully at decode gives. This - // is the SAME dialect-writer path Unit 0's opening frame takes in `encode_egress`. - write_up_to_arena(upstream_dialect, client_event, state, ctx) - } - - fn decode_response<'u>( - &self, - frames: &mut FrameCursor<'u>, - dest: &VerifiedDestination, - st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Decode> { - // A one-shot operation has no session, by construction: `decode_one_shot` admitted it - // without one and there is none to hand back here. Requiring one turned every transcribe - // and every text-to-speech answer into a decode refusal, so the unit that WAS admitted - // never saw its own answer and never metered anything at all. - let Some(halfbox) = st else { - return decode_one_shot_response(frames, ctx); - }; - let state = halfbox - .get_mut::() - .ok_or(Decode::MissingDeclaredFact)?; - let upstream_dialect = upstream_dialect_for(self, dest); - let client_dialect = client_dialect_from_session(ctx).unwrap_or(upstream_dialect); - - let frame = frames.next_frame().ok_or(Decode::Malformed)?; - // The reader is handed the frame WHERE IT IS. It parses the bytes and keeps none of them, so - // buying them a second home first bought nothing — and a duplex session reads fifty frames a - // second in each direction for the length of a call. - let reader = reader_for(upstream_dialect); - let events = reader.read_down_ref(WireRef(frame.bytes.as_slice()), &mut state.codec); - // ALL events of the frame are folded, not just the first. A tool call is the one wire frame - // that genuinely decodes to more than one IR event: Gemini delivers `CallOpen`→`CallArgs`→ - // `CallClose` in a single `toolCall` frame, and OpenAI's `…arguments.done` frame carries the - // complete arguments AND the close together. Acting on only the first event dropped the - // `CallClose` that dispatches the call, so its executor never ran — the defect this fold - // fixes. Each event updates the session's accumulation state; the meaningful outcome (the - // `OneShot` a `CallClose` mints) is the one returned. Every reference dialect emits at most - // one non-state event per frame, so this is identical to acting on the first for every - // non-tool frame. - let mut outcome = Progress::Discard { - reason: DiscardCode::Unsupported, - }; - for event in events { - match progress_from_server_event(event, state, client_dialect, ctx)? { - Progress::Discard { .. } => {} - progress => outcome = progress, - } - } - Ok(outcome) - } - - fn encode_response<'u>( - &self, - r: &Response<'u>, - _st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Encode> { - // See the module doc comment's `encode_response` note: `decode_response` already rendered - // client-dialect bytes into `r.ir`; this is the passthrough. - ctx.arena() - .alloc_bytes(r.ir.body()) - .map_err(|_| Encode::ScratchExhausted) - } - - fn encode_refusal<'u>( - &self, - refusal: &Refusal, - _draft: Option<&UnitDraft<'u>>, - _st: Option<&PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Encode> { - let (code, message) = refusal_render(refusal.reason); - let event = IrServerEvent::Error { - code: code.to_string(), - message: message.to_string(), - }; - // A refusal is rendered in the OpenAI Realtime shape unconditionally: `st` is deliberately - // `&PlaneSessionState` (immutable — a refusal never advances codec state, per the trait's own - // doc comment), so this plane cannot read back which dialect a not-yet-open session even - // claimed. `error` is one of the few wire shapes both duplex dialects converge on closely - // enough that a client library for either can surface it; a fully dialect-correct refusal - // would need the immutable half of the state to still carry the negotiated dialect, which it - // does today (`VoiceSessionState::dialect`) but this method has no path to it before Unit 0 - // completes. Flagged rather than guessed past. - // The write seam threads the session's decode state (it holds what framing cannot answer - // per-event); a refusal reaches none of the session's state here, and needs none — it is one - // self-contained frame with nothing accumulated behind it, so it is framed against a fresh one. - let bytes = OpenAiRealtimeCodec - .write_down(event, &mut DecodeState::default()) - .ok_or(Encode::Unrepresentable)? - .0; - ctx.arena() - .alloc_bytes(&bytes) - .map_err(|_| Encode::ScratchExhausted) - } - - fn encode_end<'u>( - &self, - _u: &Unit<'u>, - _end: &UnitEnd, - _st: Option<&mut PlaneSessionState>, - _ctx: &Ctx<'u>, - ) -> Result>, Encode> { - // A turn's own ending is always rendered as a `Progress::Terminal` `Response` through - // `encode_response` (the upstream's `response.done`/error IS the ending); there is no further - // trailer this dialect writes at the unit's own close. - Ok(None) - } - - fn authenticate<'u>(&self, u: &Unit<'u>, _ctx: &Ctx<'u>) -> CredentialLocator { - // Twilio, OpenAI Realtime and Gemini Live all authenticate once at session open and cache - // the result for the session's life (`claims::Dialect::authenticates_from_session`); the two - // one-shot HTTP operations present a credential on the one request they are. The dialect is - // the draft's own fact, sealed onto the unit by the kernel, so this step reads what decode - // determined rather than a session fact that a one-shot unit does not have at all. - CredentialLocator { - narrowing: None, - from_session: draft_dialect(u).is_some_and(Dialect::authenticates_from_session), - } - } - - fn verify<'u>(&self, u: &Unit<'u>, ctx: &Ctx<'u>) -> DestinationFacts { - if u.op().as_str() == "tool_call" { - // The leg names the key; the value is the call identifier `decode_response` already - // minted on this unit's `correlation_out`, in the unit's own arena, as itself. This - // plane no longer folds that identifier into a number to get it past a `'static` - // bound — two calls open at once is the ordinary shape of a turn that asks for two - // tools, and it is the pair a fold is least entitled to confuse. - return DestinationFacts::Client { - selector: "*", - mode: ClientMode::AwaitReply { - correlation_key: FACT_TOOL_CORRELATION, - deadline_secs: TOOL_REPLY_DEADLINE_SECS, - }, - }; - } - let session_upstream_count = ctx.session().map(|s| s.upstream_count()).unwrap_or(0); - if session_upstream_count > 0 { - // Route to the upstream this session DIALED at open, not the first configured one. Unit 0 - // (the fresh branch below) picked `default_upstream(arriving)` off the arriving dialect; - // a paired turn must name that SAME upstream by its real config index and lane, or a - // session that dialed the second-declared provider — a gemini-live client where - // openai-realtime is declared first — has every turn metered on the first provider's - // lane: the wrong provider's money. The session's own bound client dialect is the arriving - // dialect Unit 0 chose from, read back off the declared session fact (falling back to the - // unit's own draft fact for the same reason `default_upstream` reads it there). - let dialed = client_dialect_from_session(ctx).or_else(|| draft_dialect(u)); - let idx = dialed - .filter(|d| d.is_duplex_upstream()) - .and_then(|d| self.upstreams().iter().position(|up| up.dialect == d)) - .unwrap_or(0); - let lane = self - .upstreams() - .get(idx) - .map(|up| up.lane) - .unwrap_or(busbar_contract::ids::LaneId::new("streaming")); - return DestinationFacts::SessionUpstream { - upstream: busbar_contract::ids::UpstreamIdx(u8::try_from(idx).unwrap_or(0)), - stream: None, - lane, - }; - } - // The dialect the decode step named, off the unit's own sealed draft facts. - let arriving = draft_dialect(u).unwrap_or(Dialect::OpenaiRealtime); - match self.default_upstream(arriving) { - Some(up) => DestinationFacts::Upstream { - transport: claims::WS_TRANSPORT, - address: busbar_contract::UpstreamAddress::socket(up.host), - lane: up.lane, - }, - None => DestinationFacts::Upstream { - transport: claims::WS_TRANSPORT, - address: busbar_contract::UpstreamAddress::socket(""), - lane: busbar_contract::ids::LaneId::new("streaming"), - }, - } - } - - fn route<'u>(&self, _u: &Unit<'u>, _ctx: &Ctx<'u>) -> RoutePlan { - // `verify` already named the one destination this unit reaches; there is no second leg. - RoutePlan::default() - } - - fn meter<'u>(&self, u: &Unit<'u>, r: &Response<'u>, _ctx: &Ctx<'u>) -> UsageLocators { - let mut lines = busbar_contract::bounded::BoundedVec::new(); - // THE CLASS IS THE DECLARED SYMBOL, NEVER A RE-SPELLING OF IT. Each of these is the const - // `meta` declares in `METER_CLASSES`, so the class a count is EMITTED under and the class - // the plane DECLARED are one value the compiler checks — not two strings that agree today. - // A typo in a literal here emits a count under a class no card can name: silently unpriced, - // or a boot refusal under #77(5), and in neither case anything that points at this line. - // `cached_tokens` is NOT here: it is a subset of the input token classes above, not a - // billable class beside them (architect ruling #71 — see `meta::CLASS_CACHED_TOKENS`'s doc). - // Pricing it too would double-bill the same cached input; the figure is attribution only, - // carried on the `cached_tokens` fact, never on a meter line. - let classes = [ - (meta::FACT_AUDIO_TOKENS_IN, meta::CLASS_AUDIO_TOKENS_IN), - (meta::FACT_AUDIO_TOKENS_OUT, meta::CLASS_AUDIO_TOKENS_OUT), - (meta::FACT_TEXT_TOKENS_IN, meta::CLASS_TEXT_TOKENS_IN), - (meta::FACT_TEXT_TOKENS_OUT, meta::CLASS_TEXT_TOKENS_OUT), - ]; - // A duplex turn's figures all come off the upstream's usage report, which is the answer. A - // one-shot request's input figure comes off the request, which decode read and put on the - // unit. The answer is asked first either way: a figure the destination confirmed beats one - // this node estimated, and the unit's own is only reached where the answer reported none. - let reported = |key: &str| match r.facts.get(key) { - Some(value) => Some(value), - None => u.draft_facts().get(key), - }; - for (fact_key, class) in classes { - if let Some(FactValue::Int(v)) = reported(fact_key) { - let _ = lines.push(UsageLocator { - class, - location: None, - quantity: u64::try_from(v).ok(), - lane: None, - }); - } - } - // The two halves of one duration, ADDED rather than one preferred over the other: the frame - // that opened the turn stated its own milliseconds on the draft (it never reaches the relay - // seam), and every frame after it is counted at that seam and reported on the answer. This - // is not a figure the destination confirmed, so the "answer wins" rule above does not - // govern it — taking the answer alone dropped the opening frame's audio every turn. - let opening = match u.draft_facts().get(meta::FACT_AUDIO_MS_IN) { - Some(FactValue::Int(ms)) => Some(ms), - _ => None, - }; - let relayed = match r.facts.get(meta::FACT_AUDIO_MS_IN) { - Some(FactValue::Int(ms)) => Some(ms), - _ => None, - }; - if opening.is_some() || relayed.is_some() { - let ms = opening.unwrap_or(0).saturating_add(relayed.unwrap_or(0)); - // The counter is milliseconds; the class is seconds. The conversion is the meter - // boundary's, stated once in `meta` — a millisecond figure carried through under a - // seconds-denominated class settles at a thousand times the duration it describes. - let _ = lines.push(UsageLocator { - class: meta::CLASS_AUDIO_SECONDS_IN, - location: None, - quantity: u64::try_from(ms).ok().map(meta::audio_seconds_in), - lane: None, - }); - } - if let Some(FactValue::Int(calls)) = r.facts.get(meta::FACT_TOOL_CALLS) { - let _ = lines.push(UsageLocator { - class: meta::CLASS_TOOL_CALLS, - location: None, - quantity: u64::try_from(calls).ok(), - lane: None, - }); - } - UsageLocators { lines } - } - - fn audit<'u>(&self, u: &Unit<'u>, out: &UnitEnd, _ctx: &Ctx<'u>) -> AuditFacts { - AuditFacts { - op_class: u.op(), - // One mapping, written once in the contract and read by every plane. Every unit this - // plane audits is a turn of a live session, so a completed one is `TurnComplete`. - finish: busbar_contract::unit::finish_class_of(out, FinishClass::TurnComplete), - } - } - - fn plane_facts<'u>( - &self, - verb: AdminVerbId, - _subject: Option<&'u str>, - ctx: &Ctx<'u>, - ) -> Result, Decode> { - if verb != meta::VERB_DIALECTS { - return Err(Decode::UnsupportedOperation); - } - let mut facts = Facts::new(); - let count = ctx - .arena() - .alloc_str(&claims::DIALECT_CLAIMS.len().to_string()) - .map_err(|_| Decode::Oversize)?; - facts - .set("dialect_count", FactValue::Str(count)) - .map_err(|_| Decode::Oversize)?; - Ok(PlaneFacts { facts }) - } - - fn content_facts<'u>( - &self, - _u: &Unit<'u>, - r: &Response<'u>, - _ctx: &Ctx<'u>, - ) -> ContentFacts<'u> { - let mut facts = Facts::new(); - for key in ::CONTENT_FACTS { - if let Some(v) = r.facts.get(key) { - let _ = facts.set(key, v); - } - } - ContentFacts { facts } - } -} - -impl SessionPlane for StreamingPlane { - fn open_session<'u>(&self, ctx: &Ctx<'u>) -> PlaneSessionState { - let dialect = ctx - .transport() - .fact(FACT_PATH) - .and_then(claims::dialect_for) - .unwrap_or(Dialect::OpenaiRealtime); - PlaneSessionState::new(VoiceSessionState::for_dialect(dialect)) - } - - fn open_upstream<'u>(&self, dest: &VerifiedDestination, _ctx: &Ctx<'u>) -> PlaneSessionState { - let dialect = upstream_dialect_for(self, dest); - PlaneSessionState::new(VoiceSessionState::for_dialect(dialect)) - } -} - -/// What a refused session is told, in the closed vocabulary a client is allowed to see. -/// -/// The internal reason is a kernel enum with names for the money, the buckets and the store -/// (`OverdraftCeiling`, `StaleSlice`, `DurabilityUnavailable`). Formatting it onto the wire told -/// every caller which internal ceiling it met and pinned this node's private vocabulary as the -/// dialect's `error.code` — a name no client library has a case for and no dialect documents. What -/// goes out instead is the same small opaque set every other plane in this workspace renders (see -/// `busbar-plane-admin`'s own table): the caller learns the CLASS of refusal and nothing about why -/// this node reached it. -fn refusal_render(reason: busbar_contract::unit::RefusalReason) -> (&'static str, &'static str) { - use busbar_contract::abi::plane::{class_of_refusal, RefusalClass as C}; - // A class-to-wire table over the one classification (the P-item "refusal-reason collapse"). - // There is no `_` arm: this table used to end in one that answered every reason it did not - // name as "internal", so a spent budget, a frozen group, a replayed key or a deadline told the - // caller the node had broken, and the caller retried the wrong thing. Only a node fault is - // "internal" now; every other class is a refusal the caller is told the class of. - match class_of_refusal(reason) { - C::Unreadable | C::TooLarge => ("invalid_request", "the request could not be read"), - C::Rejected => ( - "invalid_request", - "the session could not be opened at this time", - ), - C::Unauthenticated => ("unauthorized", "the session did not carry usable authority"), - C::Forbidden => ( - "forbidden", - "the caller may not open a session for this operation", - ), - C::Throttled | C::Busy => ("rate_limited", "too many sessions at once"), - C::QuotaExhausted => ( - "rate_limited", - "the session could not be opened at this time", - ), - C::NotFound | C::Unreachable => ( - "unavailable", - "no provider is reachable for this session right now", - ), - C::Unavailable | C::Timeout => ( - "unavailable", - "the session could not be opened at this time", - ), - // The node's own fault. A caller is told it failed here and nothing more. - C::PlaneFault | C::NodeFault => { - ("internal", "the session could not be opened at this time") - } - } -} - -/// Which dialect the upstream a verified destination names speaks, by matching its host against -/// this plane's configured upstream list. Falls back to OpenAI Realtime (the more common of the two -/// and this plane's documented default elsewhere) when the destination is a `SessionUpstream` this -/// function cannot resolve a host for, or names no configured upstream at all. -fn upstream_dialect_for(plane: &StreamingPlane, dest: &VerifiedDestination) -> Dialect { - match dest.facts() { - DestinationFacts::Upstream { address, .. } => plane - .upstreams() - .iter() - .find(|u| Some(u.host) == address.authority()) - .map(|u| u.dialect) - .unwrap_or(Dialect::OpenaiRealtime), - _ => plane - .upstreams() - .first() - .map(|u| u.dialect) - .unwrap_or(Dialect::OpenaiRealtime), - } -} - -/// The dialect the decode step named, read back off the unit's sealed draft facts. -/// -/// The one place this plane's later steps ask what dialect a unit is: decode is the step that read -/// the bytes and matched the claim, and what it determined travels on the unit. A one-shot unit has -/// no session at all, so a session fact could not have answered for it. -fn draft_dialect(u: &Unit<'_>) -> Option { - match u.draft_facts().get(meta::FACT_DIALECT) { - Some(FactValue::Str(name)) => dialect_from_name(name), - _ => None, - } -} - -/// The client's own dialect, read back off the session fact this plane declared (`SESSION_FACTS`). -fn client_dialect_from_session<'u>(ctx: &Ctx<'u>) -> Option { - ctx.session() - .and_then(|s| s.session_fact(meta::FACT_DIALECT)) - .and_then(dialect_from_name) -} - -/// The inverse of [`Dialect::name`]. -fn dialect_from_name(name: &str) -> Option { - match name { - "openai-realtime" => Some(Dialect::OpenaiRealtime), - "gemini-live" => Some(Dialect::GeminiLive), - "twilio-media-streams" => Some(Dialect::TwilioMediaStreams), - "transcribe" => Some(Dialect::OneShotTranscribe), - "tts" => Some(Dialect::OneShotTts), - _ => None, - } -} - -/// Every body pointer this plane declares: none. -/// -/// This plane's frames are decoded by the codec rather than located by pointer — a duplex event -/// carries its own shape, and the quantities are counted as audio moves rather than pointed at in -/// a document. The span table is still built by the one scanner from this declaration, so -/// "declares nothing" is something the loop can read instead of a table nobody filled in. -const BODY_PTRS: &[&str] = &[]; - -/// The span view of a body, built from the pointers this plane declares. -fn view<'u>(body: &'u [u8], ctx: &Ctx<'u>) -> Result, Decode> { - let spans = busbar_contract::spans::resolve(body, BODY_PTRS, ctx.arena()) - .map_err(|_| Decode::Oversize)?; - Ok(Ir::new(body, spans)) -} - -/// Decode a one-shot (`http`) transcribe/tts request: no session, a single `OneShot` unit. -fn decode_one_shot<'u>(frames: &mut FrameCursor<'u>, ctx: &Ctx<'u>) -> Result, Decode> { - let path = ctx - .transport() - .fact(FACT_PATH) - .ok_or(Decode::MissingDeclaredFact)?; - let dialect = claims::dialect_for(path).ok_or(Decode::UnsupportedOperation)?; - let op = match dialect { - Dialect::OneShotTranscribe => OpClassId::new("transcribe"), - Dialect::OneShotTts => OpClassId::new("tts"), - _ => return Err(Decode::UnsupportedOperation), - }; - let frame = frames.next_frame(); - let body: &'u [u8] = frame.map(|f| f.bytes.as_slice()).unwrap_or(&[]); - let mut facts = Facts::new(); - facts - .set(meta::FACT_DIALECT, FactValue::Str(dialect.name())) - .map_err(|_| Decode::Oversize)?; - // What a text-to-speech request is priced on is the text it asks to be spoken, and that text is - // in the request rather than in the answer. Decode is the step that reads the request's bytes, - // so the figure is taken here and travels on the unit; the metering step reads it back off the - // draft rather than opening the body a second time. - if dialect == Dialect::OneShotTts { - if let Some(text) = crate::oneshot::tts_input_text(body) { - facts - .set( - meta::FACT_TEXT_TOKENS_IN, - FactValue::Int( - i64::try_from(meta::text_tokens_of(text.len())).unwrap_or(i64::MAX), - ), - ) - .map_err(|_| Decode::Oversize)?; - } - } - Ok(Ingress::OneShot(Box::new(UnitDraft { - op, - body_ir: view(body, ctx)?, - correlates: None, - correlation_out: None, - facts, - }))) -} - -/// Decode the answer to a one-shot transcribe or text-to-speech request. -/// -/// There is no session and no codec state: the operation is one request and one answer, and the -/// answer ends the unit. What the answer states is what is stamped — a transcription's own text, -/// under the class the emitted text prices at. A speech answer is audio bytes whose duration this -/// plane cannot read without knowing the format the upstream chose, so it states nothing about -/// them: the request's own text estimate, taken at decode, is what that unit is priced on. -fn decode_one_shot_response<'u>( - frames: &mut FrameCursor<'u>, - ctx: &Ctx<'u>, -) -> Result, Decode> { - let path = ctx - .transport() - .fact(FACT_PATH) - .ok_or(Decode::MissingDeclaredFact)?; - let dialect = claims::dialect_for(path).ok_or(Decode::UnsupportedOperation)?; - if !matches!(dialect, Dialect::OneShotTranscribe | Dialect::OneShotTts) { - return Err(Decode::UnsupportedOperation); - } - let frame = frames.next_frame(); - let body: &'u [u8] = frame.map(|f| f.bytes.as_slice()).unwrap_or(&[]); - let mut facts = Facts::new(); - if dialect == Dialect::OneShotTranscribe { - if let Some(text) = transcript_text(body) { - facts - .set( - meta::FACT_TEXT_TOKENS_OUT, - FactValue::Int( - i64::try_from(meta::text_tokens_of(text.len())).unwrap_or(i64::MAX), - ), - ) - .map_err(|_| Decode::Oversize)?; - } - } - Ok(Progress::Terminal { - for_: None, - r: Box::new(Response { - ir: view(body, ctx)?, - finish: FinishClass::TurnComplete, - facts, - }), - }) -} - -/// The text a one-shot transcription answered with, in the provisional wire shape this plane -/// documents (`{"text": "..."}`). `None` for anything else, which states nothing rather than -/// guessing a figure. -fn transcript_text(body: &[u8]) -> Option { - let value: serde_json::Value = serde_json::from_slice(body).ok()?; - value - .get("text") - .and_then(serde_json::Value::as_str) - .map(str::to_string) -} - -/// Decode one frame of a duplex session bound to one of the two WS dialects (OpenAI Realtime or -/// Gemini Live). -fn decode_ws_frame<'u>( - frames: &mut FrameCursor<'u>, - state: &mut VoiceSessionState, - dialect: Dialect, - ctx: &Ctx<'u>, -) -> Result, Decode> { - let frame = frames.next_frame().ok_or(Decode::Malformed)?; - // A frame the cursor carved but that carries no bytes yet is the one genuinely partial shape at - // this layer (mcp's own decode makes the same distinction on an empty body): wait for the rest. - if frame.bytes.is_empty() { - return Ok(Ingress::NeedMore); - } - // A frame WITH bytes that is not readable text is a refusal, not a partial read: nothing more - // ever arrives to complete an already-complete WS frame that failed to decode as UTF-8. - std::str::from_utf8(frame.bytes.as_slice()).map_err(|_| Decode::Malformed)?; - // Borrowed, not copied: see `decode_response`'s own note on the downlink side of this. - let reader = reader_for(dialect); - let events = reader.read_up_ref(WireRef(frame.bytes.as_slice()), &mut state.codec); - let Some(event) = events.into_iter().next() else { - // A JSON document this dialect does not recognise is answered exactly the way - // `decode_response` answers an unrecognised server event and mcp answers an unrecognised - // notice: dropped, never left pending forever. - return Ok(Ingress::Discard { - reason: DiscardCode::Unsupported, - }); - }; - // Stashed so `encode_ingress_frame` never calls the stateful reader a second time for the same - // wire frame (see `crate::session::Pending`'s doc comment). - state.pending = Some(Pending::Ingress(event.clone())); - // The original wire frame, handed on so an OPENING frame's Unit-0 body is the client's own - // message (`encode_egress` re-frames it onto the upstream), not the extracted relay. - let wire = frame.bytes.as_slice(); - ingress_from_client_event(event, state, dialect, wire, ctx) -} - -/// Decode one frame of a `twilio-media`-carried session. -fn decode_twilio_frame<'u>( - frames: &mut FrameCursor<'u>, - state: &mut VoiceSessionState, - ctx: &Ctx<'u>, -) -> Result, Decode> { - let frame = frames.next_frame().ok_or(Decode::Malformed)?; - let event = match twilio::decode(frame.bytes.as_slice()) { - Ok(event) => event, - // An event name this reader does not model is not a broken frame: this carrier adds events - // over time and tells its clients to ignore the ones they do not know, and the WS transport - // under this session already discards what it cannot place. Refusing the session over one - // would drop a live call the day the carrier ships a new lifecycle event. A frame that is - // not this carrier's JSON at all is still a refusal. - Err(twilio::TwilioError::UnknownEvent(_)) => { - return Ok(Ingress::Discard { - reason: DiscardCode::Unsupported, - }) - } - // `start` negotiated a `mediaFormat` other than the one this crate's Twilio path assumes - // (`twilio::decode`'s own doc). Every later `media` frame on this stream would be decoded, - // and TIMED, under a wrong assumption — and duration is a LEDGER quantity, so a wrong count - // is a wrong ledger. Refused rather than served on a guess (#42's own spirit: an unknown is - // refused, never silently defaulted). - Err(twilio::TwilioError::UnsupportedMediaFormat) => { - return Err(Decode::UnsupportedOperation) - } - Err(_) => return Err(Decode::Malformed), - }; - match event { - // Lifecycle events with no audio and nothing left to bind: consumed, no unit, no state - // change beyond what `Start` below records. - twilio::TwilioEvent::Connected => Ok(Ingress::Discard { - reason: DiscardCode::Unsupported, - }), - twilio::TwilioEvent::Start { stream_sid, .. } => { - // `twilio::decode` never hands back an empty `stream_sid` for `Start` (it refuses the - // frame itself, `TwilioError::Malformed`, above) — so by construction, everything bound - // here is a real, non-empty identity that CAN bind. See `twilio::decode`'s own note for - // why an empty id must never reach this assignment. - debug_assert!( - !stream_sid.is_empty(), - "twilio::decode must refuse an empty streamSid before Start reaches this arm" - ); - state.twilio_stream_sid = Some(stream_sid); - state.dialect = Some(Dialect::TwilioMediaStreams); - Ok(Ingress::Discard { - reason: DiscardCode::Unsupported, - }) - } - twilio::TwilioEvent::Media { - stream_sid, - payload, - } => { - // The forgery/replay guard the architecture note for this dialect names: a `media` - // frame naming a `streamSid` other than the one `start` bound is discarded, not - // refused — it costs nothing and ends no unit, exactly what a discard is for. - if state.twilio_stream_sid.as_deref() != Some(stream_sid.as_str()) { - return Ok(Ingress::Discard { - reason: DiscardCode::ForgedSource, - }); - } - // Counted at the relay seam, not here: see `encode_ingress_frame`. A frame that OPENS - // the turn is the one exception — it becomes the unit's own egress body and never - // reaches that seam — so its milliseconds travel on the draft the unit is minted from. - let ms = AudioFormat::G711Ulaw.bytes_to_ms(payload.len() as u64); - let arena_bytes = ctx - .arena() - .alloc_bytes(&payload) - .map_err(|_| Decode::Oversize)?; - // The OPENING frame's body is the carrier's ORIGINAL `media` envelope, so `encode_egress` - // can re-run the same µ-law -> PCM -> `input_audio_buffer.append` shaping the relay seam - // does; a byte pass-through would have shipped raw µ-law upstream. The extracted relay is - // still handed on for the already-open case. - open_or_relay( - state, - Dialect::TwilioMediaStreams, - frame.bytes.as_slice(), - arena_bytes, - None, - Some(ms), - ctx, - ) - } - twilio::TwilioEvent::Mark { .. } => Ok(Ingress::Discard { - reason: DiscardCode::Unsupported, - }), - twilio::TwilioEvent::Dtmf { .. } => Ok(Ingress::Discard { - reason: DiscardCode::Unsupported, - }), - twilio::TwilioEvent::Stop => { - // The call ended mid-turn. The open turn's counters are the only record of the audio the - // caller spoke and the tool calls the upstream opened since the last usage report, and - // closing the turn is what takes them — so they travel on the ending, under the same keys - // `meter` reads off a terminal answer. Taking them and dropping them metered every - // hung-up turn at zero: the ledger said nothing happened where audio was served. - let for_ = state.turn_correlation; - let counters = state.close_turn(); - let mut facts = Facts::new(); - set_counter_facts(&mut facts, counters)?; - Ok(Ingress::Close { - for_, - facts: Box::new(facts), - }) - } - } -} - -/// Turn one decoded client→server IR event into an `Ingress` answer, for the two WS dialects. -/// -/// Any client event — audio, control or a tool result — opens the turn if none is open yet: a -/// session's very first frame is not always audio (`session.update` routinely arrives first), and -/// there is no reason to wait for audio specifically before a unit exists to hold the session's -/// facts and price its hold. -fn ingress_from_client_event<'u>( - event: IrClientEvent, - state: &mut VoiceSessionState, - dialect: Dialect, - wire: &'u [u8], - ctx: &Ctx<'u>, -) -> Result, Decode> { - // A tool RESULT is not a frame of the conversation; it is the answer to a call the upstream - // opened, and it belongs to that call's own unit. Relayed onto the open turn — which is what - // every client tool event did — it arrived under the turn's hold naming the turn's correlation, - // so the one unit actually waiting for it could not be told it had been answered, and the reply - // and the wait passed each other on the same session. - if let IrClientEvent::Tool(IrDuplexTool::CallResult { call_id, .. }) = &event { - let id = ctx - .arena() - .alloc_str(call_id) - .map_err(|_| Decode::Oversize)?; - let mut facts = Facts::new(); - facts - .set(meta::FACT_CALL_ID, FactValue::Str(id)) - .map_err(|_| Decode::Oversize)?; - return Ok(Ingress::Frame { - // The identifier as itself, under the key the leg named. Whole value, because the pair - // this has to tell apart is two calls open on one session at one moment. - for_: Some(CorrelationRef { - fact_key: FACT_TOOL_CORRELATION, - value: CorrelationValue::Str(id), - }), - relay: ScratchBytes::new(&[]), - facts: Box::new(facts), - }); - } - let (relay, interrupt_ms, audio_ms) = match &event { - IrClientEvent::AudioFrame(f) => { - // See the module doc comment: the uplink format is assumed PCM16 for this estimate. - // The figure is not accumulated here — see `encode_ingress_frame`, which counts what - // is actually relayed on the half the response step later reads it back from. - let ms = AudioFormat::Pcm16.bytes_to_ms(f.media.len() as u64); - let bytes = ctx - .arena() - .alloc_bytes(&f.media) - .map_err(|_| Decode::Oversize)?; - (bytes, None, Some(ms)) - } - IrClientEvent::Control(IrDuplexControl::ItemTruncate { - audio_played_ms, .. - }) => (ScratchBytes::new(&[]), Some(*audio_played_ms), None), - IrClientEvent::Control(_) | IrClientEvent::Tool(_) => (ScratchBytes::new(&[]), None, None), - }; - open_or_relay(state, dialect, wire, relay, interrupt_ms, audio_ms, ctx) -} - -/// Open a fresh turn (this is its first frame) or relay onto the one already open, attaching the -/// interrupt fact where the caller says the event carried one, either way. -/// -/// A frame that CARRIES an interrupt always opens. The scheduler reads the interrupt off an OPEN — -/// that is the only shape whose dispatch reaches the compare-and-set that supersedes the unit in -/// flight — so a barge-in delivered as one more frame of the very turn it interrupts named the -/// superseded unit to nobody: the interrupted turn kept the direction's slot and kept pricing while -/// the caller was already talking over it, and the turn that took over could never open. -fn open_or_relay<'u>( - state: &mut VoiceSessionState, - dialect: Dialect, - open_wire: &'u [u8], - relay: ScratchBytes<'u>, - interrupt_ms: Option, - audio_ms: Option, - ctx: &Ctx<'u>, -) -> Result, Decode> { - let mut facts = Facts::new(); - if let Some(ms) = interrupt_ms { - let _ = facts.set( - meta::FACT_INTERRUPT_AUDIO_PLAYED_MS, - FactValue::Int(i64::try_from(ms).unwrap_or(i64::MAX)), - ); - // The interrupted turn ends here; the frame that interrupted it opens the next one, and - // carries the fact that says which one it took over from. The interrupted turn is superseded, - // not answered: no usage report ever meters it, so its counters — the audio the caller - // already spoke and the tool calls the upstream already opened — are CARRIED onto the turn - // that takes over rather than reset with the correlation. Resetting them here dropped every - // barged-in second and call from the ledger; carried, each is emitted exactly once, on - // whichever terminal (usage, error or `stop`) next closes a turn on this state. - let carried = state.close_turn(); - state.turn = carried; - } - if !state.turn_open { - // The opening frame becomes the unit's own egress body rather than travelling through the - // relay seam, so what it carries of the caller's audio is stated on the draft. Every later - // frame of the turn is counted where it relays. - if let Some(ms) = audio_ms { - let _ = facts.set( - meta::FACT_AUDIO_MS_IN, - FactValue::Int(i64::try_from(ms).unwrap_or(i64::MAX)), - ); - } - let correlation = state.open_turn(); - let _ = facts.set(meta::FACT_DIALECT, FactValue::Str(dialect.name())); - // The opening frame's body is the client's ORIGINAL wire frame, not the extracted relay: - // `encode_egress` re-frames it onto the upstream dialect (see its note), so what it carries - // must be the full client message (a `session.update` config, a carrier `media` envelope), - // never the empty/raw relay a byte pass-through would have shipped upstream unshaped. - let ir = view(open_wire, ctx)?; - Ok(Ingress::Open(Box::new(UnitDraft { - op: OpClassId::new("duplex_turn"), - body_ir: ir, - correlates: None, - correlation_out: Some(correlation), - facts, - }))) - } else { - Ok(Ingress::Frame { - for_: state.turn_correlation, - relay, - facts: Box::new(facts), - }) - } -} - -/// Write a closed turn's own counters under the keys `meter` reads them back from. -/// -/// One writer for every ending that takes the counters out of the state — a usage report, an upstream -/// error and a carrier `stop` — so no ending can take them and forget to say what they were. -fn set_counter_facts( - facts: &mut Facts<'_>, - counters: crate::session::TurnCounters, -) -> Result<(), Decode> { - facts - .set( - meta::FACT_AUDIO_MS_IN, - FactValue::Int(i64::try_from(counters.audio_ms_in).unwrap_or(i64::MAX)), - ) - .map_err(|_| Decode::Oversize)?; - facts - .set( - meta::FACT_TOOL_CALLS, - FactValue::Int(i64::try_from(counters.tool_calls).unwrap_or(i64::MAX)), - ) - .map_err(|_| Decode::Oversize)?; - Ok(()) -} - -/// Turn one decoded server→client IR event into a `Progress` answer, rendering the client-shaped -/// bytes immediately (see the module doc comment's `encode_response` note). -fn progress_from_server_event<'u>( - event: IrServerEvent, - state: &mut VoiceSessionState, - client_dialect: Dialect, - ctx: &Ctx<'u>, -) -> Result, Decode> { - let for_ = state.turn_correlation; - match event { - IrServerEvent::SessionCreated { .. } | IrServerEvent::RateLimits => Ok(Progress::Discard { - reason: DiscardCode::Unsupported, - }), - IrServerEvent::Tool(IrDuplexTool::CallOpen { call_id, name, .. }) => { - // A tool call is ANNOUNCED here. Its arguments stream in as `CallArgs` and it is - // dispatched to its executor ONCE, on `CallClose`, with the name and the ACCUMULATED - // arguments — a call dispatched with no arguments is a different call, which is what the - // old open-mints-empty path did. Announcing opens nothing on its own; the name is - // remembered so `CallClose` (which carries none) can name the executor call. - state.turn.open_tool_call(); - state.codec.remember_call_name(&call_id, &name); - Ok(Progress::Discard { - reason: DiscardCode::Unsupported, - }) - } - IrServerEvent::Tool(IrDuplexTool::CallArgs { - call_ref, - json_delta, - .. - }) => { - // Accumulate one streamed argument fragment. `push_call_args` appends a fragment and - // REPLACES on a whole JSON object (the complete arguments a streamed call states when it - // closes, or an atomic call's `args`), so the OpenAI done-repeat and the Gemini atomic - // call both land the right bytes rather than a fragment spliced onto its own prefix. - state.codec.push_call_args(call_ref, &json_delta); - Ok(Progress::Discard { - reason: DiscardCode::Unsupported, - }) - } - IrServerEvent::Tool(IrDuplexTool::CallClose { call_ref, call_id }) => { - // The arguments are complete: dispatch the tool call as its own `OneShot` unit, carrying - // the tool name and the arguments the model asked for, correlated by the wire call id so - // its reply reaches it. This is where a tool call becomes visible, priced and audited as - // its own `tool_call` unit. - let mut facts = Facts::new(); - let name = state.codec.call_name(&call_id).to_string(); - if !name.is_empty() { - let name_arena = ctx.arena().alloc_str(&name).map_err(|_| Decode::Oversize)?; - facts - .set(meta::FACT_TOOL_NAME, FactValue::Str(name_arena)) - .map_err(|_| Decode::Oversize)?; - } - // The call identifier travels as itself. It is a string on the wire and it is a string - // here, allocated in the unit's own arena — a fold into sixty four bits would be one - // collision away from answering a tool call with another's reply. - let call_id_arena = ctx - .arena() - .alloc_str(&call_id) - .map_err(|_| Decode::Oversize)?; - facts - .set(meta::FACT_CALL_ID, FactValue::Str(call_id_arena)) - .map_err(|_| Decode::Oversize)?; - // The arguments the model asked for. An accumulation that never closed readable (past the - // byte ceiling, or a call joined mid-stream with nothing decodable) leaves an empty body - // rather than inventing a null argument list the model never sent. - let body_ir = match state.codec.take_call_args(call_ref) { - Some(args) => { - let bytes = serde_json::to_vec(&args).map_err(|_| Decode::Malformed)?; - let arena = ctx - .arena() - .alloc_bytes(&bytes) - .map_err(|_| Decode::Oversize)?; - view(arena.as_slice(), ctx)? - } - None => Ir::empty(), - }; - Ok(Progress::OneShot(Box::new(UnitDraft { - op: OpClassId::new("tool_call"), - body_ir, - correlates: None, - correlation_out: Some(CorrelationRef { - fact_key: FACT_TOOL_CORRELATION, - value: CorrelationValue::Str(call_id_arena), - }), - facts, - }))) - } - IrServerEvent::Tool(_) => Ok(Progress::Frame { - for_, - r: Box::new(Response { - ir: Ir::empty(), - finish: FinishClass::Partial, - facts: Facts::new(), - }), - }), - IrServerEvent::SpeechStarted { .. } => { - let ms = state.codec.flush_playback(); - let mut facts = Facts::new(); - facts - .set( - meta::FACT_INTERRUPT_AUDIO_PLAYED_MS, - FactValue::Int(i64::try_from(ms).unwrap_or(i64::MAX)), - ) - .map_err(|_| Decode::Oversize)?; - Ok(Progress::Frame { - for_, - r: Box::new(Response { - ir: Ir::empty(), - finish: FinishClass::Partial, - facts, - }), - }) - } - IrServerEvent::SpeechStopped { .. } | IrServerEvent::AudioDone { .. } => { - Ok(Progress::Frame { - for_, - r: Box::new(Response { - ir: Ir::empty(), - finish: FinishClass::Partial, - facts: Facts::new(), - }), - }) - } - IrServerEvent::AudioFrame(f) => { - let writer = writer_for(client_dialect); - let media_len = f.media.len() as u64; - // One buffer, filled and then copied into the arena. The identifier is BORROWED from the - // binding the `start` event made: it is the same string on every frame of the call, and - // a call sends fifty frames a second, so cloning it per frame is fifty copies a second - // of a string that never changes. - // - // Each arm reaches the arena on its own rather than through one shared owning type: the - // carrier's renderer refills a buffer the SESSION holds, and handing its bytes through a - // per-frame owner put back exactly the allocation per frame the renderer exists to - // remove — the renderer's committed zero was true of the renderer and false of its only - // caller. - let bytes = match client_dialect { - Dialect::TwilioMediaStreams => { - let mulaw = ulaw::encode_frame(&f.media); - // Taken and handed straight back, because the identifier beside it is borrowed - // from the same state. - let mut out = core::mem::take(&mut state.render_buf); - let sid = state.twilio_stream_sid.as_deref().unwrap_or_default(); - twilio::encode_media_into(&mut out, sid, &mulaw); - let bytes = ctx.arena().alloc_bytes(&out).map_err(|_| Decode::Oversize); - state.render_buf = out; - bytes? - } - _ => { - let rendered = writer - .write_down(IrServerEvent::AudioFrame(f), &mut state.codec) - .ok_or(Decode::Malformed)? - .0; - ctx.arena() - .alloc_bytes(&rendered) - .map_err(|_| Decode::Oversize)? - } - }; - let mut facts = Facts::new(); - facts - .set( - meta::EGRESS_PACING_FACT_KEY, - FactValue::Int(i64::try_from(state.codec.played_ms()).unwrap_or(i64::MAX)), - ) - .map_err(|_| Decode::Oversize)?; - let _ = media_len; - Ok(Progress::Frame { - for_, - r: Box::new(Response { - ir: view(bytes.as_slice(), ctx)?, - finish: FinishClass::Partial, - facts, - }), - }) - } - IrServerEvent::Usage(usage) => { - let counters = state.close_turn(); - let mut facts = Facts::new(); - let ints: [(&str, i64); 7] = [ - ( - meta::FACT_AUDIO_TOKENS_IN, - i64::try_from(usage.audio_in).unwrap_or(i64::MAX), - ), - ( - meta::FACT_AUDIO_TOKENS_OUT, - i64::try_from(usage.audio_out).unwrap_or(i64::MAX), - ), - // The two halves travel separately. Summing them was what forced the whole figure - // through a single input-direction class; the upstream reports them apart and this - // plane keeps them apart, so the emitted half prices as output. - ( - meta::FACT_TEXT_TOKENS_IN, - i64::try_from(usage.text_in).unwrap_or(i64::MAX), - ), - ( - meta::FACT_TEXT_TOKENS_OUT, - i64::try_from(usage.text_out).unwrap_or(i64::MAX), - ), - ( - meta::FACT_CACHED_TOKENS, - i64::try_from(usage.cached).unwrap_or(i64::MAX), - ), - ( - meta::FACT_AUDIO_MS_IN, - i64::try_from(counters.audio_ms_in).unwrap_or(i64::MAX), - ), - ( - meta::FACT_TOOL_CALLS, - i64::try_from(counters.tool_calls).unwrap_or(i64::MAX), - ), - ]; - for (k, v) in ints { - facts - .set(k, FactValue::Int(v)) - .map_err(|_| Decode::Oversize)?; - } - Ok(Progress::Terminal { - for_, - r: Box::new(Response { - ir: Ir::empty(), - finish: FinishClass::TurnComplete, - facts, - }), - }) - } - IrServerEvent::Error { code, message } => { - // A turn that ends in an upstream error consumed the same uplink audio and ran the same - // tool calls as one that ends in a usage report. The kernel-side counters are the only - // record of either, and closing the turn is what takes them: dropping them here would - // meter the failed turn at zero and hand back for free every second the caller spoke. - let counters = state.close_turn(); - let mut facts = Facts::new(); - let code_arena = ctx.arena().alloc_str(&code).map_err(|_| Decode::Oversize)?; - let message_arena = ctx - .arena() - .alloc_str(&message) - .map_err(|_| Decode::Oversize)?; - facts - .set(meta::FACT_ERROR_CODE, FactValue::Str(code_arena)) - .map_err(|_| Decode::Oversize)?; - facts - .set(meta::FACT_ERROR_MESSAGE, FactValue::Str(message_arena)) - .map_err(|_| Decode::Oversize)?; - set_counter_facts(&mut facts, counters)?; - Ok(Progress::Terminal { - for_, - r: Box::new(Response { - ir: Ir::empty(), - finish: FinishClass::Error, - facts, - }), - }) - } - } -} diff --git a/crates/busbar-plane-streaming/src/register.rs b/crates/busbar-plane-streaming/src/register.rs index 91325136ec..72a57631dc 100644 --- a/crates/busbar-plane-streaming/src/register.rs +++ b/crates/busbar-plane-streaming/src/register.rs @@ -44,8 +44,8 @@ //! //! The plane opens NO socket (#7): its whole closure is pure. The duplex WebSocket ingress, the TLS //! termination and the SSE/duplex reframe are the composition root's host seams — the same -//! `busbar-transport-ws` edge the `plane-streaming` feature pulls (`busbar-voice?/runtime` arms the neutral -//! full-duplex WS transport; the plane SELECTS `Transport::WebSocket` and the host opens it). The +//! `busbar-transport-ws` edge the `plane-streaming` feature pulls (the neutral full-duplex WS +//! transport; the plane SELECTS `Transport::WebSocket` and the host opens it). The //! `plane-streaming` feature forwards to that same transport edge; the plane holds no socket plumbing. //! //! ## S4 — HOT-ABI / SDK (compiled-in AND dropped-in) diff --git a/crates/busbar-plane-streaming/src/request_unit.rs b/crates/busbar-plane-streaming/src/request_unit.rs new file mode 100644 index 0000000000..9558d3aee5 --- /dev/null +++ b/crates/busbar-plane-streaming/src/request_unit.rs @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! ONE REQUEST UNIT AS THE KERNEL'S ROUTE PUMP DRIVES IT (`BUSBAR-1.6.0.md` Part 3, section 12): +//! the pieces of a one-request door (the ephemeral-secret mint, the SDP offer, the metadata +//! document), in the order the pump pushes them, and what the plane answers each with. +//! +//! Per attempt the pump pushes an ATTEMPT piece first, then the caller's body (re-pushed on every +//! attempt), then the far end's answer. The plane answers: +//! +//! | piece | mint | SDP offer | metadata | +//! |---|---|---|---| +//! | ATTEMPT | the mint request, whole | the request head; its body follows | the document | +//! | the caller's body | nothing (the mint reads no caller body) | relayed to the far end as it arrived | the document, if not yet answered | +//! | the far end's answer | gathered; on its last piece, the caller's answer | the same | refused: it dials nothing | +//! +//! Every answer is plain data in the plane's own vocabulary; writing it into the host's buffers is +//! the door's job. A new ATTEMPT forgets what the previous attempt's far end said. + +use crate::codec::ir::config::SessionConfig; +use crate::driven::{ + mint_failed, mint_reply, sdp_reply, Attempt, Door, Reply, Steps, FIELD_LOCATION, +}; + +/// Who pushed a piece. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum From { + /// The caller: the request body. + Caller, + /// The far end: its answer. + FarEnd, + /// The kernel: an ATTEMPT, numbered from `1`. + Kernel(u32), +} + +/// One piece, as the door read it off the crossing. +#[derive(Debug, Clone, Copy)] +pub struct Piece<'a> { + /// Who pushed it. + pub from: From, + /// Its bytes. + pub bytes: &'a [u8], + /// On the far end's first piece: the status it answered. + pub status: Option, + /// No piece of this `from` follows. + pub last: bool, + /// On the far end's first piece: the response head fields the plane's need keeps, names + /// lower-case. + pub head: &'a [(&'a [u8], &'a [u8])], +} + +/// The plane's answer to one piece. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Answer { + /// Nothing to emit yet. + Nothing, + /// The request this attempt sends the far end; a body that follows arrives as [`Answer::ToFarEnd`]. + Attempt(Attempt), + /// More of the request's body, bound for the far end. + ToFarEnd(Vec), + /// The caller's whole answer: the unit is done. + ToCaller(Reply), + /// The piece is one this door never takes. + Refused, +} + +/// One unit on a one-request door. +#[derive(Debug, Clone)] +pub struct RequestUnit { + door: Door, + locked: SessionConfig, + caller_ref: Option, + status: Option, + location: Option, + far: Vec, + answered: bool, +} + +impl RequestUnit { + /// Open the plane's state for one kernel unit on `door` (the unit's identity is the kernel's; + /// the door keys this state by it): minting over the `locked` session params, naming the caller + /// by `caller_ref` (the kernel's reference, never the principal). `None` for a door that opens + /// a live session. + #[must_use] + pub fn open(door: Door, locked: SessionConfig, caller_ref: Option) -> Option { + if door.is_session() { + return None; + } + Some(Self { + door, + locked, + caller_ref, + status: None, + location: None, + far: Vec::new(), + answered: false, + }) + } + + /// Its door. + #[must_use] + pub const fn door(&self) -> Door { + self.door + } + + /// `true` once the caller's answer has been given. + #[must_use] + pub const fn answered(&self) -> bool { + self.answered + } + + fn answer(&mut self, reply: Reply) -> Answer { + self.answered = true; + Answer::ToCaller(reply) + } + + /// The plane's answer to `piece`. + pub fn on_piece(&mut self, piece: Piece<'_>) -> Answer { + if self.answered { + return Answer::Nothing; + } + match (self.door, piece.from) { + (_, From::Kernel(0)) => Answer::Refused, + (_, From::Kernel(_)) => { + self.status = None; + self.location = None; + self.far.clear(); + match self + .door + .route(&self.locked, self.caller_ref.as_deref(), &[]) + { + Ok(Some(attempt)) => Answer::Attempt(attempt), + Ok(None) => Answer::Refused, + Err(reason) => { + let reply = mint_failed(&reason); + self.answer(reply) + } + } + } + (Door::Sdp, From::Caller) => { + if piece.bytes.is_empty() { + Answer::Nothing + } else { + Answer::ToFarEnd(piece.bytes.to_vec()) + } + } + (_, From::Caller) => Answer::Nothing, + (_, From::FarEnd) => self.far_end_piece(piece), + } + } + + fn far_end_piece(&mut self, piece: Piece<'_>) -> Answer { + if let Some(status) = piece.status { + self.status = Some(status); + self.location = piece + .head + .iter() + .find(|(name, _)| name.eq_ignore_ascii_case(FIELD_LOCATION.as_bytes())) + .and_then(|(_, value)| std::str::from_utf8(value).ok()) + .map(str::to_owned); + } + self.far.extend_from_slice(piece.bytes); + if !piece.last { + return Answer::Nothing; + } + let Some(status) = self.status else { + return Answer::Refused; + }; + let reply = match self.door { + Door::Mint => mint_reply(status, &self.far), + _ => sdp_reply(status, self.location.as_deref(), &self.far), + }; + self.answer(reply) + } +} + +#[cfg(test)] +#[path = "tests/request_unit_tests.rs"] +mod tests; diff --git a/crates/busbar-plane-streaming/src/session.rs b/crates/busbar-plane-streaming/src/session.rs index 495de2f18d..6da37c3979 100644 --- a/crates/busbar-plane-streaming/src/session.rs +++ b/crates/busbar-plane-streaming/src/session.rs @@ -1,35 +1,11 @@ -//! The plane's per-connection codec state — the one place cross-frame state may live. -//! -//! `busbar_contract::plane::PlaneSessionState` is a type-erased `Box` the kernel -//! holds one half of per connection: one for the client, one more per upstream a session dials. This -//! module is the concrete type this plane wraps in it. +//! The plane's per-turn bookkeeping: the counters a turn keeps that no usage report carries, and a +//! closed turn's counts per declared class. -use crate::codec::ir::{AudioFormat, DecodeState, IrClientEvent, IrDuplexUsage}; -use busbar_contract::ids::{CorrelationRef, CorrelationValue, MeterClassId}; +use crate::codec::ir::{AudioFormat, IrDuplexUsage}; +use busbar_contract::ids::MeterClassId; -use crate::claims::Dialect; use crate::meta; -/// One pending, already-decoded IR event, stashed across the two-call boundary a step pair leaves -/// open. -/// -/// `decode_ingress`/`encode_ingress_frame` are two separate calls the kernel makes about the SAME -/// inbound frame — the first says what it means, the second says what to send onward. The codecs' -/// `read_up` method is stateful (it advances the per-session frame sequence), so calling it a second -/// time to re-derive what the first call already decoded would double-count that state. Stashing the -/// already-decoded event here and consuming it once is what keeps the codec state's counters correct -/// across the split. -/// -/// There is ONE arm, and there was only ever one reason for a second: the downlink split needed a -/// stash because a later step could not see what decode had determined. It can now — a unit carries -/// its draft's facts — and `encode_response` renders the bytes `decode_response` already produced, -/// so the egress arm existed for a problem that is no longer the shape of the surface. -#[derive(Debug, Clone)] -pub enum Pending { - /// A client→server event decoded at `decode_ingress`, consumed at `encode_ingress_frame`. - Ingress(IrClientEvent), -} - /// The plane's own bookkeeping for the current, still-open turn. /// /// Closed (its counts reset to zero) every time a turn is ANSWERED — a usage report, an upstream error or @@ -125,7 +101,7 @@ impl TurnCounters { /// alike). Counts only: no rate is read and no figure results. /// /// This is the plane's one reading of a turn for a host that drives the session itself rather than -/// through the unit loop; `tests::codec` holds it to `meter` over the same turn. +/// through the unit loop. #[must_use] pub fn class_counts( usage: Option<&IrDuplexUsage>, @@ -147,85 +123,3 @@ pub fn class_counts( .filter(|(_, n)| *n != 0) .collect() } - -/// The codec state one connection half of a voice session holds. -/// -/// One value per [`busbar_contract::plane::PlaneSessionState`] half: the client half opened by -/// this plane's `SessionPlane::open_session`, one more per upstream opened by its -/// `open_upstream` (see `crate::plane`). Everything here is exactly what a plane may hold across -/// frames and nothing else — no connection, no clock, no credential. -#[derive(Debug, Default)] -pub struct VoiceSessionState { - /// The dialect this half of the connection speaks. `None` only in the sliver of time before the - /// first frame has named one; every method that needs it treats an absent dialect as a decode - /// failure rather than guessing. - pub dialect: Option, - /// The shared duplex codec's per-session state (frame sequencing, the `CallRef` correlation - /// table, the negotiated output format, and the barge-in playback-position bookkeeping). Reused - /// for both codec-backed dialects (`OpenAI Realtime`, `Gemini Live`): the shared IR is what - /// makes one state shape valid for either. - pub codec: DecodeState, - /// Whether the current turn is open (a unit has been opened and not yet closed). - pub turn_open: bool, - /// The correlation the currently open turn answers frames under, once one has been minted. - pub turn_correlation: Option>, - /// The next turn identity to mint. Monotonic per session; a duplex session opens and closes many - /// turns in sequence, and each needs a correlation value distinct from the last so a stray late - /// frame from a just-closed turn cannot be mistaken for one belonging to the next. - pub next_turn_id: u64, - /// This turn's own derived counters (see [`TurnCounters`]). - pub turn: TurnCounters, - /// Twilio's `streamSid` for this connection, bound at the `start` event and checked against - /// every later `media` frame — the forgery/replay guard the architecture note for this dialect - /// names. - pub twilio_stream_sid: Option, - /// The one already-decoded event a two-call step pair is carrying across (see [`Pending`]). - pub pending: Option, - /// The buffer one downlink audio frame is rendered into, held across frames. - /// - /// The renderer this crate carries for the carrier dialect clears and refills a buffer the - /// caller owns, and its committed cost is stated for a buffer that has already carried a frame - /// — which is the only shape that costs nothing. A call sends fifty downlink frames a second, - /// so a fresh vector per frame is fifty allocations a second the renderer was written to - /// remove. This is where the one buffer lives. - pub render_buf: Vec, -} - -impl VoiceSessionState { - /// A fresh state already bound to a known dialect — what a session's client half opens with, - /// once the claim that matched the connection is known. - #[must_use] - pub fn for_dialect(dialect: Dialect) -> Self { - Self { - dialect: Some(dialect), - ..Self::default() - } - } - - /// The fact key a turn's correlation is minted under. Declared once here so the mint site and - /// every read site agree. - pub const TURN_FACT_KEY: &'static str = "turn_id"; - - /// Open a fresh turn, minting the correlation later frames must carry to relay onto it. - pub fn open_turn(&mut self) -> CorrelationRef<'static> { - let id = self.next_turn_id; - self.next_turn_id += 1; - let correlation = CorrelationRef { - fact_key: Self::TURN_FACT_KEY, - value: CorrelationValue::Num(id), - }; - self.turn_open = true; - self.turn_correlation = Some(correlation); - correlation - } - - /// Close the current turn, snapshotting and resetting its counters. - /// - /// Returns the counters as they stood at close, which is what feeds the response facts the - /// `meter` step reads back out (`crate::plane`'s `decode_response`/`meter`). - pub fn close_turn(&mut self) -> TurnCounters { - self.turn_open = false; - self.turn_correlation = None; - self.turn.close() - } -} diff --git a/crates/busbar-plane-streaming/src/session_door.rs b/crates/busbar-plane-streaming/src/session_door.rs new file mode 100644 index 0000000000..92b62871ba --- /dev/null +++ b/crates/busbar-plane-streaming/src/session_door.rs @@ -0,0 +1,418 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! LIVE SESSIONS THROUGH THE DOOR (`BUSBAR-1.6.0.md` Part 3, section 12, "Duplex sessions" (K6); +//! THE DESIGN, section 7, "A session is one unit with one line"). +//! +//! A piece that names a stream is a live session's. The door keeps one [`Live`] per stream, built +//! at the stream's first piece over the newest live generation's `streams:` section, and answers +//! each piece in the driver's duplex vocabulary: +//! +//! - A CALLER piece goes through the session ([`SessionUnit::from_caller`]); a frame it owes the +//! far end is that answer's `EMIT_TO_FAR_END` (one turn). The caller's last piece ends the +//! session: its open turn settles once and the answer is `EMIT_DONE`. +//! - A FAR-END piece goes through the session ([`SessionUnit::from_far_end`]); a frame it owes the +//! caller is that answer's emission. +//! - ONE FRAME PER ANSWER. Every frame keeps its own message boundary, so a piece that leaves more +//! than one frame owed (a barge-in's cancel and truncate, a caller frame and its echo) answers the +//! first and queues the rest. A queued frame is the session's unsolicited output: the door wakes +//! the instance's driver ticket, `drive` names the session, and each collection (`FROM_KERNEL`, +//! no bytes, no attempt) answers the next queued frame, toward the side it is bound for. +//! - EVERY ANSWER REPORTS THE SESSION'S CUMULATIVE UNITS, the six classes the plane counts, and, +//! once the far end has first answered the session, its fee unit ([`PER_SESSION_CLASS`], `1`): +//! a session whose far end never answered (its open failed) reports none, and its fee is refunded. +//! The kernel checkpoints them and writes the session's one line at its end; the plane holds no +//! reservation and never cuts on money. +//! - THE WALL-CLOCK CEILING (`streams.session_max_secs`, Q21a) is measured on the kernel's tick +//! clock: while a live generation configures one the door asks to be ticked every +//! [`CEILING_TICK_NS`], a session's clock starts at the first tick after its first piece, and a +//! session past its ceiling is told why in its dialect and ends once its queued frames are +//! collected. +//! - A CANCEL on either side's ticket ends the session: it is forgotten, and the kernel's cleanup +//! writes its line. + +use std::collections::{BTreeMap, HashMap, VecDeque}; + +use busbar_contract::abi::mechanism::ticket::Ticket; + +use crate::codec::ir::codec::OpenAiRealtimeCodec; +use crate::codec::ir::config::SessionConfig; +use crate::codec::ir::GeminiLiveCodec; +use crate::config::StreamsCfg; +use crate::driven::{Door, Steps}; +use crate::session_params::g711_config; +use crate::session_unit::{CumulativeUnits, Plan, SessionUnit}; + +/// Nanoseconds per second, on the kernel's tick clock. +const NS_PER_SEC: u64 = 1_000_000_000; + +/// How often the door asks to be ticked while a live generation configures a session ceiling: a +/// session's clock starts, and its ceiling is enforced, at most this late. +/// +/// The clock starts at a tick rather than at the first piece because a piece carries no clock +/// reading on the tick clock, and a ceiling compared across two clocks is no ceiling. +pub const CEILING_TICK_NS: u64 = NS_PER_SEC; + +/// The tail's class index of the session's fee unit (`per_session`), after the six counted +/// classes ([`crate::session_unit::CLASSES`]). +pub const PER_SESSION_CLASS: u32 = crate::session_unit::CLASSES.len() as u32; + +/// The most live sessions the door keeps at once; past it, a new stream is refused. +pub const MAX_SESSIONS: usize = 4096; + +/// The side a session's frame is bound for. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Side { + /// The far end: one turn. + FarEnd, + /// The caller. + Caller, +} + +/// The session over its door's codec. +#[derive(Debug)] +enum Unit { + /// The OpenAI Realtime far end (the sideband socket and the telephony socket). + Realtime(SessionUnit), + /// The Gemini Live far end. + Gemini(SessionUnit), +} + +macro_rules! each { + ($unit:expr, |$u:ident| $body:expr) => { + match $unit { + Unit::Realtime($u) => $body, + Unit::Gemini($u) => $body, + } + }; +} + +/// What one session answer carries. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Emit { + /// The frame this answer writes, and the side it is bound for; `None` = nothing. + pub frame: Option<(Side, Vec)>, + /// The session is over: this answer is its last. + pub done: bool, +} + +/// ONE LIVE SESSION on the door. +#[derive(Debug)] +pub struct Live { + unit: Unit, + /// Frames owed a side, oldest first, past the one each answer carries. + queued: VecDeque<(Side, Vec)>, + /// The session ended; it is over once its queued frames are collected. + ending: bool, + /// The wall-clock ceiling of the generation it opened under, in seconds. + ceiling_secs: Option, + /// When its clock started, on the tick clock; `None` until the first tick after its open. + started_ns: Option, + /// The far end has answered the session: its fee unit was incurred. + answered: bool, + /// What the frame a caller-side answer is writing still owes the reply buffer. + pub(crate) near: crate::piece::Owed, + /// What the frame a far-side answer is writing still owes the reply buffer. + pub(crate) far: crate::piece::Owed, + /// A caller-side answer that did not fit its buffers: the re-call carries the same piece, + /// which is answered from here rather than read twice. + pub(crate) short_near: Option, + /// The same, for a far-side answer. + pub(crate) short_far: Option, +} + +impl Live { + /// The session a stream's first piece on `door` opens, under `cfg`; `None` for a door that + /// answers one request. The telephony door locks the carrier's µ-law on both legs; the other + /// doors lock the section's session. + #[must_use] + pub fn open(door: Door, cfg: &StreamsCfg) -> Option { + Self::open_locked(door, cfg, None) + } + + /// The session params `door` locks under `cfg`, as the open's hooks are shown them: the + /// carrier's µ-law for the telephony door, the section's session for every other. + #[must_use] + pub fn locked(door: Door, cfg: &StreamsCfg) -> SessionConfig { + match door { + Door::Twilio => g711_config(), + _ => cfg.session.clone(), + } + } + + /// [`Self::open`], locked to `rewritten` where a session-open hook rewrote the params + /// ([`Self::locked`]) this session opens under. + #[must_use] + pub fn open_locked( + door: Door, + cfg: &StreamsCfg, + rewritten: Option, + ) -> Option { + let ceiling = cfg.session_max_secs.map(|s| u64::from(s.get())); + let locked = rewritten.unwrap_or_else(|| Self::locked(door, cfg)); + let unit = match door { + Door::Sideband => Unit::Realtime(SessionUnit::open( + OpenAiRealtimeCodec, + locked, + false, + 0, + None, + )), + Door::Twilio => Unit::Realtime(SessionUnit::open( + OpenAiRealtimeCodec, + locked, + true, + 0, + None, + )), + Door::Gemini => { + Unit::Gemini(SessionUnit::open(GeminiLiveCodec, locked, false, 0, None)) + } + Door::Mint | Door::Sdp => return None, + }; + Some(Live { + unit, + queued: VecDeque::new(), + ending: false, + ceiling_secs: ceiling, + started_ns: None, + answered: false, + near: crate::piece::Owed::default(), + far: crate::piece::Owed::default(), + short_near: None, + short_far: None, + }) + } + + /// Queue what `plan` owes either side, the far end's frames first. + fn take(&mut self, plan: Plan) { + self.queued + .extend(plan.to_far_end.into_iter().map(|f| (Side::FarEnd, f))); + self.queued + .extend(plan.to_caller.into_iter().map(|f| (Side::Caller, f))); + self.ending |= plan.end; + } + + /// A caller piece; `last` = the caller's side ended. + pub fn from_caller(&mut self, bytes: &[u8], last: bool) { + let plan = each!(&mut self.unit, |u| { + let mut plan = if bytes.is_empty() { + Plan::default() + } else { + u.from_caller(bytes) + }; + if last { + let ended = u.end(); + plan.end = ended.end; + } + plan + }); + self.take(plan); + } + + /// A far-end piece, at `now_ms`. The first one incurs the session's fee unit. + pub fn from_far_end(&mut self, bytes: &[u8], now_ms: u64) { + self.answered = true; + let plan = each!(&mut self.unit, |u| u.from_far_end(bytes, now_ms)); + self.take(plan); + } + + /// The tick clock reads `now_ns`: the session's clock starts on its first tick, and a session + /// past its ceiling is told why and ends. Answers whether it now owes a frame or its end. + pub fn tick(&mut self, now_ns: u64) -> bool { + let started = *self.started_ns.get_or_insert(now_ns); + let Some(ceiling) = self.ceiling_secs else { + return false; + }; + if self.ending || now_ns.saturating_sub(started) < ceiling.saturating_mul(NS_PER_SEC) { + return false; + } + let plan = each!(&mut self.unit, |u| u.close_at_ceiling(ceiling)); + self.take(plan); + true + } + + /// The next answer: one queued frame, or, with none queued, the session's end when it ended. + /// A caller-side answer (`near`) carries a frame toward either side, oldest first, and the + /// session's end; a far-side answer reaches only the caller, so it carries the oldest frame + /// bound for the caller and never the end (the caller side answers that, once collected). + pub fn next(&mut self, near: bool) -> Emit { + let at = if near { + (!self.queued.is_empty()).then_some(0) + } else { + self.queued + .iter() + .position(|(side, _)| *side == Side::Caller) + }; + match at.and_then(|i| self.queued.remove(i)) { + Some(frame) => Emit { + frame: Some(frame), + done: false, + }, + None => Emit { + frame: None, + done: near && self.ending, + }, + } + } + + /// Whether the session owes an answer of its own: a queued frame, or its end. + #[must_use] + pub fn ready(&self) -> bool { + !self.queued.is_empty() || self.ending + } + + /// The session's cumulative units, as the door reports them: (class index, amount), the zero + /// classes left out; the fee unit is `1` once the far end has answered. + #[must_use] + pub fn units(&self) -> Vec<(u32, u64)> { + let units: CumulativeUnits = each!(&self.unit, |u| u.units()); + let mut out = Door::Sideband.meter(&units); + if self.answered { + out.push((PER_SESSION_CLASS, 1)); + } + out + } +} + +/// THE SESSION-OPEN PARAMS A COMMITTED HOOK REWRITE PRODUCED (the session-open hook projection the +/// retired streams crate applied, verbatim: ARCHITECT Q-L5B-PROJECT), or why they cannot be used. +/// The rewrite is a JSON object PATCHED over the `locked` params as the hook was shown them: a key +/// the hook named wins, a key it did not name keeps the locked value (naming a key with `null` +/// clears it). The merge comes before the decode, so a patch that names one bad field refuses the +/// open; so does output that is not JSON, or JSON that is not an object. A rewrite busbar cannot +/// read back is never opened as if it had not been made. +/// +/// # Errors +/// +/// The output is not JSON, not an object, or not a session config once merged. +pub fn committed_session_config( + locked: &SessionConfig, + rewrite: &[u8], +) -> Result { + let patch: serde_json::Value = + serde_json::from_slice(rewrite).map_err(|e| format!("the output is not JSON: {e}"))?; + let serde_json::Value::Object(patch) = patch else { + return Err("the output is JSON but not a session-params object".to_string()); + }; + let Ok(serde_json::Value::Object(mut merged)) = serde_json::to_value(locked) else { + return Err( + "the plane's own locked session params did not project to an object".to_string(), + ); + }; + merged.extend(patch); + serde_json::from_value::(serde_json::Value::Object(merged)) + .map_err(|e| format!("the output is not a session config: {e}")) +} + +/// The door's live sessions, by the kernel's stream. +#[derive(Debug, Default)] +pub struct Sessions { + live: BTreeMap, + /// The session-open params a hook rewrote, by the unit (= the stream) the session opens on, + /// until its first piece opens it. + rewritten: BTreeMap, + /// The tickets each session's pieces crossed on (one per side), so a cancel on either ends it. + tickets: HashMap, +} + +impl Sessions { + /// Stream `stream`'s session, opened on `door` under `cfg` at its first piece. `None` for a + /// door that answers one request, or a stream past [`MAX_SESSIONS`]. + pub fn get_or_open(&mut self, stream: u64, door: Door, cfg: &StreamsCfg) -> Option<&mut Live> { + if !self.live.contains_key(&stream) { + if self.live.len() >= MAX_SESSIONS { + return None; + } + let rewritten = self.rewritten.remove(&stream); + self.live + .insert(stream, Live::open_locked(door, cfg, rewritten)?); + } + self.live.get_mut(&stream) + } + + /// The session-open params a hook rewrote for the session unit `unit` opens (kept until its + /// first piece; the oldest dropped past [`MAX_SESSIONS`] kept). + pub fn rewrite(&mut self, unit: u64, params: SessionConfig) { + while self.rewritten.len() >= MAX_SESSIONS { + if self.rewritten.pop_first().is_none() { + break; + } + } + self.rewritten.insert(unit, params); + } + + /// Stream `stream`'s session, when one is open. + pub fn get(&mut self, stream: u64) -> Option<&mut Live> { + self.live.get_mut(&stream) + } + + /// A piece of stream `stream` crossed on `ticket`. + pub fn crossed(&mut self, stream: u64, ticket: Ticket) { + if ticket != Ticket::NONE { + self.tickets.insert(ticket, stream); + } + } + + /// Stream `stream`'s session is over: it and its tickets are forgotten. + pub fn close(&mut self, stream: u64) { + self.live.remove(&stream); + self.rewritten.remove(&stream); + self.tickets.retain(|_, s| *s != stream); + } + + /// The op on `ticket` is cancelled: the session it serves, if any, is over. Answers whether + /// one was. + pub fn cancel(&mut self, ticket: Ticket) -> bool { + match self.tickets.get(&ticket).copied() { + Some(stream) => { + self.close(stream); + true + } + None => false, + } + } + + /// The tick clock reads `now_ns`: every session's clock and ceiling. Answers whether any + /// session now owes an answer of its own. + pub fn tick(&mut self, now_ns: u64) -> bool { + let mut owed = false; + for live in self.live.values_mut() { + owed |= live.tick(now_ns); + } + owed + } + + /// The streams whose sessions owe an answer of their own, up to `cap`. + #[must_use] + pub fn ready(&self, cap: usize) -> Vec { + self.live + .iter() + .filter(|(_, l)| l.ready()) + .map(|(s, _)| *s) + .take(cap) + .collect() + } + + /// How many streams' sessions owe an answer of their own. + #[must_use] + pub fn ready_count(&self) -> usize { + self.live.values().filter(|l| l.ready()).count() + } + + /// How many sessions are live. + #[must_use] + pub fn len(&self) -> usize { + self.live.len() + } + + /// Whether none is. + #[must_use] + pub fn is_empty(&self) -> bool { + self.live.is_empty() + } +} + +#[cfg(test)] +#[path = "tests/session_door_tests.rs"] +mod tests; diff --git a/crates/busbar-plane-streaming/src/session_pump.rs b/crates/busbar-plane-streaming/src/session_pump.rs index 2c44ddfa99..25c9757ebc 100644 --- a/crates/busbar-plane-streaming/src/session_pump.rs +++ b/crates/busbar-plane-streaming/src/session_pump.rs @@ -21,8 +21,6 @@ use std::collections::HashMap; -use bytes::Bytes; - use crate::codec::ir::codec::{DecodeState, DuplexReader, DuplexWriter, WireEvent}; use crate::codec::ir::config::SessionConfig; use crate::codec::ir::control::IrDuplexControl; @@ -80,31 +78,13 @@ impl TurnSink for Unmetered { } } -/// A tool call the far end closed that this session serves itself: the call's correlation, its id, -/// its name and its accumulated arguments. The caller runs it and hands the output back through -/// [`SessionPump::tool_executed`]. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ToolRun { - /// The call's correlation. - pub call_ref: CallRef, - /// The call id on the wire. - pub call_id: String, - /// The tool's name. - pub name: String, - /// The arguments, accumulated across the streamed deltas. - pub args: Vec, -} - -/// ONE IN-FLIGHT TOOL CALL, accumulated across the `CallOpen → CallArgs* → CallClose` frames the -/// model streams. The raw call id is kept so the stateless writer can frame the output without -/// consulting the map. +/// ONE IN-FLIGHT TOOL CALL, correlated across the `CallOpen → CallArgs* → CallClose` frames the +/// model streams: its wire id, so a close that carries only the correlation still names its call, +/// and whether its close was already handed to the node's open-call table. #[derive(Debug, Default, Clone)] struct PendingCall { call_id: String, - name: String, - args: Vec, - closed: bool, - executed: bool, + planned: bool, } /// THE SESSION PUMP over the session's codec `C`. @@ -120,8 +100,8 @@ pub struct SessionPump { /// The format uplink audio is counted in when a frame's wire states none: the locked config's /// input format, else PCM16. audio_in: AudioFormat, - /// The node's open-call table, when one is composed. `None`: every call is served in-process and - /// a caller-authored result is carried upstream verbatim. + /// The node's open-call table, when one is composed. `None`: a caller-authored result is carried + /// upstream verbatim. governed: Option, } @@ -186,18 +166,18 @@ where ) } - /// A FRAME FROM THE FAR END, at `now_ms`. `serves` answers whether this session runs a named tool - /// itself; the calls it does are returned beside the plan, in close order, to be run and handed - /// back through [`Self::tool_executed`]. + /// A FRAME FROM THE FAR END, at `now_ms`. A tool call is part of the model's response: it is + /// relayed to the caller as the model streamed it (translated only between dialects), and the + /// caller runs the tool and answers it (Law 11; QUESTIONS Q98). Nothing here runs a tool or + /// authors its result. A call's close enters the wait for the caller's reply in the node's + /// open-call table, when one is bound. pub fn on_server_frame( &mut self, frame: WireEvent, now_ms: u64, sink: &mut dyn TurnSink, - serves: &dyn Fn(&str) -> bool, - ) -> (Outbound, Vec) { + ) -> Outbound { let mut out = Outbound::default(); - let mut to_exec: Vec = Vec::new(); let events = self.codec.read_down(frame, &mut self.decode); for ev in events { if matches!(ev, IrServerEvent::Error { .. }) { @@ -229,47 +209,43 @@ where } IrServerEvent::Tool(t) => { let call_ref = t.call_ref(); - match t { - IrDuplexTool::CallOpen { call_id, name, .. } => { + match &t { + IrDuplexTool::CallOpen { call_id, .. } => { self.turn.open_tool_call(); - let e = self.calls.entry(call_ref).or_default(); - e.call_id = call_id; - e.name = name; + self.calls + .entry(call_ref) + .or_default() + .call_id + .clone_from(call_id); } - IrDuplexTool::CallArgs { - call_id, - json_delta, - .. - } => { + IrDuplexTool::CallArgs { call_id, .. } => { let e = self.calls.entry(call_ref).or_default(); if e.call_id.is_empty() { - e.call_id = call_id; + e.call_id.clone_from(call_id); } - e.args.extend_from_slice(&json_delta); } IrDuplexTool::CallClose { call_id, .. } => { let e = self.calls.entry(call_ref).or_default(); if e.call_id.is_empty() { - e.call_id = call_id; + e.call_id.clone_from(call_id); } - e.closed = true; - if !e.executed { - e.executed = true; - match &self.governed { - Some(g) if !serves(&e.name) => { - g.calls.planned(g.session, &e.call_id, now_ms); - } - _ => to_exec.push(ToolRun { - call_ref, - call_id: e.call_id.clone(), - name: e.name.clone(), - args: e.args.clone(), - }), + if !e.planned { + e.planned = true; + if let Some(g) = &self.governed { + g.calls.planned(g.session, &e.call_id, now_ms); } } } IrDuplexTool::CallResult { .. } => {} } + // The call is the caller's to answer, as the realtime protocols define it: it is + // relayed as-is, and the gateway never authors its result. + if !matches!(t, IrDuplexTool::CallResult { .. }) { + out.downlink.extend( + self.codec + .write_down(IrServerEvent::Tool(t), &mut self.decode), + ); + } } ev @ (IrServerEvent::AudioFrame(_) | IrServerEvent::AudioDone { .. } @@ -282,25 +258,7 @@ where IrServerEvent::RateLimits => {} } } - (out, to_exec) - } - - /// A tool this session ran answered `output`: the result goes to the far end, then a request for - /// the model's next response. - pub fn tool_executed(&mut self, run: ToolRun, output: Vec, out: &mut Outbound) { - out.push_up(self.codec.write_up( - IrClientEvent::Tool(IrDuplexTool::CallResult { - call_ref: run.call_ref, - call_id: run.call_id, - name: run.name, - output: Bytes::from(output), - }), - &mut self.decode, - )); - out.push_up(self.codec.write_up( - IrClientEvent::Control(IrDuplexControl::ResponseCreate { response: None }), - &mut self.decode, - )); + out } /// A FRAME FROM THE CALLER. A `session.update` is replaced by the locked config; a tool reply is diff --git a/crates/busbar-plane-streaming/src/session_unit.rs b/crates/busbar-plane-streaming/src/session_unit.rs new file mode 100644 index 0000000000..9134ce9208 --- /dev/null +++ b/crates/busbar-plane-streaming/src/session_unit.rs @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! ONE LIVE SESSION AS THE DRIVER SERVES IT, sans I/O: the session pump, the telephony carrier's +//! envelope where the session came in over the telephony door, the session's cumulative units and +//! its wall-clock ceiling. +//! +//! The driver pushes the caller's pieces ([`SessionUnit::from_caller`]) and the far end's +//! ([`SessionUnit::from_far_end`]); each answers a [`Plan`] of bytes for each side and whether the +//! session ends. Every closed turn adds to the session's cumulative units ([`SessionUnit::units`]), +//! which each answer reports; the kernel checkpoints them and decides a cut. The plane never cuts a +//! session on money: a cut is the kernel's, and it reaches the caller through the refusal render. + +use bytes::Bytes; + +use crate::codec::ir::codec::{DuplexReader, DuplexWriter, WireEvent}; +use crate::codec::ir::config::SessionConfig; +use crate::codec::ir::usage::IrDuplexUsage; +use crate::codec::topology::twilio::{CallerStep, TwilioBridge}; +use crate::governed::GovernedSession; +use crate::meta; +use crate::session::{class_counts, TurnCounters}; +use crate::session_pump::{SessionPump, TurnSink}; +use crate::session_row::VoiceSessionRow; + +/// The billable classes a session reports, in the tail's order (their index is the class a unit +/// count names). +pub const CLASSES: [busbar_contract::ids::MeterClassId; 6] = [ + meta::CLASS_AUDIO_TOKENS_IN, + meta::CLASS_AUDIO_TOKENS_OUT, + meta::CLASS_TEXT_TOKENS_IN, + meta::CLASS_TEXT_TOKENS_OUT, + meta::CLASS_AUDIO_SECONDS_IN, + meta::CLASS_TOOL_CALLS, +]; + +/// The session's units so far, per class: every closed turn adds to them, and the session always +/// continues (the kernel owns the cut). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct CumulativeUnits(pub [u64; 6]); + +impl TurnSink for CumulativeUnits { + fn turn_closed(&mut self, usage: Option<&IrDuplexUsage>, counters: TurnCounters) -> bool { + for (class, n) in class_counts(usage, counters) { + if let Some(i) = CLASSES.iter().position(|c| *c == class) { + self.0[i] = self.0[i].saturating_add(n); + } + } + true + } +} + +/// What one piece asks the driver to carry. Every frame a session emits, to either side, is one +/// TEXT message ([`FRAMES_ARE_TEXT`]): the realtime dialects and the telephony envelope are JSON +/// events. What arrives may be either: Gemini Live's server sends its JSON in binary frames, and a +/// frame is read by its bytes, not its opcode. +#[derive(Debug, Default, PartialEq, Eq)] +pub struct Plan { + /// Frames bound for the far end, in order. + pub to_far_end: Vec>, + /// Frames bound for the caller, in order. + pub to_caller: Vec>, + /// The session ends after these frames. + pub end: bool, +} + +/// Every frame a session emits is one text message: the door sets the text bit on each. +pub const FRAMES_ARE_TEXT: bool = true; + +/// ONE LIVE SESSION over the far end's codec `C`. +#[derive(Debug)] +pub struct SessionUnit { + pump: SessionPump, + /// The session's record: opened, its turn cursor, its provider call id, its end. + row: VoiceSessionRow, + /// Record writes not yet handed to the kernel, oldest first. + writes: Vec, + units: CumulativeUnits, + twilio: Option, + opened_ns: u64, + ceiling_secs: Option, + ended: bool, +} + +impl SessionUnit +where + C: DuplexReader + DuplexWriter, +{ + /// A session over `codec`, locked to `locked`, opened at `now_ns`, bounded by `ceiling_secs`. + /// `telephony` puts the telephony carrier's envelope on the caller's side. `id` names the session + /// and `owner` is the caller's reference (never the principal); the session's record is written + /// at open. + pub fn open( + codec: C, + locked: SessionConfig, + telephony: bool, + now_ns: u64, + ceiling_secs: Option, + ) -> Self { + Self::open_as(codec, locked, telephony, now_ns, ceiling_secs, "", "") + } + + /// [`Self::open`], naming the session `id` for `owner`. + pub fn open_as( + codec: C, + locked: SessionConfig, + telephony: bool, + now_ns: u64, + ceiling_secs: Option, + id: &str, + owner: &str, + ) -> Self { + let row = VoiceSessionRow { + id: id.to_string(), + owner: owner.to_string(), + turns: 0, + updated_at: now_ns / 1_000_000_000, + terminal: false, + rtc_call_id: None, + }; + SessionUnit { + writes: vec![row.clone()], + row, + pump: SessionPump::new(codec, Some(locked)), + units: CumulativeUnits::default(), + twilio: telephony.then(TwilioBridge::default), + opened_ns: now_ns, + ceiling_secs, + ended: false, + } + } + + /// Bind the node's open-call table for this session. + pub fn bind_governed(&mut self, governed: GovernedSession) { + self.pump.bind_governed(governed); + } + + /// The session's cumulative units, per class in [`CLASSES`] order. + #[must_use] + pub fn units(&self) -> CumulativeUnits { + self.units + } + + /// The record writes not yet handed to the kernel, oldest first; taking them clears them. + pub fn take_writes(&mut self) -> Vec { + std::mem::take(&mut self.writes) + } + + /// The provider named the session's call: it goes on the session's record. + pub fn set_rtc_call_id(&mut self, rtc_call_id: &str, now_s: u64) { + self.row.rtc_call_id = Some(rtc_call_id.to_string()); + self.row.updated_at = now_s; + self.writes.push(self.row.clone()); + } + + /// `true` once the session has ended. + #[must_use] + pub fn ended(&self) -> bool { + self.ended + } + + fn caller_frames(&self, frames: Vec) -> Vec> { + frames + .into_iter() + .filter_map(|w| match &self.twilio { + Some(bridge) => bridge.to_caller(&w.0), + None => Some(w.0.to_vec()), + }) + .collect() + } + + /// A piece from the caller. + pub fn from_caller(&mut self, bytes: &[u8]) -> Plan { + if self.ended { + return Plan::default(); + } + let frame = match self.twilio.as_mut() { + None => WireEvent(Bytes::copy_from_slice(bytes)), + Some(bridge) => match bridge.from_caller(bytes) { + CallerStep::Audio(w) => w, + CallerStep::Stop | CallerStep::Refused => return self.end(), + CallerStep::Nothing => return Plan::default(), + }, + }; + let out = self.pump.on_client_frame(frame); + Plan { + to_far_end: out.upstream.into_iter().map(|w| w.0.to_vec()).collect(), + to_caller: Vec::new(), + end: false, + } + } + + /// A piece from the far end, at `now_ms`. A tool call in it is relayed to the caller as-is: the + /// caller runs the tool (Law 11). + pub fn from_far_end(&mut self, bytes: &[u8], now_ms: u64) -> Plan { + if self.ended { + return Plan::default(); + } + let out = self.pump.on_server_frame( + WireEvent(Bytes::copy_from_slice(bytes)), + now_ms, + &mut self.units, + ); + Plan { + to_far_end: out.upstream.into_iter().map(|w| w.0.to_vec()).collect(), + to_caller: self.caller_frames(out.downlink), + end: false, + } + } + + /// The clock, at `now_ns` and `now_ms`: a session past its ceiling is told why and ends; the + /// node's unanswered calls past their deadline are swept. + pub fn tick(&mut self, now_ns: u64, now_ms: u64) -> Plan { + if self.ended { + return Plan::default(); + } + self.pump.sweep_expired(now_ms); + let Some(ceiling) = self.ceiling_secs else { + return Plan::default(); + }; + if now_ns.saturating_sub(self.opened_ns) < ceiling.saturating_mul(1_000_000_000) { + return Plan::default(); + } + self.close_at_ceiling(ceiling) + } + + /// The session reached the wall-clock ceiling of `ceiling_secs` its host measured: it is told + /// why, in its dialect, and ends. + pub fn close_at_ceiling(&mut self, ceiling_secs: u64) -> Plan { + if self.ended { + return Plan::default(); + } + let told = self.pump.ceiling_error(ceiling_secs); + let mut plan = self.end(); + plan.to_caller = self.caller_frames(told.into_iter().collect()); + plan + } + + /// The session ends: its open turn settles once, the node forgets its open calls, and the + /// session's record is written terminal. + pub fn end(&mut self) -> Plan { + if !self.ended { + self.ended = true; + self.pump.settle_open_turn(&mut self.units); + self.pump.forget_governed_calls(); + self.row.terminal = true; + self.writes.push(self.row.clone()); + } + Plan { + end: true, + ..Plan::default() + } + } +} + +#[cfg(test)] +#[path = "tests/session_unit_tests.rs"] +mod tests; diff --git a/crates/busbar-plane-streaming/src/sessions.rs b/crates/busbar-plane-streaming/src/sessions.rs new file mode 100644 index 0000000000..765221a6f4 --- /dev/null +++ b/crates/busbar-plane-streaming/src/sessions.rs @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE DOOR'S SESSION CONSTRUCTOR: which live session a unit on a session door opens. Every session +//! door builds its session here, and nowhere else, so the door a unit arrived through decides its +//! codec and its caller-side envelope in one place. + +use crate::codec::ir::codec::gemini::GeminiLiveCodec; +use crate::codec::ir::codec::OpenAiRealtimeCodec; +use crate::codec::ir::config::SessionConfig; +use crate::driven::Door; +use crate::session_params::g711_config; +use crate::session_unit::{CumulativeUnits, Plan, SessionUnit}; + +/// One live session, over the codec its door speaks. +#[derive(Debug)] +pub enum Session { + /// The OpenAI Realtime dialect: the browser sideband, or the telephony door behind the Twilio + /// envelope. + Realtime(SessionUnit), + /// The Gemini Live dialect. + Gemini(SessionUnit), +} + +/// Open the session a unit on `door` opens: `None` for a door that answers one request. The +/// telephony door is the OpenAI Realtime dialect locked to `g711_ulaw` both ways, behind the Twilio +/// envelope, locked as the served telephony leg locked it; the other session doors lock the +/// operator's session params. +#[must_use] +pub fn open( + door: Door, + locked: &SessionConfig, + now_ns: u64, + ceiling_secs: Option, + id: &str, + owner: &str, +) -> Option { + Some(match door { + Door::Sideband => Session::Realtime(SessionUnit::open_as( + OpenAiRealtimeCodec, + locked.clone(), + false, + now_ns, + ceiling_secs, + id, + owner, + )), + Door::Twilio => Session::Realtime(SessionUnit::open_as( + OpenAiRealtimeCodec, + g711_config(), + true, + now_ns, + ceiling_secs, + id, + owner, + )), + Door::Gemini => Session::Gemini(SessionUnit::open_as( + GeminiLiveCodec, + locked.clone(), + false, + now_ns, + ceiling_secs, + id, + owner, + )), + Door::Mint | Door::Sdp => return None, + }) +} + +impl Session { + /// A piece from the caller. + pub fn from_caller(&mut self, bytes: &[u8]) -> Plan { + match self { + Session::Realtime(s) => s.from_caller(bytes), + Session::Gemini(s) => s.from_caller(bytes), + } + } + + /// A piece from the far end, at `now_ms`. + pub fn from_far_end(&mut self, bytes: &[u8], now_ms: u64) -> Plan { + match self { + Session::Realtime(s) => s.from_far_end(bytes, now_ms), + Session::Gemini(s) => s.from_far_end(bytes, now_ms), + } + } + + /// The session's cumulative units. + #[must_use] + pub fn units(&self) -> CumulativeUnits { + match self { + Session::Realtime(s) => s.units(), + Session::Gemini(s) => s.units(), + } + } +} + +#[cfg(test)] +#[path = "tests/sessions_tests.rs"] +mod tests; diff --git a/crates/busbar-plane-streaming/src/tests/codec.rs b/crates/busbar-plane-streaming/src/tests/codec.rs deleted file mode 100644 index d17150b091..0000000000 --- a/crates/busbar-plane-streaming/src/tests/codec.rs +++ /dev/null @@ -1,1956 +0,0 @@ -//! Codec-path tests: fixtures decode to the expected turn units, interrupt facts and pacing facts. -//! -//! The OpenAI `session.update` fixture below restates (does not literally `include!`, because the -//! entry point shape differs — busbar-voice's own test calls `OpenAiRealtimeCodec::read_up` -//! directly, this one calls `StreamingPlane::decode_ingress`) the fixture at -//! `crates/busbar-voice/src/ir/codec/tests.rs::ga_session_server_vad` (lines 52-74 at the time of -//! writing). The audio-frame and `session.created`/usage fixtures are built from the same wire -//! `type` tokens `crates/busbar-voice/src/ir/codec/mod.rs`'s `wire` module names -//! (`input_audio_buffer.append`, `session.created`, `response.done`). - -use busbar_contract::bounded::{FactValue, Facts, Labels}; -use busbar_contract::ids::LaneId; -use busbar_contract::plane::{Ingress, Plane, PlaneSessionState, Progress, SessionPlane}; -use busbar_contract::wire::FrameCursor; -use serde_json::json; - -use crate::claims::Dialect; -use crate::tests::harness::{ctx, destination, frame, unit, EmptyConfig, LeakPlaneAlloc, WsStack}; -use crate::{StreamingPlane, Upstream}; - -fn openai_plane() -> StreamingPlane { - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime"), - host: "api.openai.com", - dialect: Dialect::OpenaiRealtime, - }]; - StreamingPlane::new(UPSTREAMS) -} - -fn open_client_session( - plane: &StreamingPlane, - ctx: &busbar_contract::unit::Ctx<'_>, -) -> PlaneSessionState { - SessionPlane::open_session(plane, ctx) -} - -fn client_wire(bytes: &[u8]) -> Vec { - bytes.to_vec() -} - -/// The `session.update` fixture restated from `busbar-voice`'s own `ga_session_server_vad` fixture -/// (`crates/busbar-voice/src/ir/codec/tests.rs`, lines 52-74). -fn session_update_fixture() -> Vec { - serde_json::to_vec(&json!({ - "type": "session.update", - "session": { - "modalities": ["audio", "text"], - "instructions": "You are a helpful voice agent.", - "voice": "marin", - "input_audio_format": "pcm16", - "output_audio_format": "g711_ulaw", - "turn_detection": { - "type": "server_vad", - "threshold": 0.5, - "prefix_padding_ms": 300, - "silence_duration_ms": 200, - "create_response": true, - "interrupt_response": true - }, - "tools": [{ "type": "function", "name": "lookup", "parameters": { "type": "object" } }], - "tool_choice": "auto", - "max_output_tokens": 4096 - } - })) - .expect("fixture serializes") -} - -#[test] -fn session_update_opens_a_turn_and_names_the_dialect() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let bytes = client_wire(&session_update_fixture()); - let frames = [frame(&bytes)]; - let mut cursor = FrameCursor::new(&frames); - - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("session.update decodes"); - let Ingress::Open(draft) = ingress else { - panic!("expected Ingress::Open, got {ingress:?}"); - }; - assert_eq!(draft.op.as_str(), "duplex_turn"); - assert_eq!( - draft.facts.get(crate::meta::FACT_DIALECT), - Some(busbar_contract::bounded::FactValue::Str("openai-realtime")) - ); - assert!(draft.correlation_out.is_some()); -} - -#[test] -fn a_second_frame_of_the_same_turn_relays_rather_than_reopens() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let first = client_wire(&session_update_fixture()); - let frames1 = [frame(&first)]; - let mut cursor1 = FrameCursor::new(&frames1); - let ingress1 = plane - .decode_ingress(&mut cursor1, Some(&mut state), &c) - .expect("first frame decodes"); - assert!(matches!(ingress1, Ingress::Open(_))); - - let audio = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": "AAAA", - })) - .expect("audio fixture serializes"); - let frames2 = [frame(&audio)]; - let mut cursor2 = FrameCursor::new(&frames2); - let ingress2 = plane - .decode_ingress(&mut cursor2, Some(&mut state), &c) - .expect("second frame decodes"); - let Ingress::Frame { for_, .. } = ingress2 else { - panic!("expected Ingress::Frame, got {ingress2:?}"); - }; - assert!( - for_.is_some(), - "the relay frame must carry the turn's correlation" - ); -} - -#[test] -fn client_truncate_writes_the_declared_interrupt_fact() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let truncate = serde_json::to_vec(&json!({ - "type": "conversation.item.truncate", - "item_id": "item_1", - "content_index": 0, - "audio_end_ms": 640, - })) - .expect("truncate fixture serializes"); - let frames = [frame(&truncate)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("truncate decodes"); - let Ingress::Open(draft) = ingress else { - panic!("expected Ingress::Open (first frame of the turn), got {ingress:?}"); - }; - assert_eq!( - draft - .facts - .get(::INTERRUPT_FACT.unwrap()), - Some(busbar_contract::bounded::FactValue::Int(640)) - ); -} - -#[test] -fn upstream_speech_started_synthesizes_the_interrupt_fact_from_playback_position() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut upstream_state = SessionPlane::open_upstream( - &plane, - &destination("api.openai.com", LaneId::new("realtime")), - &c, - ); - - // Two downlink audio deltas totalling 96 bytes of pcm16 (48 bytes/ms) = 2 ms played, then a - // speech-started barge-in signal. - let delta = serde_json::to_vec(&json!({ - "type": "response.output_audio.delta", - "delta": base64_of(&[0u8; 96]), - })) - .unwrap(); - let started = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.speech_started", - "audio_start_ms": 0, - "item_id": "item_1", - })) - .unwrap(); - - let frames1 = [frame(&delta)]; - let mut cursor1 = FrameCursor::new(&frames1); - let _ = plane - .decode_response( - &mut cursor1, - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut upstream_state), - &c, - ) - .expect("audio delta decodes"); - - let frames2 = [frame(&started)]; - let mut cursor2 = FrameCursor::new(&frames2); - let progress = plane - .decode_response( - &mut cursor2, - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut upstream_state), - &c, - ) - .expect("speech_started decodes"); - let Progress::Frame { r, .. } = progress else { - panic!("expected Progress::Frame, got {progress:?}"); - }; - assert_eq!( - r.facts - .get(::INTERRUPT_FACT.unwrap()), - Some(busbar_contract::bounded::FactValue::Int(2)) - ); -} - -#[test] -fn downlink_audio_frames_carry_the_declared_pacing_fact() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut upstream_state = SessionPlane::open_upstream( - &plane, - &destination("api.openai.com", LaneId::new("realtime")), - &c, - ); - - let delta = serde_json::to_vec(&json!({ - "type": "response.output_audio.delta", - "delta": base64_of(&[0u8; 48]), - })) - .unwrap(); - let frames = [frame(&delta)]; - let mut cursor = FrameCursor::new(&frames); - let progress = plane - .decode_response( - &mut cursor, - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut upstream_state), - &c, - ) - .expect("audio delta decodes"); - let Progress::Frame { r, .. } = progress else { - panic!("expected Progress::Frame, got {progress:?}"); - }; - assert_eq!( - r.facts.get( - ::EGRESS_PACING_FACT.unwrap() - ), - Some(busbar_contract::bounded::FactValue::Int(1)) - ); -} - -/// A tool call is dispatched ON CLOSE, as its own unit, carrying the arguments the model streamed. -/// -/// The model announces the call (`output_item.added`), streams its arguments as deltas, and states -/// them complete on the `…arguments.done` frame. Only when the arguments are complete is the call -/// its own `tool_call` unit — dispatched with a name AND its arguments, not a name and nothing. -/// The open and delta frames mint nothing on their own; they accumulate. -#[test] -fn a_tool_call_dispatches_on_close_with_its_arguments() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut upstream_state = SessionPlane::open_upstream(&plane, &dest, &c); - - // The announcement mints nothing — it accumulates. - let opened = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": "call_1", "name": "lookup" }, - })) - .unwrap(); - { - let frames = [frame(&opened)]; - let mut cursor = FrameCursor::new(&frames); - assert!( - matches!( - plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("an announcement decodes"), - Progress::Discard { .. } - ), - "an announcement mints no unit on its own" - ); - } - - // A partial argument delta mints nothing either — it accumulates. - let delta = serde_json::to_vec(&json!({ - "type": "response.function_call_arguments.delta", - "call_id": "call_1", - "delta": "{\"city\":\"Pa", - })) - .unwrap(); - { - let frames = [frame(&delta)]; - let mut cursor = FrameCursor::new(&frames); - assert!( - matches!( - plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("a delta decodes"), - Progress::Discard { .. } - ), - "an argument delta mints no unit on its own" - ); - } - - // The close states the complete arguments and dispatches the call. - let done = serde_json::to_vec(&json!({ - "type": "response.function_call_arguments.done", - "call_id": "call_1", - "name": "lookup", - "arguments": "{\"city\":\"Paris\"}", - })) - .unwrap(); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::OneShot(draft) = plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("a tool-call close decodes") - else { - panic!("a completed tool call is dispatched as its own unit"); - }; - assert_eq!(draft.op.as_str(), "tool_call"); - assert_eq!( - draft.facts.get(crate::meta::FACT_TOOL_NAME), - Some(busbar_contract::bounded::FactValue::Str("lookup")) - ); - // The arguments the model asked for reach the executor, not an empty body. - let args: serde_json::Value = - serde_json::from_slice(draft.body_ir.body()).expect("the body carries the arguments"); - assert_eq!(args["city"], "Paris"); -} - -/// P-ITEM: VOICE TOOL-ARGS through the plane's own `decode_response` (spec DONE item 2; the drive -/// log's P5, commit 470351a480: "streamed tool-call arguments are discarded"). The arguments arrive -/// ONLY as fragments and the close states none of its own, so nothing but accumulation can deliver -/// them: the dispatched `tool_call` unit carries the fragments concatenated in order. -/// -/// The 1.5.5 behaviour matched is the llm surface's streamed tool call (owner correction -/// 2026-09-28): every argument fragment of an open tool block accumulated, the call emitted once on -/// its block stop with the fully reassembled arguments (v1.5.5 -/// `crates/busbar/src/proto/gemini/writer.rs:734-780`). -#[test] -fn p_item_voice_tool_args_fragments_alone_reach_the_dispatched_call_whole() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut upstream_state = SessionPlane::open_upstream(&plane, &dest, &c); - - let mut events = vec![json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": "call_9", "name": "weather" }, - })]; - for fragment in ["{\"lo", "c\":\"S", "F\"}"] { - events.push(json!({ - "type": "response.function_call_arguments.delta", - "call_id": "call_9", - "delta": fragment, - })); - } - for event in events { - let bytes = serde_json::to_vec(&event).unwrap(); - let frames = [frame(&bytes)]; - let mut cursor = FrameCursor::new(&frames); - assert!( - matches!( - plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("a tool-call frame decodes"), - Progress::Discard { .. } - ), - "no unit before the call closes" - ); - } - let done = serde_json::to_vec(&json!({ - "type": "response.function_call_arguments.done", - "call_id": "call_9", - })) - .unwrap(); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::OneShot(draft) = plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("a tool-call close decodes") - else { - panic!("a completed tool call is dispatched as its own unit"); - }; - assert_eq!(draft.op.as_str(), "tool_call"); - assert_eq!( - draft.facts.get(crate::meta::FACT_TOOL_NAME), - Some(busbar_contract::bounded::FactValue::Str("weather")) - ); - assert_eq!( - draft.body_ir.body(), - b"{\"loc\":\"SF\"}", - "the fragments reach the call whole, in order" - ); -} - -/// Two tool calls open at once are two different things to wait on. -/// -/// A turn that asks for two tools opens two units, and each waits for its own reply. The reply leg -/// used to name the constant zero for every one of them, so the two were indistinguishable: the -/// first reply satisfied whichever leg was found first, and the second call waited out its whole -/// deadline against an answer that had already been delivered elsewhere. -/// -/// What the leg names is the declared KEY; what tells the two calls apart is the identifier the -/// draft mints under it, in the unit's own arena, as itself. This plane states neither twice — it -/// no longer folds the identifier into a number to get a copy of it onto the sealed leg. -#[test] -fn two_open_tool_calls_wait_on_two_different_correlations() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut upstream_state = SessionPlane::open_upstream(&plane, &dest, &c); - - let mut waits_on = |call_id: &str| { - // Drive the whole call: announce, then close with its arguments. The call is its own unit - // on close. - let opened = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": call_id, "name": "lookup" }, - })) - .unwrap(); - let frames = [frame(&opened)]; - let mut cursor = FrameCursor::new(&frames); - let _ = plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("a tool-call announcement decodes"); - let done = serde_json::to_vec(&json!({ - "type": "response.function_call_arguments.done", - "call_id": call_id, - "name": "lookup", - "arguments": "{\"q\":\"x\"}", - })) - .unwrap(); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::OneShot(draft) = plane - .decode_response(&mut cursor, &dest, Some(&mut upstream_state), &c) - .expect("a tool-call close decodes") - else { - panic!("a completed tool call is its own unit"); - }; - let out = draft - .correlation_out - .expect("a tool call mints a correlation"); - // The draft's own correlation carries the identifier itself, not a fold of it. - assert_eq!( - out.value, - busbar_contract::ids::CorrelationValue::Str(call_id) - ); - let unit = crate::tests::harness::unit(draft.op, draft.body_ir, draft.facts); - match plane.verify(&unit, &c) { - busbar_contract::dest::DestinationFacts::Client { - mode: - busbar_contract::dest::ClientMode::AwaitReply { - correlation_key, .. - }, - .. - } => { - // The leg names the key the draft minted under. If those two ever disagreed the - // kernel would have a wait it could never satisfy. - assert_eq!(correlation_key, out.fact_key); - format!("{:?}", out.value) - } - other => panic!("a tool call is delivered to the client, got {other:?}"), - } - }; - - let first = waits_on("call_1"); - let second = waits_on("call_2"); - assert_ne!( - first, second, - "two open tool calls waited on the same correlation" - ); - assert_eq!( - second, - waits_on("call_2"), - "the same call named two different correlations" - ); -} - -/// A tool reply names the call it answers, not the turn it arrived on. -/// -/// The reply is the other half of the leg the call planned. Decoded as an ordinary frame of the -/// open turn it named the turn's correlation, so the identifier the waiting unit was entered under -/// never reached the frame at all — the wait and its answer went past each other on one session, -/// and the call expired holding a reservation for an answer that had already come back. -#[test] -fn a_client_tool_reply_names_the_call_it_answers() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - // The turn is open first, so "it relayed onto the turn" is a live alternative rather than - // something the fixture ruled out. - let opening = client_wire(&session_update_fixture()); - let frames = [frame(&opening)]; - let mut cursor = FrameCursor::new(&frames); - let opened = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the turn opens"); - let Ingress::Open(turn) = opened else { - panic!("expected Ingress::Open, got {opened:?}"); - }; - let turn_correlation = turn.correlation_out.expect("a turn correlates"); - - let reply = serde_json::to_vec(&json!({ - "type": "conversation.item.create", - "item": { - "type": "function_call_output", - "call_id": "call_2", - "output": "{\"tide\":\"out\"}", - }, - })) - .expect("the reply fixture serializes"); - let frames = [frame(&reply)]; - let mut cursor = FrameCursor::new(&frames); - let answered = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("a tool reply decodes"); - let Ingress::Frame { for_, facts, .. } = answered else { - panic!("expected Ingress::Frame, got {answered:?}"); - }; - let for_ = for_.expect("a reply carries the correlation it answers"); - assert_eq!( - for_.fact_key, - crate::plane::FACT_TOOL_CORRELATION, - "under the key the call's own leg named" - ); - assert_eq!( - for_.value, - busbar_contract::ids::CorrelationValue::Str("call_2"), - "and carrying the identifier itself" - ); - assert_ne!( - for_.value, turn_correlation.value, - "a reply that named the turn would wake no tool call at all" - ); - assert_eq!( - facts.get(crate::meta::FACT_CALL_ID), - Some(FactValue::Str("call_2")) - ); -} - -#[test] -fn usage_closes_the_turn_and_meter_reads_every_declared_class() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut upstream_state = SessionPlane::open_upstream( - &plane, - &destination("api.openai.com", LaneId::new("realtime")), - &c, - ); - - let done = serde_json::to_vec(&json!({ - "type": "response.done", - "response": { - "usage": { - "input_token_details": { "audio_tokens": 10, "text_tokens": 3, "cached_tokens": 1 }, - "output_token_details": { "audio_tokens": 20, "text_tokens": 4 }, - } - } - })) - .unwrap(); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let unit_dest = destination("api.openai.com", LaneId::new("realtime")); - let progress = plane - .decode_response(&mut cursor, &unit_dest, Some(&mut upstream_state), &c) - .expect("response.done decodes"); - let Progress::Terminal { r, .. } = progress else { - panic!("expected Progress::Terminal, got {progress:?}"); - }; - assert_eq!(r.finish, busbar_contract::unit::FinishClass::TurnComplete); - - let unit = crate::tests::harness::unit( - busbar_contract::ids::OpClassId::new("duplex_turn"), - r.ir, - Facts::new(), - ); - let locators = plane.meter(&unit, &r, &c); - let classes: Vec<&str> = locators - .lines - .as_slice() - .iter() - .map(|l| l.class.as_str()) - .collect(); - for expected in [ - "audio_tokens_in", - "audio_tokens_out", - "text_tokens_in", - "text_tokens_out", - "audio_seconds_in", - ] { - assert!( - classes.contains(&expected), - "expected meter class {expected} in {classes:?}" - ); - } - // #71: cached_tokens is a SUBSET of audio_tokens_in/text_tokens_in, not a class beside them, so - // the usage report naming it must NOT produce a meter line for it — pricing it too would - // double-bill the same cached input. - assert!( - !classes.contains(&"cached_tokens"), - "cached_tokens must never be a meter class (#71); got {classes:?}" - ); - - // The point of the split, asserted as a quantity and not just as a label: the fixture reports - // three text tokens consumed and four emitted, and the two land on different classes at their - // reported figures. Summed into one input-direction class -- which is what this plane used to do - // -- the emitted four would have priced at the input rate. - let quantity = |class: &str| { - locators - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == class) - .and_then(|l| l.quantity) - }; - assert_eq!(quantity("text_tokens_in"), Some(3)); - assert_eq!(quantity("text_tokens_out"), Some(4)); - // The audio split is the one that moves real money -- audio prices well above text on every - // realtime rate card -- so it is asserted the same way: the fixture reports ten audio tokens - // consumed and twenty emitted, and swapping the two directions has to be a failure here rather - // than a label that still reads as present. - assert_eq!(quantity("audio_tokens_in"), Some(10)); - assert_eq!(quantity("audio_tokens_out"), Some(20)); - // The cached figure is a SUBSET of the input figure above, not a class beside it (#71): pricing - // it too would double-bill the same cached input, so it never reaches the meter as its own line. - assert_eq!(quantity("cached_tokens"), None); -} - -#[test] -fn twilio_media_after_start_admits_a_ulaw_audio_frame() { - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime"), - host: "api.openai.com", - dialect: Dialect::OpenaiRealtime, - }]; - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/twilio/call-123"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - // The dialect is bound directly rather than resolved from the path. The telephony CLAIM is - // gone — its transport has no crate — so no selector maps `/twilio/...` onto this dialect any - // more; the CODEC is what this cell is about and it is untouched. Binding the state here is - // what an arrival on a registered telephony transport would do. - let mut state = PlaneSessionState::new(crate::session::VoiceSessionState::for_dialect( - Dialect::TwilioMediaStreams, - )); - - let start = serde_json::to_vec(&json!({ - "event": "start", - "start": { - "streamSid": "MZ123", - "callSid": "CA123", - "mediaFormat": { "encoding": "audio/x-mulaw", "sampleRate": 8000, "channels": 1 }, - }, - })) - .unwrap(); - let frames1 = [frame(&start)]; - let mut cursor1 = FrameCursor::new(&frames1); - let ingress1 = plane - .decode_ingress(&mut cursor1, Some(&mut state), &c) - .expect("start decodes"); - assert!(matches!(ingress1, Ingress::Discard { .. })); - - let media = serde_json::to_vec(&json!({ - "event": "media", - "streamSid": "MZ123", - "media": { "payload": base64_of(&[0xFFu8; 4]) }, - })) - .unwrap(); - let frames2 = [frame(&media)]; - let mut cursor2 = FrameCursor::new(&frames2); - let ingress2 = plane - .decode_ingress(&mut cursor2, Some(&mut state), &c) - .expect("media decodes"); - assert!(matches!(ingress2, Ingress::Open(_))); -} - -#[test] -fn twilio_media_with_a_forged_stream_sid_is_discarded() { - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime"), - host: "api.openai.com", - dialect: Dialect::OpenaiRealtime, - }]; - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/twilio/call-123"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - // The dialect is bound directly rather than resolved from the path. The telephony CLAIM is - // gone — its transport has no crate — so no selector maps `/twilio/...` onto this dialect any - // more; the CODEC is what this cell is about and it is untouched. Binding the state here is - // what an arrival on a registered telephony transport would do. - let mut state = PlaneSessionState::new(crate::session::VoiceSessionState::for_dialect( - Dialect::TwilioMediaStreams, - )); - - let start = serde_json::to_vec(&json!({ - "event": "start", - "start": { - "streamSid": "MZ-bound", - "callSid": "CA123", - "mediaFormat": { "encoding": "audio/x-mulaw", "sampleRate": 8000, "channels": 1 }, - }, - })) - .unwrap(); - let frames1 = [frame(&start)]; - let mut cursor1 = FrameCursor::new(&frames1); - let _ = plane - .decode_ingress(&mut cursor1, Some(&mut state), &c) - .expect("start decodes"); - - let media = serde_json::to_vec(&json!({ - "event": "media", - "streamSid": "MZ-forged", - "media": { "payload": base64_of(&[0xFFu8; 4]) }, - })) - .unwrap(); - let frames2 = [frame(&media)]; - let mut cursor2 = FrameCursor::new(&frames2); - let ingress2 = plane - .decode_ingress(&mut cursor2, Some(&mut state), &c) - .expect("media decodes"); - assert!(matches!( - ingress2, - Ingress::Discard { - reason: busbar_contract::wire::DiscardCode::ForgedSource - } - )); -} - -#[test] -fn twilio_dtmf_decodes_and_is_discarded_as_unsupported() { - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime"), - host: "api.openai.com", - dialect: Dialect::OpenaiRealtime, - }]; - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/twilio/call-123"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = PlaneSessionState::new(crate::session::VoiceSessionState::for_dialect( - Dialect::TwilioMediaStreams, - )); - - let dtmf = serde_json::to_vec(&json!({ - "event": "dtmf", - "streamSid": "MZ123", - "sequenceNumber": "5", - "dtmf": { "track": "inbound_track", "digit": "5" }, - })) - .unwrap(); - let frames = [frame(&dtmf)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("dtmf decodes, not malformed"); - assert!(matches!( - ingress, - Ingress::Discard { - reason: busbar_contract::wire::DiscardCode::Unsupported - } - )); -} - -/// An event this carrier adds tomorrow is dropped, and the frame that is not this carrier's shape -/// at all is still refused. -/// -/// The carrier publishes new event names over the life of the protocol and tells its clients to -/// ignore the ones they do not recognise; the transport under this session already discards what it -/// cannot place. Refusing the frame ended a live call on the day a new lifecycle event shipped — -/// for a message that carries no audio and asks for nothing. -#[test] -fn twilio_unknown_event_is_dropped_and_a_non_carrier_frame_is_still_refused() { - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime"), - host: "api.openai.com", - dialect: Dialect::OpenaiRealtime, - }]; - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/twilio/call-123"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = PlaneSessionState::new(crate::session::VoiceSessionState::for_dialect( - Dialect::TwilioMediaStreams, - )); - - let unknown = serde_json::to_vec(&json!({ - "event": "teleport", - "streamSid": "MZ123", - })) - .unwrap(); - let frames = [frame(&unknown)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("an event this reader does not model is dropped, not refused"); - assert!(matches!( - ingress, - Ingress::Discard { - reason: busbar_contract::wire::DiscardCode::Unsupported - } - )); - - // The line the discard does not cross: bytes that are not this carrier's JSON at all. - let garbage = [frame(b"not a carrier frame at all")]; - let mut cursor = FrameCursor::new(&garbage); - assert_eq!( - plane.decode_ingress(&mut cursor, Some(&mut state), &c), - Err(busbar_contract::wire::Decode::Malformed) - ); -} - -/// A turn the upstream ends with an error consumed exactly as much of the caller's time and ran -/// exactly as many tools as one it ends with a usage report, and the two counters this plane derives -/// itself are the only record of either. Forty seconds of uplink and two tool calls, then an error -/// frame: both figures must still reach the meter. -#[test] -fn an_upstream_error_still_meters_the_turn_it_ended() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - // PCM16 at 24 kHz is 48 bytes per millisecond, so a second of uplink is 48 000 bytes. Forty of - // them is the forty seconds the caller spoke. - let one_second = vec![0u8; 48_000]; - let relay_unit = crate::tests::harness::unit( - busbar_contract::ids::OpClassId::new("duplex_turn"), - busbar_contract::bounded::Ir::empty(), - Facts::new(), - ); - for _ in 0..40 { - let append = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": base64_of(&one_second), - })) - .expect("audio fixture serializes"); - let frames = [frame(&append)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("an uplink audio frame decodes"); - // And then relays, which is where the caller's seconds are counted. - plane - .encode_ingress_frame( - &relay_unit, - &frames[0], - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut state), - &c, - ) - .expect("an uplink audio frame relays"); - } - - for call_id in ["call_1", "call_2"] { - let opened = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": call_id, "name": "lookup" }, - })) - .expect("tool-call fixture serializes"); - let frames = [frame(&opened)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_response( - &mut cursor, - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut state), - &c, - ) - .expect("a tool-call open decodes"); - } - - let err = serde_json::to_vec(&json!({ - "type": "error", - "error": { "code": "rate_limit_exceeded", "message": "slow down" }, - })) - .expect("error fixture serializes"); - let frames = [frame(&err)]; - let mut cursor = FrameCursor::new(&frames); - let unit_dest = destination("api.openai.com", LaneId::new("realtime")); - let progress = plane - .decode_response(&mut cursor, &unit_dest, Some(&mut state), &c) - .expect("an error frame decodes"); - let Progress::Terminal { r, .. } = progress else { - panic!("expected Progress::Terminal, got {progress:?}"); - }; - assert_eq!(r.finish, busbar_contract::unit::FinishClass::Error); - - let unit = crate::tests::harness::unit( - busbar_contract::ids::OpClassId::new("duplex_turn"), - r.ir, - Facts::new(), - ); - let locators = plane.meter(&unit, &r, &c); - let quantity = |class: &str| { - locators - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == class) - .and_then(|l| l.quantity) - }; - // Forty seconds of admitted audio, metered in the seconds the class is denominated in. - assert_eq!(quantity("audio_seconds_in"), Some(40)); - assert_eq!(quantity("tool_calls"), Some(2)); -} - -/// The caller's audio is metered on the half the answer comes back on. -/// -/// A session holds one state per CONNECTION: the client's, and one per upstream it dials. The -/// uplink counter used to be taken at decode, against the client's half, and read back at the -/// response step, against the upstream's — a different value entirely, and always zero. Every -/// second a customer spoke on a duplex turn metered at nothing. -#[test] -fn uplink_audio_meters_on_the_half_the_upstream_answer_arrives_on() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut client = open_client_session(&plane, &c); - let mut upstream = SessionPlane::open_upstream(&plane, &dest, &c); - - // The turn opens on a control event, so the audio that follows is a relayed frame rather than - // the unit's own body: the frame under test is the ordinary one, of which a call carries fifty - // a second. - let opening = client_wire(&session_update_fixture()); - let frames = [frame(&opening)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Open(draft) = plane - .decode_ingress(&mut cursor, Some(&mut client), &c) - .expect("the turn opens") - else { - panic!("the first client event opens the turn"); - }; - let unit = crate::tests::harness::unit(draft.op, draft.body_ir, draft.facts); - - // One second of PCM16 at 24 kHz. - let append = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": base64_of(&vec![0u8; 48_000]), - })) - .expect("audio fixture serializes"); - let frames = [frame(&append)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_ingress(&mut cursor, Some(&mut client), &c) - .expect("an uplink audio frame decodes"); - plane - .encode_ingress_frame(&unit, &frames[0], &dest, Some(&mut upstream), &c) - .expect("an uplink audio frame relays to the provider"); - - let done = serde_json::to_vec(&json!({ - "type": "response.done", - "response": { "usage": { "input_token_details": { "audio_tokens": 1 } } } - })) - .expect("usage fixture serializes"); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::Terminal { r, .. } = plane - .decode_response(&mut cursor, &dest, Some(&mut upstream), &c) - .expect("the usage report decodes") - else { - panic!("a usage report ends the turn"); - }; - - let seconds = plane - .meter(&unit, &r, &c) - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == "audio_seconds_in") - .and_then(|l| l.quantity); - assert_eq!( - seconds, - Some(1), - "the second the caller spoke was relayed to the provider and must be metered" - ); -} - -/// A Gemini Live caller's uplink is metered at the rate its blob's `mimeType` states. -/// -/// Gemini requires 16 kHz PCM on its uplink: 32 bytes a millisecond. The relay step counted every -/// uplink frame at the 24 kHz rate (48 bytes a millisecond), so one second a Gemini caller spoke -/// metered as 666 ms and `audio_seconds_in` reported two-thirds of the audio (audit HIGH 9). A blob -/// that states 24 kHz is metered at 24 kHz. -#[test] -fn a_gemini_live_uplink_is_metered_at_the_rate_its_blob_states() { - use crate::tests::harness::{ctx_with_session, PairedSession}; - - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime-gemini"), - host: "api.gemini.example", - dialect: Dialect::GeminiLive, - }]; - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new( - "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent", - ); - let labels = Labels::new(); - let session = PairedSession::new(Dialect::GeminiLive.name(), 1); - let c = ctx_with_session(&arena, &config, &transport, &labels, &session); - let dest = destination("api.gemini.example", LaneId::new("realtime-gemini")); - let relay_unit = unit( - busbar_contract::ids::OpClassId::new("duplex_turn"), - busbar_contract::bounded::Ir::empty(), - Facts::new(), - ); - - // The uplink milliseconds one relayed blob of `bytes` bytes at `mime` adds to the turn. - let relayed_ms = |mime: &str, bytes: usize| { - let mut upstream = SessionPlane::open_upstream(&plane, &dest, &c); - let blob = serde_json::to_vec(&json!({ - "realtimeInput": { "audio": { "mimeType": mime, "data": base64_of(&vec![0u8; bytes]) } }, - })) - .expect("audio fixture serializes"); - let frames = [frame(&blob)]; - let _relayed = - plane.encode_ingress_frame(&relay_unit, &frames[0], &dest, Some(&mut upstream), &c); - upstream - .get_mut::() - .expect("the plane's own session state") - .turn - .audio_ms_in - }; - - // One second at 16 kHz: 32 000 bytes. - assert_eq!( - relayed_ms("audio/pcm;rate=16000", 32_000), - 1_000, - "one second of 16 kHz uplink is one second, not two-thirds of one" - ); - // An untagged blob is the direction's own rate, 16 kHz. - assert_eq!(relayed_ms("audio/pcm", 32_000), 1_000); - // One second at 24 kHz: 48 000 bytes. - assert_eq!(relayed_ms("audio/pcm;rate=24000", 48_000), 1_000); -} - -/// The duration class is denominated in seconds, and the counter behind it is in milliseconds. -/// -/// The design names the class `audio_seconds_in`. This plane counts milliseconds, because that is -/// what a frame's byte count divides down to. Pushed through verbatim, three seconds of admitted -/// audio settled as three thousand seconds -- a thousandfold over-report on a duration-priced class. -#[test] -fn admitted_milliseconds_meter_as_seconds() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let cfg = EmptyConfig; - let stack = WsStack::new("/v1/realtime"); - let labels = Labels::default(); - let c = ctx(&arena, &cfg, &stack, &labels); - - let seconds_line = |ms: i64| { - let mut facts = Facts::new(); - facts - .set(crate::meta::FACT_AUDIO_MS_IN, FactValue::Int(ms)) - .expect("fits"); - let response = busbar_contract::plane::Response { - ir: busbar_contract::bounded::Ir::new(b"{}", &[]), - finish: busbar_contract::unit::FinishClass::TurnComplete, - facts, - }; - let unit = crate::tests::harness::unit( - busbar_contract::ids::OpClassId::new("duplex_turn"), - response.ir, - Facts::new(), - ); - plane - .meter(&unit, &response, &c) - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == "audio_seconds_in") - .and_then(|l| l.quantity) - }; - - assert_eq!(seconds_line(3_000), Some(3)); - // A part-second rounds up rather than vanishing: audio that arrived is not audio that cost - // nothing, and a zero line settles exactly as an absent one. - assert_eq!(seconds_line(1), Some(1)); - assert_eq!(seconds_line(1_500), Some(2)); - assert_eq!(seconds_line(0), Some(0)); -} - -/// A one-shot request sees its own answer, and the unit meters what it was priced on. -/// -/// A transcribe or speech unit is admitted with no session, because it is one request and one -/// answer. The response step demanded a session anyway, so the answer was a decode refusal and the -/// unit metered nothing at all: a whole operation class billed at zero. -#[test] -fn a_one_shot_text_to_speech_unit_meters_the_text_it_was_asked_to_speak() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/audio/speech"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - - let request = - serde_json::to_vec(&json!({ "input": "eight chars of text here", "voice": "marin" })) - .expect("request fixture serializes"); - let frames = [frame(&request)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::OneShot(draft) = plane - .decode_ingress(&mut cursor, None, &c) - .expect("a speech request is one whole unit") - else { - panic!("a one-shot request admits as one unit"); - }; - assert_eq!(draft.op.as_str(), "tts"); - - // The answer is audio, and it arrives with no session because there never was one. - let audio = [0u8; 64]; - let answers = [frame(&audio)]; - let mut answer_cursor = FrameCursor::new(&answers); - let Progress::Terminal { r, .. } = plane - .decode_response( - &mut answer_cursor, - &destination("api.openai.com", LaneId::new("realtime")), - None, - &c, - ) - .expect("a one-shot answer decodes without a session") - else { - panic!("a one-shot answer ends the unit"); - }; - - let unit = crate::tests::harness::unit(draft.op, draft.body_ir, draft.facts); - let locators = plane.meter(&unit, &r, &c); - let quantity = |class: &str| { - locators - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == class) - .and_then(|l| l.quantity) - }; - // "eight chars of text here" is 24 bytes, six tokens at the class's own declared divisor. - assert_eq!(quantity("text_tokens_in"), Some(6)); -} - -/// A one-shot transcription's answer states the text it produced, and that is what it prices at. -#[test] -fn a_one_shot_transcription_meters_the_text_it_returned() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/audio/transcriptions"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - - let audio = [0u8; 64]; - let frames = [frame(&audio)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::OneShot(draft) = plane - .decode_ingress(&mut cursor, None, &c) - .expect("a transcription request is one whole unit") - else { - panic!("a one-shot request admits as one unit"); - }; - assert_eq!(draft.op.as_str(), "transcribe"); - - let answer = serde_json::to_vec(&json!({ "text": "twelve bytes" })).unwrap(); - let answers = [frame(&answer)]; - let mut answer_cursor = FrameCursor::new(&answers); - let Progress::Terminal { r, .. } = plane - .decode_response( - &mut answer_cursor, - &destination("api.openai.com", LaneId::new("realtime")), - None, - &c, - ) - .expect("a one-shot answer decodes without a session") - else { - panic!("a one-shot answer ends the unit"); - }; - - let unit = crate::tests::harness::unit(draft.op, draft.body_ir, draft.facts); - let quantity = |class: &str| { - plane - .meter(&unit, &r, &c) - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == class) - .and_then(|l| l.quantity) - }; - // "twelve bytes" is twelve bytes, three tokens at the class's own declared divisor. - assert_eq!(quantity("text_tokens_out"), Some(3)); -} - -/// A tiny standard base64 encoder, independent of the one this crate's `twilio` module carries, so -/// the test fixtures above do not depend on that module's own correctness to construct their input. -fn base64_of(bytes: &[u8]) -> String { - use base64_stdlib::encode; - encode(bytes) -} - -mod base64_stdlib { - const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - pub fn encode(data: &[u8]) -> String { - let mut out = String::new(); - for chunk in data.chunks(3) { - let b0 = u32::from(chunk[0]); - let b1 = u32::from(*chunk.get(1).unwrap_or(&0)); - let b2 = u32::from(*chunk.get(2).unwrap_or(&0)); - let n = (b0 << 16) | (b1 << 8) | b2; - out.push(ALPHABET[((n >> 18) & 63) as usize] as char); - out.push(ALPHABET[((n >> 12) & 63) as usize] as char); - out.push(if chunk.len() > 1 { - ALPHABET[((n >> 6) & 63) as usize] as char - } else { - '=' - }); - out.push(if chunk.len() > 2 { - ALPHABET[(n & 63) as usize] as char - } else { - '=' - }); - } - out - } -} - -#[test] -fn ws_frame_with_invalid_utf8_fails_closed_rather_than_hanging_on_need_more() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let invalid_utf8: &[u8] = &[0x74, 0x79, 0x70, 0x65, 0xff, 0xfe]; - let frames = [frame(invalid_utf8)]; - let mut cursor = FrameCursor::new(&frames); - let err = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect_err("a frame that cannot be read as UTF-8 is a refusal, not a partial read"); - assert!(matches!(err, busbar_contract::wire::Decode::Malformed)); -} - -#[test] -fn ws_frame_with_an_unrecognised_event_is_dropped_never_left_pending() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let unrecognised = serde_json::to_vec(&json!({ - "type": "teleport.now", - "destination": "mars", - })) - .unwrap(); - let frames = [frame(&unrecognised)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("an unrecognised event decodes to a discard, not an error"); - assert!(matches!( - ingress, - Ingress::Discard { - reason: busbar_contract::wire::DiscardCode::Unsupported - } - )); -} - -#[test] -fn ws_frame_carrying_no_bytes_yet_is_genuinely_need_more() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let frames = [frame(&[])]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("an empty frame is a partial read, not a refusal"); - assert!(matches!(ingress, Ingress::NeedMore)); -} - -/// A refused session is told a class, never this node's own vocabulary. -/// -/// The reason a unit stops is a kernel enum whose names are the money, the buckets and the journal. -/// Rendered onto the wire verbatim it made every internal ceiling a client-visible `error.code` — -/// a name no dialect documents and no client library has a case for. -#[test] -fn a_refusal_renders_an_opaque_code_not_the_internal_reason() { - use busbar_contract::unit::{Refusal, RefusalReason, Step}; - - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - - const OPAQUE: &[&str] = &[ - "invalid_request", - "unauthorized", - "forbidden", - "rate_limited", - "unavailable", - "internal", - ]; - let reasons = [ - RefusalReason::OverdraftCeiling, - RefusalReason::StaleSlice, - RefusalReason::DurabilityUnavailable, - RefusalReason::ScopeMissing, - RefusalReason::CredentialRejected, - RefusalReason::NoDestination, - RefusalReason::RateLimited, - RefusalReason::BodyTooLarge, - RefusalReason::PlanePanic, - ]; - for reason in reasons { - let refusal = Refusal { - step: Step::Decode, - reason, - retry_after_secs: None, - stream: None, - correlates: None, - }; - let bytes = plane - .encode_refusal(&refusal, None, None, &c) - .expect("a refusal renders"); - let parsed: serde_json::Value = - serde_json::from_slice(bytes.as_slice()).expect("the refusal is this dialect's JSON"); - assert_eq!(parsed["type"], "error", "the dialect's own error event"); - let code = parsed["error"]["code"] - .as_str() - .expect("the error names a code"); - assert!( - OPAQUE.contains(&code), - "{reason:?} rendered {code:?}, which is outside the codes a client may be told" - ); - assert!( - parsed["error"]["message"].is_string(), - "the error carries a message" - ); - } -} - -/// P-ITEM: REFUSAL-REASON COLLAPSE (spec DONE item 2, "All P-item behaviours match 1.5.5"; TODO -/// L-ENG9). This plane's refusal table ended in `_ => "internal"`, so a spent budget, a frozen -/// group, a replayed key, a superseded unit or a deadline told the caller the node had broken. -/// 1.5.5's one surface answered none of its limit reasons as an internal error (v1.5.5 -/// `crates/busbar/src/ingress/mod.rs:237-305`). Pinned through the plane's own `encode_refusal` -/// bytes: a refusal reads `internal` exactly when its class is a node fault, and every reason of -/// one class reads the same, so the family is the one classification's -/// (`busbar_contract::abi::plane::RefusalCode::class`) and never this plane's own. -#[test] -fn p_item_refusal_reason_collapse_only_a_node_fault_is_internal_and_one_class_one_answer() { - use busbar_contract::abi::plane::{reason_of, RefusalClass, RefusalCode}; - use busbar_contract::unit::{Refusal, RefusalReason, Step}; - - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut answers: Vec<(RefusalClass, (String, String))> = Vec::new(); - for code in RefusalCode::ALL { - // Two codes are the kernel's own money verdicts and never reach a plane (`reason_of`). - let Some(reason) = reason_of(code.code()) else { - continue; - }; - let class = code.class(); - let refusal = Refusal { - step: Step::Decode, - reason: RefusalReason::from(reason), - retry_after_secs: None, - stream: None, - correlates: None, - }; - let bytes = plane - .encode_refusal(&refusal, None, None, &c) - .expect("a refusal renders"); - let parsed: serde_json::Value = - serde_json::from_slice(bytes.as_slice()).expect("the refusal is this dialect's JSON"); - let answer = ( - parsed["error"]["code"] - .as_str() - .unwrap_or_default() - .to_string(), - parsed["error"]["message"] - .as_str() - .unwrap_or_default() - .to_string(), - ); - assert_eq!( - answer.0 == "internal", - class.is_node_fault(), - "{code:?} (class {class:?}) renders {answer:?}" - ); - match answers.iter().find(|(c, _)| *c == class) { - Some((_, first)) => assert_eq!( - *first, answer, - "{code:?} answers differently from the rest of {class:?}" - ), - None => answers.push((class, answer)), - } - } -} - -/// A barge-in on an OPEN turn opens the turn that takes over. -/// -/// The scheduler reads the interrupt fact off an open and nowhere else — that is the one dispatch -/// that reaches the compare-and-set which supersedes the unit in flight. Delivered as one more -/// frame of the turn it interrupts, the fact named the superseded unit to nobody: the interrupted -/// turn kept the direction's slot and kept pricing while the caller was already speaking over it. -#[test] -fn a_barge_in_on_an_open_turn_opens_the_turn_that_supersedes_it() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - // A turn is open and speaking. - let opening = client_wire(&session_update_fixture()); - let frames = [frame(&opening)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Open(first) = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the turn opens") - else { - panic!("the first client event opens a turn"); - }; - - // The caller talks over it. - let truncate = serde_json::to_vec(&json!({ - "type": "conversation.item.truncate", - "item_id": "item_1", - "content_index": 0, - "audio_end_ms": 640, - })) - .expect("truncate fixture serializes"); - let frames = [frame(&truncate)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the barge-in decodes"); - let Ingress::Open(second) = ingress else { - panic!("a barge-in opens the turn that takes over, got {ingress:?}"); - }; - assert_eq!( - second - .facts - .get(::INTERRUPT_FACT.unwrap()), - Some(FactValue::Int(640)), - "the open carries the interrupt the scheduler supersedes on" - ); - assert_ne!( - second - .correlation_out - .expect("the new turn correlates") - .value, - first - .correlation_out - .expect("the first turn correlates") - .value, - "a late frame of the superseded turn must not relay onto the one that replaced it" - ); -} - -/// Unit 0's egress body — the FIRST upstream message a fresh duplex session sends — must be the -/// opening client frame re-framed onto the upstream dialect, not an empty byte pass-through. A -/// session that opens with `session.update` used to relay an EMPTY body, so the provider was handed -/// nothing and voice/instructions/tools/turn_detection silently never applied. -#[test] -fn unit_zero_egress_reframes_the_opening_session_update() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let bytes = client_wire(&session_update_fixture()); - let frames = [frame(&bytes)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("session.update decodes"); - let Ingress::Open(draft) = ingress else { - panic!("expected Ingress::Open, got {ingress:?}"); - }; - let draft = *draft; - let u = unit(draft.op, draft.body_ir, draft.facts); - - let dest = destination("api.openai.com", LaneId::new("realtime")); - let egress = plane - .encode_egress(&u, &dest, Some(&mut state), &c) - .expect("Unit 0 encodes an egress body"); - - let sent = egress.body.as_slice(); - assert!( - !sent.is_empty(), - "Unit 0's egress body must carry the opening frame, not an empty relay" - ); - let value: serde_json::Value = - serde_json::from_slice(sent).expect("the egress body is a shaped upstream JSON message"); - assert_eq!( - value.get("type").and_then(serde_json::Value::as_str), - Some("session.update"), - "the opening frame reaches the provider as a session.update" - ); - let session = value - .get("session") - .expect("the session config the client asked to apply travels upstream"); - assert_eq!( - session.get("voice").and_then(serde_json::Value::as_str), - Some("marin"), - "the negotiated voice must reach the provider" - ); - assert_eq!( - session - .get("instructions") - .and_then(serde_json::Value::as_str), - Some("You are a helpful voice agent."), - "the instructions must reach the provider" - ); - assert!( - session.get("turn_detection").is_some(), - "turn_detection must reach the provider" - ); - assert!( - session.get("tools").is_some(), - "the tool set must reach the provider" - ); -} - -/// A session whose FIRST frame is audio must have Unit 0 reach the provider as a dialect-shaped -/// `input_audio_buffer.append`, never as the raw PCM the byte pass-through shipped. The session's -/// stash is cleared first so the seam re-reads the opening frame off Unit 0's own body — proving the -/// body carries the client's wire frame (the fix), not the extracted relay. -#[test] -fn unit_zero_egress_reframes_an_opening_audio_frame() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let mut state = open_client_session(&plane, &c); - - let audio = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": "AAAA", - })) - .expect("audio fixture serializes"); - let frames = [frame(&audio)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the opening audio frame decodes"); - let Ingress::Open(draft) = ingress else { - panic!("an audio frame opens the turn too, got {ingress:?}"); - }; - let draft = *draft; - // The Unit-0 body must carry the client's own wire frame (not the extracted raw PCM relay): the - // egress seam re-reads it off the body to re-frame it onto the upstream dialect. - let u = unit(draft.op, draft.body_ir, draft.facts); - - let dest = destination("api.openai.com", LaneId::new("realtime")); - let egress = plane - .encode_egress(&u, &dest, Some(&mut state), &c) - .expect("Unit 0 encodes an egress body"); - - let sent = egress.body.as_slice(); - let value: serde_json::Value = serde_json::from_slice(sent) - .expect("the egress body is a shaped upstream JSON message, not raw PCM"); - assert_eq!( - value.get("type").and_then(serde_json::Value::as_str), - Some("input_audio_buffer.append"), - "an opening audio frame reaches the provider enveloped, not as raw PCM" - ); - assert!( - value - .get("audio") - .and_then(serde_json::Value::as_str) - .is_some_and(|a| !a.is_empty()), - "the enveloped append must carry the base64 audio" - ); -} - -/// The quantity `meter` states for `class`, or `None` when it states no line for it. -fn metered(locators: &busbar_contract::unit::UsageLocators, class: &str) -> Option { - locators - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == class) - .and_then(|l| l.quantity) -} - -/// A carrier `stop` mid-turn settles the open turn: what the caller spoke and what the upstream -/// opened since the last usage report travel on the ending, and `meter` reads them off it. -/// -/// Item 137. The `stop` arm took the open turn's counters out of the state and dropped them, so a -/// call that hung up mid-turn ended with an empty fact set: the ledger recorded none of the audio -/// relayed after the opening frame and none of the tool calls, where both were served. -#[test] -fn a_twilio_stop_settles_the_open_turn() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/twilio/call-123"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut state = PlaneSessionState::new(crate::session::VoiceSessionState::for_dialect( - Dialect::TwilioMediaStreams, - )); - - let start = serde_json::to_vec(&json!({ - "event": "start", - "start": { - "streamSid": "MZ1", - "callSid": "CA1", - "mediaFormat": { "encoding": "audio/x-mulaw", "sampleRate": 8000, "channels": 1 }, - }, - })) - .unwrap(); - let frames = [frame(&start)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("start decodes"); - - // G.711 µ-law at 8 kHz is 8 bytes a millisecond: 8 000 bytes is one second. - let media = serde_json::to_vec(&json!({ - "event": "media", - "streamSid": "MZ1", - "media": { "payload": base64_of(&vec![0xFFu8; 8_000]) }, - })) - .unwrap(); - // The first second OPENS the turn and is stated on its draft. - let frames = [frame(&media)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Open(draft) = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("media decodes") - else { - panic!("the first media frame opens the turn"); - }; - let unit = crate::tests::harness::unit(draft.op, draft.body_ir, draft.facts); - // The second second is RELAYED, which is where it is counted. - let frames = [frame(&media)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("media decodes"); - plane - .encode_ingress_frame(&unit, &frames[0], &dest, Some(&mut state), &c) - .expect("media relays"); - // The upstream opens one tool call before the caller hangs up. - let opened = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": "call_1", "name": "lookup" }, - })) - .unwrap(); - let frames = [frame(&opened)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_response(&mut cursor, &dest, Some(&mut state), &c) - .expect("a tool-call open decodes"); - - let stop = serde_json::to_vec(&json!({ "event": "stop", "streamSid": "MZ1" })).unwrap(); - let frames = [frame(&stop)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Close { facts, .. } = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("stop decodes") - else { - panic!("stop closes the open turn"); - }; - let ending = busbar_contract::plane::Response { - ir: busbar_contract::bounded::Ir::empty(), - finish: busbar_contract::unit::FinishClass::Partial, - facts: *facts, - }; - let locators = plane.meter(&unit, &ending, &c); - assert_eq!( - metered(&locators, "audio_seconds_in"), - Some(2), - "the opening second and the relayed second were both served" - ); - assert_eq!(metered(&locators, "tool_calls"), Some(1)); -} - -/// A barge-in bills what it served. -/// -/// Item 137. A client-authored interrupt superseded the open turn and reset its counters with the -/// correlation. A superseded turn is never answered, so no usage report ever metered it, and -/// everything the caller had already spoken — and every tool call the upstream had already opened — -/// vanished from the ledger. Carried onto the turn that takes over, it is emitted exactly once, on -/// that turn's terminal. -#[test] -fn a_barge_in_bills_what_the_interrupted_turn_served() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut state = open_client_session(&plane, &c); - - let opening = client_wire(&session_update_fixture()); - let frames = [frame(&opening)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Open(first) = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the turn opens") - else { - panic!("the first client event opens a turn"); - }; - let first_unit = crate::tests::harness::unit(first.op, first.body_ir, first.facts); - - // One second of PCM16 at 24 kHz, relayed on the first turn. - let append = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": base64_of(&vec![0u8; 48_000]), - })) - .unwrap(); - let frames = [frame(&append)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("audio decodes"); - plane - .encode_ingress_frame(&first_unit, &frames[0], &dest, Some(&mut state), &c) - .expect("audio relays"); - let opened = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": "call_1", "name": "lookup" }, - })) - .unwrap(); - let frames = [frame(&opened)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_response(&mut cursor, &dest, Some(&mut state), &c) - .expect("a tool-call open decodes"); - - // The caller talks over it. - let truncate = serde_json::to_vec(&json!({ - "type": "conversation.item.truncate", - "item_id": "item_1", - "content_index": 0, - "audio_end_ms": 640, - })) - .unwrap(); - let frames = [frame(&truncate)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Open(second) = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the barge-in decodes") - else { - panic!("a barge-in opens the turn that takes over"); - }; - let second_unit = crate::tests::harness::unit(second.op, second.body_ir, second.facts); - - let done = serde_json::to_vec(&json!({ - "type": "response.done", - "response": { "usage": { "input_token_details": { "audio_tokens": 1 } } } - })) - .unwrap(); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::Terminal { r, .. } = plane - .decode_response(&mut cursor, &dest, Some(&mut state), &c) - .expect("the usage report decodes") - else { - panic!("a usage report ends the turn"); - }; - let locators = plane.meter(&second_unit, &r, &c); - assert_eq!( - metered(&locators, "audio_seconds_in"), - Some(1), - "the second the caller spoke before barging in was served and must be metered" - ); - assert_eq!(metered(&locators, "tool_calls"), Some(1)); -} - -/// ONE READING OF A TURN (item 138: "one pipeline, or a proof they agree"). A host that drives the -/// session itself — the voice runtime, which serves every live session in production — ledgers -/// [`crate::session::class_counts`]; the unit loop ledgers what `meter` emits. Over the same turn -/// (one second of uplink audio, one tool call opened, a usage report naming all five token classes) -/// the two are the same set of `(class, count)` pairs. -#[test] -fn class_counts_are_the_lines_meter_emits_for_the_same_turn() { - let plane = openai_plane(); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - let dest = destination("api.openai.com", LaneId::new("realtime")); - let mut state = open_client_session(&plane, &c); - - let opening = client_wire(&session_update_fixture()); - let frames = [frame(&opening)]; - let mut cursor = FrameCursor::new(&frames); - let Ingress::Open(first) = plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("the turn opens") - else { - panic!("the first client event opens a turn"); - }; - let turn = crate::tests::harness::unit(first.op, first.body_ir, first.facts); - let append = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": base64_of(&vec![0u8; 48_000]), - })) - .unwrap(); - let frames = [frame(&append)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_ingress(&mut cursor, Some(&mut state), &c) - .expect("audio decodes"); - plane - .encode_ingress_frame(&turn, &frames[0], &dest, Some(&mut state), &c) - .expect("audio relays"); - let opened = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": "call_1", "name": "lookup" }, - })) - .unwrap(); - let frames = [frame(&opened)]; - let mut cursor = FrameCursor::new(&frames); - plane - .decode_response(&mut cursor, &dest, Some(&mut state), &c) - .expect("a tool-call open decodes"); - let done = serde_json::to_vec(&json!({ - "type": "response.done", - "response": { - "usage": { - "input_token_details": { "audio_tokens": 10, "text_tokens": 3, "cached_tokens": 1 }, - "output_token_details": { "audio_tokens": 20, "text_tokens": 4 }, - } - } - })) - .unwrap(); - let frames = [frame(&done)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::Terminal { r, .. } = plane - .decode_response(&mut cursor, &dest, Some(&mut state), &c) - .expect("the usage report decodes") - else { - panic!("a usage report ends the turn"); - }; - let mut emitted: Vec<(String, u64)> = plane - .meter(&turn, &r, &c) - .lines - .as_slice() - .iter() - .filter_map(|l| { - Some(( - l.class.as_str().to_string(), - l.quantity.filter(|n| *n != 0)?, - )) - }) - .collect(); - emitted.sort(); - - let usage = crate::codec::ir::IrDuplexUsage { - audio_in: 10, - audio_out: 20, - text_in: 3, - text_out: 4, - cached: 1, - }; - let mut counters = crate::session::TurnCounters::default(); - counters.admit_audio(crate::codec::ir::AudioFormat::Pcm16, 48_000); - counters.open_tool_call(); - let mut counted: Vec<(String, u64)> = crate::session::class_counts(Some(&usage), counters) - .into_iter() - .map(|(class, n)| (class.as_str().to_string(), n)) - .collect(); - counted.sort(); - - assert_eq!(counted, emitted, "one reading of one turn"); - // Six, not seven: cached_tokens is a SUBSET of audio_tokens_in/text_tokens_in, not a class beside - // them (#71), so it carries no line of its own even though this turn's usage report names it. - assert_eq!(counted.len(), 6, "every declared class carried a count"); -} diff --git a/crates/busbar-plane-streaming/src/tests/driven_tests.rs b/crates/busbar-plane-streaming/src/tests/driven_tests.rs new file mode 100644 index 0000000000..b02d0353cc --- /dev/null +++ b/crates/busbar-plane-streaming/src/tests/driven_tests.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-plane-streaming/src/driven.rs`. + +use super::*; + +#[test] +fn every_published_claim_arrives_at_its_door_in_its_dialect() { + let doors: Vec<_> = (0..5) + .map(|c| arrive(c).map(|a| (a.door, a.dialect, a.op_class))) + .collect(); + assert_eq!( + doors, + [ + Some((Door::Mint, 0, 0)), + Some((Door::Sdp, 0, 0)), + Some((Door::Sideband, 0, 0)), + Some((Door::Gemini, 1, 0)), + Some((Door::Twilio, 2, 0)), + ] + ); + assert_eq!( + arrive(5), + None, + "a claim the plane never published arrives nowhere (the metadata path is no claim)" + ); + assert_eq!(crate::door::ROUTES.len(), 5, "one door per published claim"); +} + +#[test] +fn the_mint_attempt_names_the_caller_and_carries_the_locked_session() { + let cfg = SessionConfig { + instructions: Some("locked".into()), + ..SessionConfig::default() + }; + let a = mint_attempt(&cfg, Some("vk-1"), None).expect("serializes"); + assert_eq!((a.verb, a.target), ("POST", "/v1/realtime/client_secrets")); + assert_eq!( + a.fields, + vec![ + ("content-type", "application/json".to_string()), + ("OpenAI-Safety-Identifier", "vk-1".to_string()), + ] + ); + let body: serde_json::Value = serde_json::from_slice(&a.body).expect("json"); + assert_eq!(body["expires_after"]["seconds"], 600); + assert_eq!(body["session"]["instructions"], "locked"); + assert!( + a.fields + .iter() + .all(|(n, _)| !n.eq_ignore_ascii_case("authorization")), + "the plane never writes the credential; the kernel adds it" + ); + let anonymous = mint_attempt(&cfg, None, None).expect("serializes"); + assert_eq!( + anonymous.fields, + vec![("content-type", "application/json".to_string())], + "no reference lent, no caller named" + ); +} + +#[test] +fn a_minted_secret_is_answered_and_a_failure_keeps_the_served_text() { + let ok = mint_reply(200, br#"{"value":"ek_1","expires_at":9}"#); + assert_eq!(ok.status, 200); + let v: serde_json::Value = serde_json::from_slice(&ok.body).expect("json"); + assert_eq!( + v, + serde_json::json!({"value": "ek_1", "expires_at_unix": 9}) + ); + + let refused = mint_reply(400, b"{}"); + assert_eq!(refused.status, 502); + assert_eq!( + String::from_utf8(refused.body).expect("utf-8"), + "streaming ephemeral-secret mint failed: ephemeral token mint failed: client-secret \ + endpoint returned 400 Bad Request" + ); + let not_ek = mint_reply(200, br#"{"value":"sk-real"}"#); + assert_eq!(not_ek.status, 502); + assert!(!String::from_utf8(not_ek.body) + .expect("utf-8") + .contains("sk-real")); +} + +#[test] +fn the_sdp_offer_is_relayed_and_its_answer_names_the_call() { + let a = sdp_attempt(b"v=0\r\n"); + assert_eq!((a.verb, a.target), ("POST", "/v1/realtime/calls")); + assert_eq!( + a.fields, + vec![("content-type", "application/sdp".to_string())] + ); + assert_eq!(a.body, b"v=0\r\n"); + + let r = sdp_reply(201, Some("/v1/realtime/calls/rtc_abc"), b"v=0 answer"); + assert_eq!(r.status, 201); + assert_eq!(r.rtc_call_id.as_deref(), Some("rtc_abc")); + assert_eq!( + r.fields, + vec![ + ("content-type", "application/sdp".to_string()), + ("location", "/v1/realtime/calls/rtc_abc".to_string()), + ] + ); + assert_eq!(sdp_reply(400, None, b"no").rtc_call_id, None); +} + +#[test] +fn a_status_prints_with_its_reason_phrase_when_it_has_one() { + assert_eq!(status_text(502), "502 Bad Gateway"); + assert_eq!(status_text(299), "299"); +} + +#[test] +fn each_loop_step_has_the_planes_answer() { + use busbar_contract::abi::plane::PRINCIPAL_REQUIRED; + assert_eq!(Door::Mint.authenticate(), PRINCIPAL_REQUIRED); + assert_eq!(Door::Twilio.authenticate(), PRINCIPAL_REQUIRED); + assert_eq!(Door::Sideband.verify(), "session.model"); + assert_eq!(Door::Sideband.approve(), ("session", "streaming-server")); + assert!( + Door::Sideband.admit().is_empty(), + "a session pays for what the far end reports" + ); + let cfg = SessionConfig::default(); + let sock = Door::Gemini + .route(&cfg, None, b"") + .expect("routes") + .expect("a session dials"); + assert_eq!(sock.verb, "GET"); + assert!( + !sock.target.contains("key="), + "the credential is never in the target" + ); + assert_eq!( + Door::Sdp.route(&cfg, None, b"v=0").expect("routes"), + Some(sdp_attempt(b"v=0")) + ); + let mut units = crate::session_unit::CumulativeUnits::default(); + units.0[1] = 20; + units.0[4] = 2; + assert_eq!(Door::Sideband.meter(&units), vec![(1, 20), (4, 2)]); + assert_eq!(Door::Mint.audit(), "streaming.session.open"); +} diff --git a/crates/busbar-plane-streaming/src/tests/harness.rs b/crates/busbar-plane-streaming/src/tests/harness.rs deleted file mode 100644 index 80dad2b5c2..0000000000 --- a/crates/busbar-plane-streaming/src/tests/harness.rs +++ /dev/null @@ -1,249 +0,0 @@ -//! The smallest thing that can call this plane. -//! -//! The same minimum-size harness `busbar-plane-llm`'s own (unused-so-far) test harness builds: an -//! arena, a clock, a configuration view, a transport view and a label set, plus the kernel-built -//! values (a unit, a verified destination) handed through a seal. Nothing here is shipped. - -use busbar_contract::bounded::{ - Facts, Ir, Labels, PlaneAlloc, PlaneAllocBudget, ScratchBytes, SlabBytes, Span, -}; -use busbar_contract::dest::{DestinationFacts, VerifiedDestination}; -use busbar_contract::ids::{LaneId, OpClassId, SessionId, StreamId}; -use busbar_contract::unit::{Clock, ConfigView, Ctx, Origin, SessionView, TransportView, Unit}; -use busbar_contract::wire::{Direction, Frame, FrameMeta}; -use std::sync::Arc; - -/// An arena that never reuses a byte — fine for a short test process. -#[derive(Debug, Default)] -pub struct LeakPlaneAlloc; - -impl PlaneAlloc for LeakPlaneAlloc { - fn alloc_bytes<'a>(&'a self, src: &[u8]) -> Result, PlaneAllocBudget> { - Ok(ScratchBytes::new(Box::leak( - src.to_vec().into_boxed_slice(), - ))) - } - - fn alloc_str<'a>(&'a self, src: &str) -> Result<&'a str, PlaneAllocBudget> { - Ok(Box::leak(src.to_string().into_boxed_str())) - } - - fn alloc_spans<'a>( - &'a self, - src: &[(&'a str, Span)], - ) -> Result<&'a [(&'a str, Span)], PlaneAllocBudget> { - Ok(Box::leak(src.to_vec().into_boxed_slice())) - } - - fn remaining(&self) -> usize { - usize::MAX - } -} - -/// A configuration block with nothing in it. -#[derive(Debug, Default)] -pub struct EmptyConfig; - -impl ConfigView for EmptyConfig { - fn get_str(&self, _key: &str) -> Option<&str> { - None - } - fn get_int(&self, _key: &str) -> Option { - None - } - fn get_bool(&self, _key: &str) -> Option { - None - } -} - -/// A transport stack that publishes a request target as a fact — the same convention -/// `busbar-plane-llm`'s harness uses for its own `path` fact. -#[derive(Debug)] -pub struct WsStack { - path: String, -} - -impl WsStack { - /// A stack that saw this request target. - #[must_use] - pub fn new(path: &str) -> Self { - Self { - path: path.to_string(), - } - } -} - -impl TransportView for WsStack { - fn key(&self) -> &'static str { - "ws" - } - fn chain(&self) -> &[&'static str] { - &["tcp", "tls", "http", "ws"] - } - fn fact(&self, key: &str) -> Option<&str> { - if key == "path" { - Some(&self.path) - } else { - None - } - } -} - -/// A session that reports zero paired upstreams, for the calls that take one. -/// -/// Not yet used by a test in this crate (every current test drives `decode_ingress`/`decode_response` -/// through a bare `Ctx` with no session), kept as shared harness for the session-bound tests a future -/// pass adds — the same reason `busbar-plane-llm`'s own harness carries fixtures its current test -/// file does not all exercise yet. -#[allow(dead_code)] -#[derive(Debug)] -pub struct FreshSession; - -impl SessionView for FreshSession { - fn id(&self) -> SessionId { - SessionId(0) - } - fn is_bound(&self) -> bool { - true - } - fn session_fact(&self, _key: &str) -> Option<&str> { - None - } - fn transport_fact(&self, _key: &str) -> Option<&str> { - None - } - fn upstream_count(&self) -> usize { - 0 - } -} - -/// A session that already holds one paired upstream and reports the client dialect it was opened -/// on, for the paired-turn routing tests. The `dialect` is the name the plane wrote to its declared -/// `dialect` session fact at session open (`Dialect::name`). -#[derive(Debug)] -pub struct PairedSession { - dialect: &'static str, - upstreams: usize, -} - -impl PairedSession { - /// A session opened on `dialect`, holding `upstreams` paired upstreams. - #[must_use] - pub fn new(dialect: &'static str, upstreams: usize) -> Self { - Self { dialect, upstreams } - } -} - -impl SessionView for PairedSession { - fn id(&self) -> SessionId { - SessionId(7) - } - fn is_bound(&self) -> bool { - true - } - fn session_fact(&self, key: &str) -> Option<&str> { - if key == "dialect" { - Some(self.dialect) - } else { - None - } - } - fn transport_fact(&self, _key: &str) -> Option<&str> { - None - } - fn upstream_count(&self) -> usize { - self.upstreams - } -} - -/// The blessed TEST seal (#65). `KernelSeal` is SEALED — no crate outside `busbar-contract` can -/// implement it — so a fixture names the contract's own `test-seal` type instead of forging one. -/// The type system stops a plugin now, not the manifest allow-list alone. -pub use busbar_contract::plugin::TestKernelSeal as TestSeal; - -/// Build a context over the pieces above. -#[must_use] -pub fn ctx<'u>( - arena: &'u LeakPlaneAlloc, - config: &'u EmptyConfig, - transport: &'u WsStack, - labels: &'u Labels<'u>, -) -> Ctx<'u> { - Ctx::new( - Clock { - unix_secs: 1_772_000_000, - monotonic_nanos: 0, - }, - config, - None, - transport, - labels, - arena, - ) -} - -/// Build a context that carries a session — for the steps that read `Ctx::session()`. -#[must_use] -pub fn ctx_with_session<'u>( - arena: &'u LeakPlaneAlloc, - config: &'u EmptyConfig, - transport: &'u WsStack, - labels: &'u Labels<'u>, - session: &'u dyn SessionView, -) -> Ctx<'u> { - Ctx::new( - Clock { - unix_secs: 1_772_000_000, - monotonic_nanos: 0, - }, - config, - Some(session), - transport, - labels, - arena, - ) -} - -/// One inbound frame carrying a whole wire event. -#[must_use] -pub fn frame(bytes: &[u8]) -> Frame { - Frame { - direction: Direction::Inbound, - stream: StreamId(0), - bytes: SlabBytes::new(Arc::from(bytes.to_vec().into_boxed_slice())), - meta: FrameMeta::default(), - } -} - -/// A unit built the way the kernel builds one, over a decoded body. -#[must_use] -pub fn unit<'u>(op: OpClassId, body: Ir<'u>, facts: Facts<'u>) -> Unit<'u> { - Unit::new( - &TestSeal, - busbar_contract::UnitKey::new(1), - Origin::Client, - None, - Some(StreamId(0)), - Direction::Inbound, - None, - op, - body, - facts, - None, - ) -} - -/// A destination sealed the way the trust unit seals one. -#[must_use] -pub fn destination(host: &'static str, lane: LaneId) -> VerifiedDestination { - VerifiedDestination::seal( - &TestSeal, - DestinationFacts::Upstream { - transport: "ws", - address: busbar_contract::UpstreamAddress::socket(host), - lane, - }, - "ws", - None, - ) -} diff --git a/crates/busbar-plane-streaming/src/tests/ladder_tests.rs b/crates/busbar-plane-streaming/src/tests/ladder_tests.rs new file mode 100644 index 0000000000..f14f513c55 --- /dev/null +++ b/crates/busbar-plane-streaming/src/tests/ladder_tests.rs @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE LADDER RED for `crates/busbar-plane-streaming/src/door.rs`: the plane's lines on a guest +//! list reproduce the served plane's dispatch ladder exactly (`BUSBAR-1.6.0.md` section 6, "Auth +//! points and guest lists", step 3). Every served route resolves to the same (dialect, door), and +//! a request no served route took resolves to no line. A request whose path a line matches under +//! another method is answered 405 with that line's Allow, as the served plane answered it. +//! +//! The match here is the spec's, stated in the test: a line matches when its method set holds the +//! method and its path does (exact bytes, or a pattern where `{…}` is exactly one non-empty +//! segment); an exact path ranks above a pattern, and two matching lines at equal rank are the +//! ambiguity boot refuses. + +use super::*; +use crate::driven::Door; + +/// The rank of a line whose path matches `path`; `None` when it does not match. +fn path_rank(line: &Route, path: &str) -> Option { + if line.exact() { + return (line.target == path).then_some(2); + } + let want: Vec<&str> = line.target.split('/').collect(); + let got: Vec<&str> = path.split('/').collect(); + if want.len() != got.len() { + return None; + } + let all = want.iter().zip(&got).all(|(w, g)| { + if w.starts_with('{') && w.ends_with('}') { + !g.is_empty() + } else { + w == g + } + }); + all.then_some(1) +} + +/// The rank of a line that matches (`method`, `path`); `None` when it does not match. +fn rank(line: &Route, method: &str, path: &str) -> Option { + if line.verb != method { + return None; + } + path_rank(line, path) +} + +/// The methods the lines whose path matches `path` take: the 405's Allow, empty when none does. +fn allow(path: &str) -> Vec<&'static str> { + ROUTES + .iter() + .filter(|l| path_rank(l, path).is_some()) + .map(|l| l.verb) + .collect() +} + +/// The line `(method, path)` resolves to, by index; `Err` on an equal-rank ambiguity. +fn resolve(method: &str, path: &str) -> Result, String> { + let mut best: Option<(u8, usize)> = None; + let mut tied = false; + for (i, line) in ROUTES.iter().enumerate() { + let Some(r) = rank(line, method, path) else { + continue; + }; + match best { + Some((b, _)) if r < b => {} + Some((b, _)) if r == b => tied = true, + _ => { + best = Some((r, i)); + tied = false; + } + } + } + if tied { + return Err(format!("{method} {path} matches two lines at equal rank")); + } + Ok(best.map(|(_, i)| i)) +} + +/// The served plane's routes, as its mount table and its one telephony claim answer them. +const SERVED: &[(&str, &str, Door, u32, &str)] = &[ + ( + "POST", + "/v1/realtime/client_secrets", + Door::Mint, + 0, + KEY_AUTH, + ), + ("POST", "/v1/realtime/calls", Door::Sdp, 0, KEY_AUTH), + ( + "GET", + "/v1/realtime/sideband/rtc_abc", + Door::Sideband, + 0, + KEY_AUTH, + ), + ( + "GET", + "/v1/realtime/gemini/call-1", + Door::Gemini, + 1, + KEY_AUTH, + ), + ("GET", "/twilio/CA123", Door::Twilio, 2, SIGNATURE_AUTH), +]; + +#[test] +fn every_served_route_resolves_to_the_same_dialect_and_door() { + for &(method, path, door, dialect, auth) in SERVED { + let i = resolve(method, path) + .expect("no ambiguity") + .unwrap_or_else(|| panic!("{method} {path} resolves to no line")); + assert_eq!(Door::of(i as u32), Some(door), "{method} {path}"); + assert_eq!(ROUTES[i].dialect, dialect, "{method} {path}"); + assert_eq!(ROUTES[i].refusal_dialect, dialect, "{method} {path}"); + assert_eq!(ROUTES[i].auth, auth, "{method} {path}"); + assert_eq!(door.dialect(), dialect, "the door's dialect is its line's"); + } + assert_eq!(SERVED.len(), ROUTES.len(), "every line is a served route"); +} + +#[test] +fn a_request_no_served_route_took_resolves_to_no_line() { + for (method, path) in [ + ("POST", "/v1/realtime"), + ("GET", "/v1/realtime"), + ("GET", "/v1/realtime/sideband"), + ("GET", "/v1/realtime/sideband/"), + ("GET", "/v1/realtime/sideband/a/b"), + ("GET", "/v1/realtime/telephony/CA123"), + ("GET", "/twilio"), + ("GET", "/twilio/"), + ("GET", "/twilio/a/b"), + ("GET", "/twiliofoo/x"), + ("POST", "/v1/audio/transcriptions"), + ] { + assert_eq!(resolve(method, path), Ok(None), "{method} {path}"); + assert!( + allow(path).is_empty(), + "{method} {path} is no-route, not 405" + ); + } +} + +#[test] +fn a_served_path_under_another_method_is_405_with_the_lines_allow() { + for (method, path, allowed) in [ + ("GET", "/v1/realtime/client_secrets", "POST"), + ("GET", "/v1/realtime/calls", "POST"), + ("POST", "/v1/realtime/sideband/rtc_abc", "GET"), + ("POST", "/v1/realtime/gemini/call-1", "GET"), + ("POST", "/twilio/CA123", "GET"), + ] { + assert_eq!( + resolve(method, path), + Ok(None), + "{method} {path} takes no line" + ); + assert_eq!( + allow(path), + [allowed], + "{method} {path} is 405, Allow: {allowed}" + ); + } +} + +#[test] +fn the_sockets_are_upgrade_lines_and_the_one_request_doors_are_not() { + for (i, line) in ROUTES.iter().enumerate() { + let door = Door::of(i as u32).expect("a door per line"); + assert_eq!(line.upgrade, door.is_session(), "{}", line.target); + assert_eq!( + line.carrier, + if line.upgrade { + WS_TRANSPORT + } else { + HTTP_TRANSPORT + }, + "an upgrade line hands the connection to the socket transport" + ); + } +} + +#[test] +fn no_two_lines_tie_and_each_lines_own_witness_reaches_it() { + for (i, line) in ROUTES.iter().enumerate() { + let witness: String = line + .target + .split('/') + .map(|seg| if seg.starts_with('{') { "w" } else { seg }) + .collect::>() + .join("/"); + assert_eq!( + resolve(line.verb, &witness), + Ok(Some(i)), + "{} {}", + line.verb, + witness + ); + } +} + +#[test] +fn no_line_is_open_and_the_telephony_line_is_signed() { + let open: Vec<_> = ROUTES + .iter() + .filter(|r| r.open()) + .map(|r| r.target) + .collect(); + assert!( + open.is_empty(), + "no door is read without a credential: the metadata path is not claimed (1.5.5 parity)" + ); + let signed: Vec<_> = ROUTES + .iter() + .filter(|r| r.auth == SIGNATURE_AUTH) + .map(|r| r.target) + .collect(); + assert_eq!(signed, ["/twilio/{call_id}"]); +} diff --git a/crates/busbar-plane-streaming/src/tests/mod.rs b/crates/busbar-plane-streaming/src/tests/mod.rs index 6883f59a4e..f8b5dfa576 100644 --- a/crates/busbar-plane-streaming/src/tests/mod.rs +++ b/crates/busbar-plane-streaming/src/tests/mod.rs @@ -1,13 +1,7 @@ -//! Test-only wiring: the shared harness, the codec/µ-law fixture tests, and the purity/determinism +//! Test-only wiring: the selector, purity/determinism //! and style checks the crate doc comments promise elsewhere (`lib.rs`'s [`crate::StreamingPlane`] doc //! comment cites [`purity`] by name). -pub mod harness; - -mod codec; -mod twilio; -mod ulaw; - /// What a request path matches, decided the same way the boot's overlap check decides it. mod selectors { use crate::claims::matches_selector; @@ -25,6 +19,48 @@ mod selectors { assert!(!matches_selector(&s, "/twilio/inbound/deeper")); assert!(!matches_selector(&s, "/other/inbound")); } + + /// Twilio Media Streams is a streaming DIALECT carried by the WebSocket transport, not a + /// transport of its own (the design lists five transports and no telephony one). Its claim is + /// on the WebSocket transport, one path level under `/twilio`, and authenticates under the + /// `webhook-signature` alternative alone: Twilio holds no busbar bearer or API key. Without the + /// claim the dialect's reader and codec are unreachable: no arrival ever names it. + #[test] + fn twilio_media_streams_is_claimed_on_the_websocket_carrier_under_the_webhook_signature() { + use crate::claims::{dialect_for, Dialect, DIALECT_CLAIMS, WS_TRANSPORT}; + use crate::StreamingPlane; + use busbar_contract::plane::PlaneMeta; + + let twilio: Vec<_> = DIALECT_CLAIMS + .iter() + .filter(|c| c.dialect == Dialect::TwilioMediaStreams) + .collect(); + assert_eq!( + twilio.len(), + 1, + "exactly one claim names the Twilio dialect" + ); + let claim = &twilio[0].claim; + assert_eq!( + claim.transport, WS_TRANSPORT, + "Twilio rides the WebSocket transport" + ); + assert_eq!(claim.selector, Selector::PrefixOneLevel("/twilio")); + assert_eq!(claim.scheme_alternatives, &["webhook-signature"]); + assert!( + ::CLAIMS.contains(claim), + "the declared claim is one the boot seal reads" + ); + // An arrival one level under `/twilio` names the dialect; the prefix alone and a deeper + // path do not, and neither does another dialect's path. + assert_eq!( + dialect_for("/twilio/inbound"), + Some(Dialect::TwilioMediaStreams) + ); + assert_eq!(dialect_for("/twilio"), None); + assert_eq!(dialect_for("/twilio/a/b"), None); + assert_eq!(dialect_for("/v1/realtime"), Some(Dialect::OpenaiRealtime)); + } } /// Purity and determinism: the properties every plane in the design is held to ("pure over its @@ -35,8 +71,8 @@ mod purity { /// `StreamingPlane` derives `Copy`. Every interior-mutable cell (`Cell`, `RefCell`, `Mutex`, /// `OnceLock`, ...) is `!Copy`, so a type that IS `Copy` structurally cannot hold one: this is /// a compile-time proof, not a convention, that the plane keeps no mutable state of its own - /// across calls — everything that varies across a session lives in the kernel-held - /// `PlaneSessionState` instead (see `session::VoiceSessionState`). + /// across calls: everything that varies across a session lives in the session's own state + /// (see `session_unit::SessionUnit`). const fn assert_copy() {} const _: () = assert_copy::(); @@ -82,100 +118,6 @@ mod purity { // vectors in both directions, and a round trip over all 256 bytes that pins the decoded sample. } -/// Where a paired turn is routed: to the upstream the session actually DIALED, not the first -/// configured one. A session opened on a duplex client dialect dialed the upstream that speaks it; -/// every later turn of that session must name that same upstream, or a turn is metered on the wrong -/// provider's lane. -mod route { - use crate::claims::Dialect; - use crate::tests::harness::{ - ctx_with_session, EmptyConfig, LeakPlaneAlloc, PairedSession, WsStack, - }; - use crate::{StreamingPlane, Upstream}; - use busbar_contract::bounded::{FactValue, Facts, Ir, Labels}; - use busbar_contract::dest::DestinationFacts; - use busbar_contract::ids::{LaneId, OpClassId}; - use busbar_contract::plane::Plane; - - /// Two upstreams, gemini declared SECOND, each on its own priced lane. - static UPSTREAMS: &[Upstream] = &[ - Upstream { - lane: LaneId::new("realtime-openai"), - host: "api.openai.example", - dialect: Dialect::OpenaiRealtime, - }, - Upstream { - lane: LaneId::new("realtime-gemini"), - host: "api.gemini.example", - dialect: Dialect::GeminiLive, - }, - ]; - - /// P-ITEM: WRONG-PROVIDER ATTRIBUTION (spec DONE item 2, "All P-item behaviours match 1.5.5"; - /// fixed red-before-green in e15713578a). A paired turn of a gemini-live session routes to the - /// gemini upstream — its real config index and its own priced lane — not to the first-declared - /// openai upstream. Before the fix the paired branch hard-coded `UpstreamIdx(0)` and - /// `upstreams().first().lane`, so a gemini session's every turn was billed on the openai lane: - /// the wrong provider's money. The 1.5.5 behaviour this matches is its one surface's (the llm - /// surface; owner correction 2026-09-28): usage is ledgered and metered against the SERVING lane - /// and its provider, never another configured one (v1.5.5 `crates/busbar/src/proxy/usage.rs:57-103`, - /// "`lane` is the SERVING lane (post-failover)"). A bug fixed to match, never a signed diff. - #[test] - fn p_item_wrong_provider_attribution_a_paired_turn_routes_to_the_dialed_upstream_not_the_first() - { - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/openai/realtime"); - let labels = Labels::new(); - let session = PairedSession::new(Dialect::GeminiLive.name(), 1); - let cx = ctx_with_session(&arena, &config, &transport, &labels, &session); - - let mut facts = Facts::new(); - let _ = facts.set("dialect", FactValue::Str(Dialect::GeminiLive.name())); - let u = crate::tests::harness::unit(OpClassId::new("duplex_turn"), Ir::empty(), facts); - - match plane.verify(&u, &cx) { - DestinationFacts::SessionUpstream { upstream, lane, .. } => { - assert_eq!( - lane, - LaneId::new("realtime-gemini"), - "a gemini session's turn must bill on the gemini lane it dialed" - ); - assert_eq!( - upstream.0, 1, - "the fact must name the gemini upstream's real config index" - ); - } - other => panic!("a paired turn must route to a session upstream, got {other:?}"), - } - } - - /// A paired turn of an openai-realtime session still routes to the openai upstream (index 0), - /// so the fix does not simply invert the bug. - #[test] - fn a_paired_openai_turn_still_routes_to_the_openai_upstream() { - let plane = StreamingPlane::new(UPSTREAMS); - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/openai/realtime"); - let labels = Labels::new(); - let session = PairedSession::new(Dialect::OpenaiRealtime.name(), 1); - let cx = ctx_with_session(&arena, &config, &transport, &labels, &session); - - let u = - crate::tests::harness::unit(OpClassId::new("duplex_turn"), Ir::empty(), Facts::new()); - - match plane.verify(&u, &cx) { - DestinationFacts::SessionUpstream { upstream, lane, .. } => { - assert_eq!(lane, LaneId::new("realtime-openai")); - assert_eq!(upstream.0, 0); - } - other => panic!("a paired turn must route to a session upstream, got {other:?}"), - } - } -} - /// Style rules this crate holds itself to, checked rather than merely asserted in prose: no /// section-sign citation and no parity-binding identifier (a two-letter prefix, a hyphen and /// digits, e.g. a two-letter code followed by a hyphen and a number) anywhere in this crate's own source — the same hard rule diff --git a/crates/busbar-plane-streaming/src/tests/open_calls_tests.rs b/crates/busbar-plane-streaming/src/tests/open_calls_tests.rs index 096003aa44..17c359ca7a 100644 --- a/crates/busbar-plane-streaming/src/tests/open_calls_tests.rs +++ b/crates/busbar-plane-streaming/src/tests/open_calls_tests.rs @@ -26,7 +26,7 @@ fn plan(table: &OpenToolCalls, session: u64, unit: u64, call_id: &str, now: Mill /// The plane's declared reply deadline, in milliseconds. fn deadline() -> u64 { - u64::from(crate::plane::TOOL_REPLY_DEADLINE_SECS) * 1_000 + u64::from(crate::open_calls::TOOL_REPLY_DEADLINE_SECS) * 1_000 } /// **Two calls open at once, and each reply wakes only its own.** diff --git a/crates/busbar-plane-streaming/src/tests/request_unit_tests.rs b/crates/busbar-plane-streaming/src/tests/request_unit_tests.rs new file mode 100644 index 0000000000..c3d36fe93f --- /dev/null +++ b/crates/busbar-plane-streaming/src/tests/request_unit_tests.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-plane-streaming/src/request_unit.rs`. + +use super::*; +use crate::broker::{CALLS_PATH, CLIENT_SECRETS_PATH, SAFETY_IDENTIFIER_HEADER}; +use crate::driven::MINT_FAILED_STATUS; + +fn unit(door: Door, caller_ref: Option<&str>) -> RequestUnit { + RequestUnit::open( + door, + SessionConfig::default(), + caller_ref.map(str::to_owned), + ) + .expect("a one-request door") +} + +fn piece(from: From, bytes: &[u8], last: bool) -> Piece<'_> { + Piece { + from, + bytes, + status: None, + last, + head: &[], + } +} + +fn far_first<'a>( + status: u16, + bytes: &'a [u8], + last: bool, + head: &'a [(&'a [u8], &'a [u8])], +) -> Piece<'a> { + Piece { + from: From::FarEnd, + bytes, + status: Some(status), + last, + head, + } +} + +fn reply(a: Answer) -> Reply { + match a { + Answer::ToCaller(r) => r, + other => panic!("expected the caller's answer, got {other:?}"), + } +} + +#[test] +fn a_session_door_is_no_request_unit() { + for door in [Door::Sideband, Door::Gemini, Door::Twilio] { + assert!(RequestUnit::open(door, SessionConfig::default(), None).is_none()); + } +} + +#[test] +fn the_mint_attempt_is_whole_and_the_callers_body_is_not_read() { + let mut u = unit(Door::Mint, Some("ab12")); + let Answer::Attempt(a) = u.on_piece(piece(From::Kernel(1), &[], false)) else { + panic!("the ATTEMPT is answered with the mint request"); + }; + assert_eq!((a.verb, a.target), ("POST", CLIENT_SECRETS_PATH)); + assert!(a + .fields + .iter() + .any(|(n, v)| *n == SAFETY_IDENTIFIER_HEADER && v == "ab12")); + assert!(!a.body.is_empty(), "the mint body goes with the ATTEMPT"); + assert_eq!( + u.on_piece(piece(From::Caller, b"{\"ignored\":true}", true)), + Answer::Nothing + ); +} + +#[test] +fn a_mint_with_no_caller_reference_names_no_caller() { + let mut u = unit(Door::Mint, None); + let Answer::Attempt(a) = u.on_piece(piece(From::Kernel(1), &[], false)) else { + panic!("the ATTEMPT is answered with the mint request"); + }; + assert!(a.fields.iter().all(|(n, _)| *n != SAFETY_IDENTIFIER_HEADER)); +} + +#[test] +fn the_far_ends_mint_answer_is_gathered_then_answered_once() { + let mut u = unit(Door::Mint, None); + let _ = u.on_piece(piece(From::Kernel(1), &[], false)); + assert_eq!( + u.on_piece(far_first(200, b"{\"value\":\"ek_1\",", false, &[])), + Answer::Nothing + ); + let r = reply(u.on_piece(piece(From::FarEnd, b"\"expires_at\":7}", true))); + assert_eq!(r.status, 200); + assert_eq!( + serde_json::from_slice::(&r.body).expect("json"), + serde_json::json!({"value": "ek_1", "expires_at_unix": 7}) + ); + assert!(u.answered()); + assert_eq!( + u.on_piece(piece(From::FarEnd, b"late", true)), + Answer::Nothing + ); +} + +#[test] +fn a_refused_mint_is_the_served_502_text() { + let mut u = unit(Door::Mint, None); + let _ = u.on_piece(piece(From::Kernel(1), &[], false)); + let r = reply(u.on_piece(far_first(401, b"nope", true, &[]))); + assert_eq!(r.status, MINT_FAILED_STATUS); + assert_eq!( + r.body, + b"streaming ephemeral-secret mint failed: ephemeral token mint failed: \ + client-secret endpoint returned 401 Unauthorized" + ); +} + +#[test] +fn the_sdp_offer_is_relayed_and_the_answer_keeps_its_location() { + let mut u = unit(Door::Sdp, None); + let Answer::Attempt(a) = u.on_piece(piece(From::Kernel(1), &[], false)) else { + panic!("the ATTEMPT is answered with the SDP request head"); + }; + assert_eq!((a.verb, a.target), ("POST", CALLS_PATH)); + assert!(a.body.is_empty(), "the offer follows as the caller's body"); + assert_eq!( + u.on_piece(piece(From::Caller, b"v=0\r\n", false)), + Answer::ToFarEnd(b"v=0\r\n".to_vec()) + ); + assert_eq!(u.on_piece(piece(From::Caller, b"", true)), Answer::Nothing); + let head: &[(&[u8], &[u8])] = &[(b"location", b"/v1/realtime/calls/rtc_abc")]; + let r = reply(u.on_piece(far_first(201, b"v=0 answer", true, head))); + assert_eq!(r.status, 201); + assert_eq!(r.body, b"v=0 answer"); + assert_eq!(r.rtc_call_id.as_deref(), Some("rtc_abc")); + assert!(r + .fields + .iter() + .any(|(n, v)| *n == FIELD_LOCATION && v == "/v1/realtime/calls/rtc_abc")); +} + +#[test] +fn a_new_attempt_forgets_the_previous_far_end() { + let mut u = unit(Door::Sdp, None); + let _ = u.on_piece(piece(From::Kernel(1), &[], false)); + let head: &[(&[u8], &[u8])] = &[(b"location", b"/calls/rtc_old")]; + let _ = u.on_piece(far_first(503, b"stale", false, head)); + let _ = u.on_piece(piece(From::Kernel(2), &[], false)); + let r = reply(u.on_piece(far_first(201, b"fresh", true, &[]))); + assert_eq!((r.status, r.body.as_slice()), (201, &b"fresh"[..])); + assert_eq!(r.rtc_call_id, None); +} + +#[test] +fn a_far_end_answer_with_no_status_is_refused() { + let mut u = unit(Door::Sdp, None); + let _ = u.on_piece(piece(From::Kernel(1), &[], false)); + assert_eq!(u.on_piece(piece(From::FarEnd, b"x", true)), Answer::Refused); +} + +#[test] +fn a_piece_from_the_kernel_that_is_no_attempt_is_refused() { + let mut u = unit(Door::Mint, None); + assert_eq!( + u.on_piece(piece(From::Kernel(0), &[], false)), + Answer::Refused + ); +} diff --git a/crates/busbar-plane-streaming/src/tests/session_door_tests.rs b/crates/busbar-plane-streaming/src/tests/session_door_tests.rs new file mode 100644 index 0000000000..de875f6334 --- /dev/null +++ b/crates/busbar-plane-streaming/src/tests/session_door_tests.rs @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-plane-streaming/src/session_door.rs`: a live session on the door, +//! one frame per answer, each side's frames in order, the session's end answered on the caller's +//! side only, and the ceiling read on the tick clock. + +use std::num::NonZeroU32; + +use super::*; + +fn json(v: serde_json::Value) -> Vec { + serde_json::to_vec(&v).expect("json") +} + +fn text(f: &[u8]) -> String { + String::from_utf8_lossy(f).into_owned() +} + +/// `ms` milliseconds of 24 kHz PCM16 uplink audio (48 bytes a millisecond). +fn uplink(ms: usize) -> Vec { + json(serde_json::json!({"type":"input_audio_buffer.append", + "audio": busbar_contract::media::base64_encode(&vec![0u8; ms * 48])})) +} + +fn usage_done() -> Vec { + json( + serde_json::json!({"type":"response.done","response":{"usage":{ + "input_token_details":{"audio_tokens":10,"text_tokens":3}, + "output_token_details":{"audio_tokens":20,"text_tokens":4}}}}), + ) +} + +fn audio_delta(item: &str) -> Vec { + json(serde_json::json!({"type":"response.output_audio.delta", + "delta": busbar_contract::media::base64_encode(&[0u8; 96]), + "item_id": item, "output_index":0, "content_index":0})) +} + +fn sideband() -> Live { + Live::open(Door::Sideband, &StreamsCfg::default()).expect("a session door") +} + +/// The class index of audio seconds in, in the tail's order. +fn seconds() -> u32 { + crate::session_unit::CLASSES + .iter() + .position(|c| *c == crate::meta::CLASS_AUDIO_SECONDS_IN) + .expect("declared") as u32 +} + +#[test] +fn a_one_request_door_opens_no_session() { + for door in [Door::Mint, Door::Sdp] { + assert!( + Live::open(door, &StreamsCfg::default()).is_none(), + "{door:?}" + ); + } +} + +#[test] +fn a_caller_frame_is_one_turn_toward_the_far_end() { + let mut s = sideband(); + s.from_caller(&uplink(1000), false); + let emit = s.next(true); + let Some((Side::FarEnd, frame)) = emit.frame else { + panic!("a frame toward the far end: {emit:?}"); + }; + assert!(text(&frame).contains("input_audio_buffer.append")); + assert!(!emit.done); + assert_eq!( + s.next(true), + Emit::default(), + "one frame, nothing more owed" + ); + assert!(!s.ready()); +} + +#[test] +fn the_far_ends_usage_reaches_the_caller_and_the_sessions_units() { + let mut s = sideband(); + s.from_caller(&uplink(2000), false); + let _ = s.next(true); + s.from_far_end(&usage_done(), 0); + assert_eq!( + s.units(), + vec![ + (0, 10), + (1, 20), + (2, 3), + (3, 4), + (seconds(), 2), + (PER_SESSION_CLASS, 1) + ], + "the closed turn's tokens, its admitted audio, and the fee the answered session incurred" + ); + let emit = s.next(false); + assert!( + matches!(&emit.frame, Some((Side::Caller, f)) if text(f).contains("response.done")) + || emit.frame.is_none(), + "{emit:?}" + ); +} + +/// A barge-in owes the far end two frames (cancel, truncate) and the caller one: a far-side answer +/// reaches only the caller, so it takes the caller's frame and leaves the far end's, in order, to +/// the caller side's collections. +#[test] +fn a_far_side_answer_carries_only_a_frame_for_the_caller() { + let mut s = sideband(); + s.from_far_end(&audio_delta("it1"), 0); + while s.next(false).frame.is_some() {} + s.from_far_end( + &json( + serde_json::json!({"type":"input_audio_buffer.speech_started", + "audio_start_ms":0,"item_id":"it1"}), + ), + 0, + ); + let far = s.next(false); + assert!( + matches!(&far.frame, Some((Side::Caller, f)) if text(f).contains("speech_started")), + "{far:?}" + ); + assert_eq!(s.next(false), Emit::default(), "no caller frame left"); + assert!( + s.ready(), + "the far end's frames are owed: the driver is named" + ); + let first = s.next(true); + let second = s.next(true); + assert!( + matches!(&first.frame, Some((Side::FarEnd, f)) if text(f).contains("response.cancel")), + "{first:?}" + ); + assert!( + matches!(&second.frame, Some((Side::FarEnd, f)) if text(f).contains("conversation.item.truncate")), + "{second:?}" + ); + assert!(!s.ready()); +} + +/// The caller's last piece settles the open turn once: the audio it admitted is in the session's +/// units, and only the caller side answers the end. +#[test] +fn the_callers_last_piece_settles_the_open_turn_and_ends_the_session() { + let mut s = sideband(); + s.from_caller(&uplink(3000), false); + let _ = s.next(true); + assert!(s.units().is_empty(), "no turn closed yet"); + s.from_caller(&[], true); + assert_eq!(s.units(), vec![(seconds(), 3)]); + assert!( + !s.next(false).done, + "a far-side answer never ends the session" + ); + assert!(s.next(true).done); +} + +#[test] +fn the_telephony_door_reads_the_carriers_envelope() { + let mut s = Live::open(Door::Twilio, &StreamsCfg::default()).expect("a session door"); + s.from_caller( + &json( + serde_json::json!({"event":"start","start":{"streamSid":"MZ1","callSid":"CA1", + "mediaFormat":{"encoding":"audio/x-mulaw","sampleRate":8000,"channels":1}}}), + ), + false, + ); + assert_eq!(s.next(true), Emit::default()); + s.from_caller( + &json(serde_json::json!({"event":"media","streamSid":"MZ1", + "media":{"payload": busbar_contract::media::base64_encode(&[0x7f; 160])}})), + false, + ); + let emit = s.next(true); + assert!( + matches!(&emit.frame, Some((Side::FarEnd, f)) if text(f).contains("input_audio_buffer.append")), + "{emit:?}" + ); + s.from_caller(&json(serde_json::json!({"event":"stop"})), false); + assert!(s.next(true).done, "a stop ends the call"); +} + +/// The ceiling is read on the tick clock: the session's clock starts at its first tick, and a +/// session past its ceiling tells the caller why and then ends. +#[test] +fn a_session_past_its_ceiling_is_told_why_then_ends() { + let cfg = StreamsCfg { + session_max_secs: NonZeroU32::new(5), + ..StreamsCfg::default() + }; + let mut s = Live::open(Door::Sideband, &cfg).expect("a session door"); + assert!(!s.tick(1_000), "the clock starts"); + assert!(!s.tick(1_000 + 4 * NS_PER_SEC), "inside its ceiling"); + assert!(s.tick(1_000 + 5 * NS_PER_SEC), "at its ceiling"); + let told = s.next(true); + assert!( + matches!(&told.frame, Some((Side::Caller, f)) if text(f).contains("session_max_secs")), + "{told:?}" + ); + assert!(s.next(true).done); + assert!( + !s.tick(1_000 + 9 * NS_PER_SEC), + "an ended session is told once" + ); +} + +#[test] +fn a_session_with_no_ceiling_runs_on() { + let mut s = sideband(); + assert!(!s.tick(0)); + assert!(!s.tick(u64::MAX / 2)); + assert!(!s.ready()); +} + +#[test] +fn a_cancel_on_either_sides_ticket_ends_the_session() { + let t = |slot| Ticket { + slot, + generation: 1, + }; + let mut all = Sessions::default(); + let cfg = StreamsCfg::default(); + for stream in [7, 8] { + assert!(all.get_or_open(stream, Door::Sideband, &cfg).is_some()); + } + all.crossed(7, t(1)); + all.crossed(7, t(2)); + all.crossed(8, t(3)); + assert!(!all.cancel(t(9)), "a ticket no session crossed on"); + assert!(all.cancel(t(2)), "the far side's ticket"); + assert!(all.get(7).is_none()); + assert!(!all.cancel(t(1)), "its other ticket is forgotten with it"); + assert_eq!(all.len(), 1); + assert!(all.get_or_open(9, Door::Mint, &cfg).is_none()); +} + +#[test] +fn drive_names_the_sessions_that_owe_output() { + let mut all = Sessions::default(); + let cfg = StreamsCfg::default(); + for stream in [3, 4, 5] { + let _ = all.get_or_open(stream, Door::Sideband, &cfg); + } + assert!(all.ready(8).is_empty()); + all.get(4).expect("open").from_caller(&[], true); + all.get(5).expect("open").from_caller(&uplink(10), false); + assert_eq!(all.ready(8), vec![4, 5]); + assert_eq!(all.ready(1), vec![4]); + assert_eq!(all.ready_count(), 2); +} + +/// THE SESSION'S FEE UNIT (ARCHITECT Q-L5-FEE (A); Q17-6): `per_session` is reported `1` once the +/// far end first answers the session, never before. A session whose far end never answered (its +/// open failed) reports no fee unit however much its caller sent, so the kernel refunds the fee. +#[test] +fn the_fee_unit_is_incurred_when_the_far_end_first_answers() { + let mut s = sideband(); + s.from_caller(&uplink(1000), false); + let _ = s.next(true); + assert!( + !s.units().iter().any(|(c, _)| *c == PER_SESSION_CLASS), + "no far answer, no fee unit: {:?}", + s.units() + ); + s.from_caller(&[], true); + assert!( + !s.units().iter().any(|(c, _)| *c == PER_SESSION_CLASS), + "an open that never reached the far end ends with no fee unit" + ); + let mut s = sideband(); + s.from_far_end(&audio_delta("it1"), 0); + assert_eq!( + s.units() + .iter() + .filter(|(c, _)| *c == PER_SESSION_CLASS) + .collect::>(), + vec![&(PER_SESSION_CLASS, 1)], + "the far end's first answer incurs it once" + ); + s.from_far_end(&audio_delta("it2"), 0); + assert!(s.units().contains(&(PER_SESSION_CLASS, 1)), "still one"); +} + +/// THE SESSION-OPEN REWRITE (ARCHITECT Q-L5B-PROJECT; the retired streams crate's projection, +/// verbatim): a hook's rewrite is a PATCH over the locked params (a key it names wins, a key it does +/// not keep the plane's value, `null` clears); output that is not JSON, not an object, or not a +/// session config once merged is refused, never opened as if no rewrite had been made. +#[test] +fn a_session_open_rewrite_patches_the_locked_params_or_refuses() { + let locked = SessionConfig { + instructions: Some("be brief".to_string()), + voice: Some("alloy".to_string()), + ..SessionConfig::default() + }; + let patched = + committed_session_config(&locked, br#"{"voice":"marin"}"#).expect("a readable patch"); + assert_eq!( + patched.voice.as_deref(), + Some("marin"), + "the named key wins" + ); + assert_eq!( + patched.instructions.as_deref(), + Some("be brief"), + "an unnamed key keeps the locked value" + ); + let cleared = committed_session_config(&locked, br#"{"instructions":null}"#).expect("clear"); + assert_eq!(cleared.instructions, None, "naming null clears"); + for bad in [&b"not json"[..], b"7", br#"{"voice":7}"#] { + assert!( + committed_session_config(&locked, bad).is_err(), + "{}", + String::from_utf8_lossy(bad) + ); + } +} + +/// A session opened after its open's hooks rewrote the params opens under the rewritten params, +/// once; a stream with none opens under what its door locks. +#[test] +fn a_rewritten_session_open_locks_the_rewritten_params_once() { + let cfg = StreamsCfg::default(); + assert_eq!(Live::locked(Door::Sideband, &cfg), cfg.session); + assert_eq!( + Live::locked(Door::Twilio, &cfg), + crate::session_params::g711_config() + ); + let mut sessions = Sessions::default(); + let rewritten = SessionConfig { + voice: Some("marin".to_string()), + ..cfg.session.clone() + }; + sessions.rewrite(7, rewritten); + assert!(sessions.get_or_open(7, Door::Sideband, &cfg).is_some()); + sessions.close(7); + assert!( + sessions.rewritten.is_empty(), + "consumed at the open and gone with the session" + ); +} diff --git a/crates/busbar-plane-streaming/src/tests/session_pump_tests.rs b/crates/busbar-plane-streaming/src/tests/session_pump_tests.rs index ce99ec9552..6c57d0fe3a 100644 --- a/crates/busbar-plane-streaming/src/tests/session_pump_tests.rs +++ b/crates/busbar-plane-streaming/src/tests/session_pump_tests.rs @@ -5,6 +5,8 @@ use std::sync::{Arc, Mutex}; +use bytes::Bytes; + use super::*; use crate::codec::ir::codec::OpenAiRealtimeCodec; use crate::governed::{GovernedCalls, ReplyRefusal}; @@ -39,14 +41,6 @@ impl TurnSink for Turns { } } -fn serves_all(_: &str) -> bool { - true -} - -fn serves_none(_: &str) -> bool { - false -} - fn usage_done() -> WireEvent { wire(serde_json::json!({ "type": "response.done", @@ -78,8 +72,8 @@ fn a_usage_report_closes_the_turn_with_its_counters_and_the_session_continues() "type":"input_audio_buffer.append","audio": b64 }))); assert_eq!(up.upstream.len(), 1, "uplink audio is written through"); - let (out, runs) = p.on_server_frame(usage_done(), 0, &mut sink, &serves_all); - assert!(!out.close && runs.is_empty()); + let out = p.on_server_frame(usage_done(), 0, &mut sink); + assert!(!out.close); assert_eq!(sink.closed.len(), 1); let (usage, counters) = sink.closed[0]; let usage = usage.expect("the turn carries its usage"); @@ -97,7 +91,7 @@ fn a_sink_that_refuses_cuts_the_session_and_tells_the_far_end_to_stop() { refuse: true, ..Turns::default() }; - let (out, _) = p.on_server_frame(usage_done(), 0, &mut sink, &serves_all); + let out = p.on_server_frame(usage_done(), 0, &mut sink); assert!(out.close); assert!(texts(&out.upstream).contains("response.cancel")); } @@ -114,16 +108,14 @@ fn a_barge_in_cancels_and_truncates_at_what_was_heard() { ), 0, &mut sink, - &serves_all, ); - let (out, _) = p.on_server_frame( + let out = p.on_server_frame( wire( serde_json::json!({"type":"input_audio_buffer.speech_started", "audio_start_ms":0,"item_id":"it7"}), ), 0, &mut sink, - &serves_all, ); let up = texts(&out.upstream); assert!(up.contains("response.cancel"), "{up}"); @@ -134,24 +126,33 @@ fn a_barge_in_cancels_and_truncates_at_what_was_heard() { assert!(texts(&out.downlink).contains("speech_started")); } +/// LAW 11 (QUESTIONS Q98): a tool call is part of the model's response. Every frame of it is relayed +/// to the caller as the model streamed it, its arguments verbatim, and nothing the gateway writes +/// upstream answers it: no `function_call_output`, no `response.create`. RED: the pump handed a +/// closed call to a server-side executor and wrote the executor's output upstream itself. #[test] -fn a_tool_call_is_run_once_on_its_close_with_its_accumulated_arguments() { +fn a_tool_call_is_relayed_to_the_caller_as_is_and_the_gateway_answers_nothing() { let mut p = pump(); let mut sink = Turns::default(); - let mut runs = Vec::new(); + let (mut down, mut up) = (Vec::new(), Vec::new()); for f in call_frames("ca", "alpha", "{\"x\":1}") { - let (_, r) = p.on_server_frame(f, 0, &mut sink, &serves_all); - runs.extend(r); + let out = p.on_server_frame(f, 0, &mut sink); + down.extend(out.downlink); + up.extend(out.upstream); } - assert_eq!(runs.len(), 1); - assert_eq!(runs[0].name, "alpha"); - assert_eq!(runs[0].call_id, "ca"); - assert_eq!(runs[0].args, b"{\"x\":1}"); - let mut out = Outbound::default(); - p.tool_executed(runs.remove(0), b"{\"ok\":true}".to_vec(), &mut out); - let up = texts(&out.upstream); - assert!(up.contains("function_call_output") && up.contains("\"call_id\":\"ca\"")); - assert!(up.contains("response.create")); + let down = texts(&down); + assert!( + down.contains("\"name\":\"alpha\"") + && down.contains("\"call_id\":\"ca\"") + && down.contains("{\\\"x\\\":1}") + && down.contains("function_call_arguments.done"), + "the call's open, its arguments and its close reach the caller: {down}" + ); + assert!( + up.is_empty(), + "the gateway writes nothing upstream for a call: {}", + texts(&up) + ); p.settle_open_turn(&mut sink); assert_eq!(sink.closed.len(), 1); assert_eq!( @@ -162,47 +163,72 @@ fn a_tool_call_is_run_once_on_its_close_with_its_accumulated_arguments() { /// P-ITEM: VOICE TOOL-ARGS (spec DONE item 2, "All P-item behaviours match 1.5.5"; the drive log's /// P5, commit 470351a480: "streamed tool-call arguments are discarded"). The model streams a call's -/// arguments as several partial-JSON fragments; they reach the executor CONCATENATED, as one call, -/// run once, on the call's close and not before. +/// arguments as several partial-JSON fragments; none of them is lost, and they reach the caller +/// whole, in order, as one call, counted once. /// /// Voice is new in 1.6.0; the 1.5.5 behaviour it matches is the llm surface's own streamed tool /// call (owner correction 2026-09-28): 1.5.5 accumulated every `InputJsonDelta` fragment of an open /// tool block and emitted the call ONCE on its block stop with the fully reassembled arguments, /// because parsing each fragment alone lost the arguments and split one call into many (v1.5.5 -/// `crates/busbar/src/proto/gemini/writer.rs:734-780`). +/// `crates/busbar/src/proto/gemini/writer.rs:734-780`). The streaming plane holds no tool executor +/// (LAW 11, QUESTIONS Q98): the caller runs the call, so the pin is on what the caller receives, +/// and the gateway answers nothing upstream on any fragment or on the close. #[test] -fn p_item_voice_tool_args_streamed_fragments_reach_the_executor_whole_and_once() { +fn p_item_voice_tool_args_streamed_fragments_reach_the_caller_whole_and_once() { let mut p = pump(); let mut sink = Turns::default(); - let mut runs = Vec::new(); + let mut down = Vec::new(); let open = wire(serde_json::json!({"type":"response.output_item.added", "item":{"type":"function_call","call_id":"cw","name":"weather"}})); - let (_, r) = p.on_server_frame(open, 0, &mut sink, &serves_all); - assert!(r.is_empty(), "an announced call is not run"); + let out = p.on_server_frame(open, 0, &mut sink); + assert!(out.upstream.is_empty(), "an announced call is not answered"); + down.extend(out.downlink); for fragment in ["{\"lo", "c\":\"S", "F\"}"] { let delta = wire( serde_json::json!({"type":"response.function_call_arguments.delta", "call_id":"cw","delta":fragment}), ); - let (_, r) = p.on_server_frame(delta, 0, &mut sink, &serves_all); + let out = p.on_server_frame(delta, 0, &mut sink); assert!( - r.is_empty(), - "a call is not run on a fragment of its arguments" + out.upstream.is_empty(), + "a fragment of a call's arguments is not answered" ); + down.extend(out.downlink); } let done = wire( serde_json::json!({"type":"response.function_call_arguments.done", "call_id":"cw"}), ); - let (_, r) = p.on_server_frame(done, 0, &mut sink, &serves_all); - runs.extend(r); - assert_eq!(runs.len(), 1, "one call, run once on its close"); - assert_eq!(runs[0].name, "weather"); - assert_eq!(runs[0].call_id, "cw"); + let out = p.on_server_frame(done, 0, &mut sink); + assert!(out.upstream.is_empty(), "a closed call is not answered"); + down.extend(out.downlink); + let frames: Vec = down + .iter() + .map(|w| serde_json::from_slice(&w.0).expect("a downlink frame is json")) + .collect(); + let args: String = frames + .iter() + .filter(|f| f["type"] == "response.function_call_arguments.delta") + .map(|f| f["delta"].as_str().expect("a delta carries its fragment")) + .collect(); + assert_eq!( + args, + "{\"loc\":\"SF\"}", + "the fragments reach the caller whole, in order: {}", + texts(&down) + ); assert_eq!( - runs[0].args, b"{\"loc\":\"SF\"}", - "the fragments reach the executor whole, in order" + frames + .iter() + .filter(|f| f["type"] == "response.function_call_arguments.done") + .count(), + 1, + "one call, closed once: {}", + texts(&down) ); + p.settle_open_turn(&mut sink); + assert_eq!(sink.closed.len(), 1); + assert_eq!(sink.closed[0].1.tool_calls, 1, "one call, counted once"); } /// A table that records what the pump asked of it. @@ -241,7 +267,7 @@ impl GovernedCalls for Table { } #[test] -fn a_call_the_session_does_not_serve_waits_for_the_callers_reply() { +fn a_relayed_call_waits_for_the_callers_reply() { let table = Arc::new(Table::default()); let mut p = pump(); p.bind_governed(GovernedSession { @@ -250,11 +276,8 @@ fn a_call_the_session_does_not_serve_waits_for_the_callers_reply() { }); let mut sink = Turns::default(); for f in call_frames("cc", "lookup", "{}") { - let (_, runs) = p.on_server_frame(f, 42, &mut sink, &serves_none); - assert!( - runs.is_empty(), - "the gateway never answers a call it does not serve" - ); + let out = p.on_server_frame(f, 42, &mut sink); + assert!(out.upstream.is_empty(), "the gateway never answers a call"); } assert_eq!( *table.planned.lock().expect("lock"), @@ -308,17 +331,138 @@ fn a_session_past_its_ceiling_is_told_why_in_its_dialect() { fn an_error_closes_the_open_turn_before_it_is_relayed() { let mut p = pump(); let mut sink = Turns::default(); - let (out, _) = p.on_server_frame( + let out = p.on_server_frame( wire(serde_json::json!({"type":"error","error":{"code":"x","message":"y"}})), 0, &mut sink, - &serves_all, ); assert_eq!(sink.closed.len(), 1); assert!(sink.closed[0].0.is_none()); assert!(texts(&out.downlink).contains("error")); } +#[test] +fn an_ungoverned_session_relays_a_call_and_never_answers_it() { + // No open-call table bound (an ungoverned node): the call reaches the caller, and nothing the + // gateway writes answers it. + let mut p = pump(); + let mut sink = Turns::default(); + let (mut down, mut up) = (Vec::new(), Vec::new()); + for f in call_frames("cu", "unknown_tool", "{\"q\":1}") { + let out = p.on_server_frame(f, 0, &mut sink); + down.extend(out.downlink); + up.extend(out.upstream); + } + let down = texts(&down); + assert!( + down.contains("unknown_tool") && down.contains("\"call_id\":\"cu\""), + "the call is relayed to the caller: {down}" + ); + assert!( + !texts(&up).contains("function_call_output"), + "no result is authored upstream by the gateway" + ); +} + +fn uplink_ms(ms: usize) -> WireEvent { + wire(serde_json::json!({"type":"input_audio_buffer.append", + "audio": busbar_contract::media::base64_encode(&vec![0u8; ms * 48])})) +} + +#[test] +fn two_open_tool_calls_wait_on_two_different_correlations() { + let table = Arc::new(Table::default()); + let mut p = pump(); + p.bind_governed(GovernedSession { + session: 1, + calls: table.clone(), + }); + let mut sink = Turns::default(); + for f in call_frames("cc", "a", "{}") + .into_iter() + .chain(call_frames("dd", "b", "{}")) + { + let _ = p.on_server_frame(f, 0, &mut sink); + } + let planned: Vec = table + .planned + .lock() + .expect("lock") + .iter() + .map(|(_, c, _)| c.clone()) + .collect(); + assert_eq!(planned, ["cc", "dd"], "each call is its own wait"); + let reply = |id: &str| { + wire( + serde_json::json!({"type":"conversation.item.create","item":{ + "type":"function_call_output","call_id":id,"output":"{}"}}), + ) + }; + assert!(!p.on_client_frame(reply("dd")).refused_reply); + assert!( + p.on_client_frame(reply("dd")).refused_reply, + "a wait is woken once" + ); + assert!(!p.on_client_frame(reply("cc")).refused_reply); +} + +#[test] +fn an_upstream_error_still_meters_the_turn_it_ended() { + let mut p = pump(); + let mut sink = Turns::default(); + let _ = p.on_client_frame(uplink_ms(40_000)); + for id in ["e1", "e2"] { + let open = call_frames(id, "t", "{}"); + let [first, ..] = open; + let _ = p.on_server_frame(first, 0, &mut sink); + } + let _ = p.on_server_frame( + wire(serde_json::json!({"type":"error","error":{"code":"x","message":"y"}})), + 0, + &mut sink, + ); + assert_eq!(sink.closed.len(), 1); + let closed = sink.closed[0].1; + assert_eq!((closed.audio_ms_in, closed.tool_calls), (40_000, 2)); +} + +#[test] +fn a_barge_in_bills_what_the_interrupted_turn_served_on_the_turn_that_takes_over() { + let mut p = pump(); + let mut sink = Turns::default(); + let _ = p.on_client_frame(uplink_ms(1_000)); + let [open, ..] = call_frames("b1", "t", "{}"); + let _ = p.on_server_frame(open, 0, &mut sink); + let _ = p.on_server_frame( + wire( + serde_json::json!({"type":"input_audio_buffer.speech_started", + "audio_start_ms":0,"item_id":"it"}), + ), + 0, + &mut sink, + ); + assert!( + sink.closed.is_empty(), + "a barge-in closes no turn and drops no counter" + ); + let _ = p.on_server_frame(usage_done(), 0, &mut sink); + assert_eq!(sink.closed.len(), 1); + let closed = sink.closed[0].1; + assert_eq!((closed.audio_ms_in, closed.tool_calls), (1_000, 1)); +} + +#[test] +fn a_caller_frame_that_is_not_utf8_or_names_no_known_event_goes_nowhere() { + let mut p = pump(); + let mut sink = Turns::default(); + let bad = p.on_client_frame(WireEvent(Bytes::from_static(&[0xff, 0xfe, 0x00]))); + assert!(bad.upstream.is_empty() && bad.downlink.is_empty() && !bad.close); + let unknown = p.on_client_frame(wire(serde_json::json!({"type":"teleport.now"}))); + assert!(unknown.upstream.is_empty() && unknown.downlink.is_empty()); + p.settle_open_turn(&mut sink); + assert!(sink.closed.is_empty(), "nothing was counted"); +} + /// The `audio_seconds_in` every closed turn bills, summed over the session. fn billed_audio_seconds(sink: &Turns) -> u64 { sink.closed @@ -342,7 +486,7 @@ fn twenty_turns_of_1050_ms_bill_21_audio_seconds_not_40() { let _ = p.on_client_frame(wire(serde_json::json!({ "type":"input_audio_buffer.append","audio": b64 }))); - let _ = p.on_server_frame(usage_done(), 0, &mut sink, &serves_all); + let _ = p.on_server_frame(usage_done(), 0, &mut sink); } assert_eq!(sink.closed.len(), 20); assert_eq!(billed_audio_seconds(&sink), 21); @@ -361,7 +505,7 @@ fn the_part_of_a_millisecond_a_frame_leaves_over_is_carried_not_floored() { "type":"input_audio_buffer.append","audio": b64 }))); } - let _ = p.on_server_frame(usage_done(), 0, &mut sink, &serves_all); + let _ = p.on_server_frame(usage_done(), 0, &mut sink); assert_eq!(sink.closed.len(), 1); assert_eq!(sink.closed[0].1.audio_ms_in, 1000); assert_eq!(billed_audio_seconds(&sink), 1); diff --git a/crates/busbar-plane-streaming/src/tests/session_unit_tests.rs b/crates/busbar-plane-streaming/src/tests/session_unit_tests.rs new file mode 100644 index 0000000000..855354cc52 --- /dev/null +++ b/crates/busbar-plane-streaming/src/tests/session_unit_tests.rs @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-plane-streaming/src/session_unit.rs`. + +use super::*; +use crate::codec::ir::codec::OpenAiRealtimeCodec; +use crate::session_params::g711_config; + +fn json(v: serde_json::Value) -> Vec { + serde_json::to_vec(&v).expect("json") +} + +fn text(frames: &[Vec]) -> String { + frames + .iter() + .map(|f| String::from_utf8_lossy(f).into_owned()) + .collect::>() + .join("\n") +} + +fn start(encoding: &str) -> Vec { + json( + serde_json::json!({"event":"start","start":{"streamSid":"MZ1","callSid":"CA1", + "mediaFormat":{"encoding":encoding,"sampleRate":8000,"channels":1}}}), + ) +} + +fn media(sid: &str, bytes: &[u8]) -> Vec { + json(serde_json::json!({"event":"media","streamSid":sid, + "media":{"payload": busbar_contract::media::base64_encode(bytes)}})) +} + +fn telephony() -> SessionUnit { + SessionUnit::open(OpenAiRealtimeCodec, g711_config(), true, 0, None) +} + +#[test] +fn the_callers_audio_on_the_telephony_door_reaches_the_far_end() { + // The served telephony leg handed Twilio's `event`-tagged frames to the realtime reader, which + // reads only `type`-tagged ones: the caller's audio reached no one. + let mut s = telephony(); + assert_eq!(s.from_caller(&start("audio/x-mulaw")), Plan::default()); + let plan = s.from_caller(&media("MZ1", &[0x7f; 160])); + let up = text(&plan.to_far_end); + assert!(up.contains("input_audio_buffer.append"), "{up}"); + assert!( + up.contains(&busbar_contract::media::base64_encode(&[0x7f; 160])), + "the caller's audio passes through unchanged: {up}" + ); +} + +#[test] +fn media_for_another_stream_and_unmodelled_events_reach_no_one() { + let mut s = telephony(); + let _ = s.from_caller(&start("audio/x-mulaw")); + assert_eq!(s.from_caller(&media("MZ-forged", &[1; 8])), Plan::default()); + assert_eq!( + s.from_caller(&json(serde_json::json!({"event":"something-new"}))), + Plan::default(), + "an event this reader does not model is dropped, not a session end" + ); + assert!(!s.ended()); +} + +#[test] +fn a_call_in_another_media_format_ends_the_session() { + let mut s = telephony(); + assert!(s.from_caller(&start("audio/l16")).end); + assert!(s.ended()); +} + +#[test] +fn model_audio_and_a_barge_in_reach_the_caller_in_the_carriers_envelope() { + let mut s = telephony(); + let _ = s.from_caller(&start("audio/x-mulaw")); + let delta = json(serde_json::json!({"type":"response.output_audio.delta", + "delta": busbar_contract::media::base64_encode(&[9; 16]), + "item_id":"it1","output_index":0,"content_index":0})); + let plan = s.from_far_end(&delta, 0); + let down = text(&plan.to_caller); + assert!( + down.contains("\"event\":\"media\"") && down.contains("MZ1"), + "{down}" + ); + let barge = json( + serde_json::json!({"type":"input_audio_buffer.speech_started", + "audio_start_ms":0,"item_id":"it1"}), + ); + let plan = s.from_far_end(&barge, 0); + assert!(text(&plan.to_caller).contains("\"event\":\"clear\"")); + assert!(text(&plan.to_far_end).contains("response.cancel")); +} + +#[test] +fn a_stop_settles_the_open_turns_counters_once() { + let mut s = telephony(); + let _ = s.from_caller(&start("audio/x-mulaw")); + // Two seconds of 8 kHz mu-law: 8 bytes a millisecond. + let _ = s.from_caller(&media("MZ1", &[0; 16_000])); + assert_eq!(s.units(), CumulativeUnits::default(), "no turn closed yet"); + assert!( + s.from_caller(&json(serde_json::json!({"event":"stop"}))) + .end + ); + let seconds = CLASSES + .iter() + .position(|c| *c == meta::CLASS_AUDIO_SECONDS_IN) + .expect("declared"); + assert_eq!(s.units().0[seconds], 2); + let _ = s.end(); + assert_eq!(s.units().0[seconds], 2, "a second end settles nothing"); +} + +#[test] +fn usage_reports_accumulate_across_turns() { + let mut s = SessionUnit::open( + OpenAiRealtimeCodec, + SessionConfig::default(), + false, + 0, + None, + ); + let done = json( + serde_json::json!({"type":"response.done","response":{"usage":{ + "input_token_details":{"audio_tokens":10,"text_tokens":3,"cached_tokens":2}, + "output_token_details":{"audio_tokens":20,"text_tokens":4}}}}), + ); + let _ = s.from_far_end(&done, 0); + let _ = s.from_far_end(&done, 0); + assert_eq!( + s.units().0, + [20, 40, 6, 8, 0, 0], + "cached tokens are attribution, never billed" + ); +} + +#[test] +fn a_session_past_its_ceiling_is_told_why_and_ends() { + let mut s = SessionUnit::open( + OpenAiRealtimeCodec, + SessionConfig::default(), + false, + 1_000, + Some(2), + ); + assert_eq!(s.tick(1_000 + 1_999_999_999, 0), Plan::default()); + let plan = s.tick(1_000 + 2_000_000_000, 0); + assert!(plan.end); + assert!(text(&plan.to_caller).contains(crate::session_pump::SESSION_CEILING_REASON)); + assert!(s.ended()); +} + +#[test] +fn the_sessions_record_is_written_at_open_on_its_call_id_and_at_its_end() { + let mut s = SessionUnit::open_as( + OpenAiRealtimeCodec, + SessionConfig::default(), + false, + 5_000_000_000, + None, + "call-1", + "ref-abc", + ); + let opened = s.take_writes(); + assert_eq!(opened.len(), 1); + assert_eq!( + ( + opened[0].id.as_str(), + opened[0].owner.as_str(), + opened[0].updated_at, + opened[0].terminal + ), + ("call-1", "ref-abc", 5, false) + ); + s.set_rtc_call_id("rtc_9", 6); + let _ = s.end(); + let _ = s.end(); + let later = s.take_writes(); + assert_eq!(later.len(), 2, "the call id once, the end once"); + assert_eq!(later[0].rtc_call_id.as_deref(), Some("rtc_9")); + assert!(later[1].terminal && later[1].rtc_call_id.as_deref() == Some("rtc_9")); + assert!(s.take_writes().is_empty()); +} + +#[test] +fn admitted_milliseconds_meter_as_whole_seconds_rounded_up() { + assert_eq!(meta::audio_seconds_in(0), 0); + assert_eq!(meta::audio_seconds_in(1), 1); + assert_eq!(meta::audio_seconds_in(1_500), 2); + assert_eq!(meta::audio_seconds_in(3_000), 3); +} + +#[test] +fn a_stop_settles_the_open_turns_tool_calls_too() { + let mut s = telephony(); + let _ = s.from_caller(&start("audio/x-mulaw")); + let _ = s.from_caller(&media("MZ1", &[0; 8_000])); + let open = json(serde_json::json!({"type":"response.output_item.added", + "item":{"type":"function_call","call_id":"c1","name":"lookup"}})); + let _ = s.from_far_end(&open, 0); + assert!( + s.from_caller(&json(serde_json::json!({"event":"stop"}))) + .end + ); + assert_eq!(s.units().0, [0, 0, 0, 0, 1, 1]); +} + +#[test] +fn one_full_turn_reports_the_six_classes_the_served_plane_metered() { + let mut s = SessionUnit::open( + OpenAiRealtimeCodec, + SessionConfig::default(), + false, + 0, + None, + ); + let pcm = busbar_contract::media::base64_encode(&vec![0u8; 48_000]); + let _ = s.from_caller(&json( + serde_json::json!({"type":"input_audio_buffer.append","audio":pcm}), + )); + let open = json(serde_json::json!({"type":"response.output_item.added", + "item":{"type":"function_call","call_id":"c1","name":"lookup"}})); + let _ = s.from_far_end(&open, 0); + let done = json( + serde_json::json!({"type":"response.done","response":{"usage":{ + "input_token_details":{"audio_tokens":10,"text_tokens":3,"cached_tokens":1}, + "output_token_details":{"audio_tokens":20,"text_tokens":4}}}}), + ); + let _ = s.from_far_end(&done, 0); + assert_eq!(s.units().0, [10, 20, 3, 4, 1, 1]); +} + +#[test] +fn a_start_with_no_stream_id_refuses_the_call() { + let mut s = telephony(); + let empty = json( + serde_json::json!({"event":"start","start":{"streamSid":"","callSid":"CA1", + "mediaFormat":{"encoding":"audio/x-mulaw","sampleRate":8000,"channels":1}}}), + ); + assert!(s.from_caller(&empty).end); + assert!(s.ended()); + assert_eq!( + s.from_caller(&media("", &[1; 80])), + Plan::default(), + "no binding was opened for an empty stream id" + ); +} + +#[test] +fn a_media_payload_resuming_after_padding_is_refused_and_never_billed() { + for payload in ["QQ==QQ==".to_string(), "QQ==".repeat(2_000)] { + let mut s = telephony(); + let _ = s.from_caller(&start("audio/x-mulaw")); + let frame = json(serde_json::json!({"event":"media","streamSid":"MZ1", + "media":{"payload": payload}})); + let plan = s.from_caller(&frame); + assert!(plan.end && plan.to_far_end.is_empty()); + assert_eq!(s.units(), CumulativeUnits::default(), "nothing is billed"); + } +} + +#[test] +fn every_emitted_frame_is_a_text_message_and_either_opcode_is_read() { + const { assert!(FRAMES_ARE_TEXT) }; + // Gemini Live's server frames arrive binary: the bytes are read as they are. + let mut s = SessionUnit::open( + crate::codec::ir::codec::gemini::GeminiLiveCodec, + SessionConfig::default(), + false, + 0, + None, + ); + let setup = json(serde_json::json!({"setupComplete": {}})); + let plan = s.from_far_end(&setup, 0); + for frame in plan.to_caller.iter().chain(plan.to_far_end.iter()) { + assert!( + std::str::from_utf8(frame).is_ok(), + "an emitted frame is text" + ); + } +} diff --git a/crates/busbar-plane-streaming/src/tests/sessions_tests.rs b/crates/busbar-plane-streaming/src/tests/sessions_tests.rs new file mode 100644 index 0000000000..0dfbe36c5b --- /dev/null +++ b/crates/busbar-plane-streaming/src/tests/sessions_tests.rs @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! Tests for `crates/busbar-plane-streaming/src/sessions.rs`. + +use super::*; + +fn json(v: serde_json::Value) -> Vec { + serde_json::to_vec(&v).expect("json") +} + +/// The telephony door opens the one Twilio reader's session. RED arm: a telephony session opened +/// without the envelope reads a Twilio frame as nothing, so the caller's audio reaches no one. +#[test] +fn the_telephony_door_opens_a_session_behind_the_twilio_envelope() { + let locked = SessionConfig::default(); + let mut s = open(Door::Twilio, &locked, 0, None, "call-1", "ref").expect("a session door"); + let start = json( + serde_json::json!({"event":"start","start":{"streamSid":"MZ1", + "callSid":"CA1","mediaFormat":{"encoding":"audio/x-mulaw","sampleRate":8000,"channels":1}}}), + ); + let _ = s.from_caller(&start); + let media = json(serde_json::json!({"event":"media","streamSid":"MZ1", + "media":{"payload": busbar_contract::media::base64_encode(&[0x7f; 80])}})); + let plan = s.from_caller(&media); + assert_eq!( + plan.to_far_end.len(), + 1, + "the caller's audio reaches the far end" + ); + assert!(String::from_utf8_lossy(&plan.to_far_end[0]).contains("input_audio_buffer.append")); + + // The RED arm: the same frames on the browser sideband, where no envelope is expected. + let mut bare = open(Door::Sideband, &locked, 0, None, "call-2", "ref").expect("a session"); + let _ = bare.from_caller(&start); + assert!(bare.from_caller(&media).to_far_end.is_empty()); +} + +#[test] +fn a_one_request_door_opens_no_session() { + let locked = SessionConfig::default(); + for door in [Door::Mint, Door::Sdp] { + assert!( + open(door, &locked, 0, None, "x", "ref").is_none(), + "{door:?}" + ); + } + assert!(matches!( + open(Door::Gemini, &locked, 0, None, "g", "ref"), + Some(Session::Gemini(_)) + )); +} diff --git a/crates/busbar-plane-streaming/src/tests/twilio.rs b/crates/busbar-plane-streaming/src/tests/twilio.rs deleted file mode 100644 index b42050ecee..0000000000 --- a/crates/busbar-plane-streaming/src/tests/twilio.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Two defects in this crate's Twilio `start` handling: -//! -//! 1. **The negotiated media format was parsed but never validated.** `twilio::decode`'s `Start` -//! already read `encoding`/`sampleRate`/`channels` off the carrier's own frame, but nothing -//! compared them against the G.711 µ-law / 8 kHz / mono format every later `media` frame is -//! decoded and TIMED as (`ulaw::decode_frame`, `AudioFormat::G711Ulaw::bytes_to_ms`). Duration is -//! a LEDGER quantity, so a wrong count from an unvalidated format is a wrong ledger, silently. -//! 2. **An empty `streamSid` passed the anti-forgery binding check vacuously.** `state -//! .twilio_stream_sid` could be bound to `Some("")`, and a `media` frame carrying an equally -//! empty `streamSid` compared equal to it — a binding that cannot bind, accepted anyway. -//! -//! The first battery drives [`twilio::decode`] directly — a pure function, no harness needed. The -//! second drives the real plane entrypoint ([`StreamingPlane::decode_ingress`]) so the fix is proven -//! wired in, not merely correct in isolation, and proves the EXISTING forged-source check (a real -//! sid mismatch is still discarded) was not weakened while the empty case was closed. - -use crate::claims::Dialect; -use crate::session::VoiceSessionState; -use crate::tests::harness::{ctx, frame, EmptyConfig, LeakPlaneAlloc, WsStack}; -use crate::twilio::{self, TwilioError, TwilioEvent}; -use crate::StreamingPlane; -use busbar_contract::bounded::Labels; -use busbar_contract::plane::{Ingress, Plane, PlaneSessionState}; -use busbar_contract::wire::{Decode, DiscardCode, FrameCursor}; - -/// A well-formed carrier `start` frame naming `stream_sid` and the negotiated format. -fn start_json(stream_sid: &str, encoding: &str, sample_rate: u64, channels: u64) -> String { - serde_json::json!({ - "event": "start", - "start": { - "streamSid": stream_sid, - "callSid": "CA-test-call", - "mediaFormat": { - "encoding": encoding, - "sampleRate": sample_rate, - "channels": channels, - }, - }, - }) - .to_string() -} - -/// A carrier `media` frame naming `stream_sid`, carrying one base64 byte of payload. -fn media_json(stream_sid: &str) -> String { - serde_json::json!({ - "event": "media", - "streamSid": stream_sid, - "media": { "payload": "AA==" }, - }) - .to_string() -} - -// ══ `twilio::decode`, DIRECTLY ══════════════════════════════════════════════════════════════════ - -#[test] -fn start_with_an_empty_stream_sid_is_refused() { - let json = start_json("", "audio/x-mulaw", 8_000, 1); - assert_eq!( - twilio::decode(json.as_bytes()), - Err(TwilioError::Malformed), - "an empty streamSid cannot bind anything and must not decode as though it could" - ); -} - -#[test] -fn start_negotiating_a_non_g711_format_is_refused() { - for (encoding, sample_rate, channels) in [ - ("audio/l16", 8_000u64, 1u64), - ("audio/x-mulaw", 16_000, 1), - ("audio/x-mulaw", 8_000, 2), - ("", 8_000, 1), - ] { - let json = start_json("CA-real-sid", encoding, sample_rate, channels); - assert_eq!( - twilio::decode(json.as_bytes()), - Err(TwilioError::UnsupportedMediaFormat), - "a negotiated format of {encoding:?}/{sample_rate}/{channels} must be refused rather \ - than silently treated as G.711/8kHz/mono" - ); - } -} - -#[test] -fn start_with_a_real_sid_and_the_assumed_format_decodes() { - let json = start_json("CA-real-sid", "audio/x-mulaw", 8_000, 1); - assert_eq!( - twilio::decode(json.as_bytes()), - Ok(TwilioEvent::Start { - stream_sid: "CA-real-sid".to_string(), - call_sid: "CA-test-call".to_string(), - encoding: "audio/x-mulaw".to_string(), - sample_rate: 8_000, - channels: 1, - }), - "a well-formed start naming the assumed format must still decode — the fix must not \ - over-refuse the ordinary case" - ); -} - -// ══ THE PLANE ENTRYPOINT ════════════════════════════════════════════════════════════════════════ - -/// A fresh Twilio-dialected session half — what a session's client half opens with once the -/// `/twilio/inbound` claim has matched, before any frame has arrived. -fn fresh_twilio_state() -> PlaneSessionState { - PlaneSessionState::new(VoiceSessionState::for_dialect(Dialect::TwilioMediaStreams)) -} - -/// What one call to [`StreamingPlane::decode_ingress`] made of a frame, OWNED rather than borrowed -/// — everything the arena/frame `decode_ingress` borrows from is local to this function, so nothing -/// past what a test needs to assert on may escape it. -#[derive(Debug, PartialEq, Eq)] -enum Outcome { - Discard(DiscardCode), - Open, - Other, -} - -/// Decode one Twilio wire frame against `state`, over a fresh arena/transport/labels local to this -/// call. `state` is the one thing carried BETWEEN calls — the session half a real connection would -/// hold across frames — exactly what the `streamSid` binding this module is testing depends on. -fn decode_one( - plane: &StreamingPlane, - state: &mut PlaneSessionState, - json: &str, -) -> Result { - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/twilio/inbound"); - let labels = Labels::new(); - let cx = ctx(&arena, &config, &transport, &labels); - let frames = [frame(json.as_bytes())]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane.decode_ingress(&mut cursor, Some(state), &cx)?; - Ok(match ingress { - Ingress::Discard { reason } => Outcome::Discard(reason), - Ingress::Open(_) => Outcome::Open, - _ => Outcome::Other, - }) -} - -#[test] -fn a_start_with_an_empty_stream_sid_refuses_the_whole_session_at_the_plane_entrypoint() { - let plane = StreamingPlane::EMPTY; - let mut state = fresh_twilio_state(); - let result = decode_one( - &plane, - &mut state, - &start_json("", "audio/x-mulaw", 8_000, 1), - ); - assert_eq!( - result, - Err(Decode::Malformed), - "an empty streamSid must refuse the session, not open a binding nothing can check against" - ); -} - -#[test] -fn a_start_negotiating_a_non_g711_format_refuses_the_whole_session_at_the_plane_entrypoint() { - let plane = StreamingPlane::EMPTY; - let mut state = fresh_twilio_state(); - let result = decode_one( - &plane, - &mut state, - &start_json("CA-real-sid", "audio/l16", 16_000, 2), - ); - assert_eq!( - result, - Err(Decode::UnsupportedOperation), - "a non-G.711/8kHz/mono negotiation must refuse the session rather than proceed with wrong \ - duration accounting" - ); -} - -#[test] -fn a_media_frame_naming_a_different_sid_is_still_discarded_after_the_empty_case_is_closed() { - let plane = StreamingPlane::EMPTY; - let mut state = fresh_twilio_state(); - - decode_one( - &plane, - &mut state, - &start_json("CA-real-sid", "audio/x-mulaw", 8_000, 1), - ) - .expect("a well-formed start must be accepted"); - - // A media frame naming a DIFFERENT, non-empty sid must still be refused as forged — proof the - // existing check was not weakened while the empty case was closed. - let forged = decode_one(&plane, &mut state, &media_json("CA-someone-elses-sid")) - .expect("a mismatched-sid media frame is discarded, not a decode error"); - assert_eq!( - forged, - Outcome::Discard(DiscardCode::ForgedSource), - "a media frame naming a sid other than the one `start` bound must still be discarded" - ); - - // A media frame naming the bound sid is accepted and opens the turn. - let matched = decode_one(&plane, &mut state, &media_json("CA-real-sid")) - .expect("a media frame carrying the bound sid must be accepted"); - assert_eq!( - matched, - Outcome::Open, - "a media frame carrying the bound sid must open the turn" - ); -} - -// ══ THE `media` PAYLOAD'S BASE64 — THE BILLED BYTE COUNT ═════════════════════════════════════════ -// -// Items 418/419. The byte count `twilio::decode` hands back for a `media` payload is the LEDGER -// quantity `crate::plane` times at 8 bytes/ms into `audio_ms_in`. The crate's private base64 copy -// used to strip `=` wherever it appeared, so `QQ==` repeated 2000 times decoded to 3000 bytes and -// was metered as 375 ms of caller audio, where the workspace's one decoder -// (`busbar_substrate_values::media::base64_decode`) refuses the same bytes: padding is terminal. - -/// A carrier `media` frame on `stream_sid` carrying `payload` verbatim as its base64. -fn media_json_with(stream_sid: &str, payload: &str) -> String { - serde_json::json!({ - "event": "media", - "streamSid": stream_sid, - "media": { "payload": payload }, - }) - .to_string() -} - -/// The bytes `twilio::decode` read out of a `media` frame carrying `payload`. -fn media_bytes(payload: &str) -> Result, TwilioError> { - match twilio::decode(media_json_with("CA-real-sid", payload).as_bytes())? { - TwilioEvent::Media { payload, .. } => Ok(payload.to_vec()), - other => panic!("a media frame decoded as {other:?}"), - } -} - -#[test] -fn a_media_payload_resuming_after_padding_is_refused_not_billed_as_a_longer_payload() { - assert_eq!(media_bytes("QQ==QQ=="), Err(TwilioError::BadPayload)); - assert_eq!(media_bytes("QQ=A"), Err(TwilioError::BadPayload)); - assert_eq!( - media_bytes(&"QQ==".repeat(2_000)), - Err(TwilioError::BadPayload) - ); -} - -#[test] -fn the_media_payload_decoder_keeps_the_rest_of_the_shared_decode_contract() { - // Padding present, absent, trailing whitespace or extra padding after it: all one byte. - assert_eq!(media_bytes("QQ=="), Ok(vec![b'A'])); - assert_eq!(media_bytes("QQ"), Ok(vec![b'A'])); - assert_eq!(media_bytes("QQ==\r\n"), Ok(vec![b'A'])); - assert_eq!(media_bytes("QQ==="), Ok(vec![b'A'])); - // Whitespace inside the data is ignored. - assert_eq!(media_bytes("QU\nJD"), Ok(b"ABC".to_vec())); - // A lone trailing sextet and a byte outside the alphabet refuse. - assert_eq!(media_bytes("QUJDR"), Err(TwilioError::BadPayload)); - assert_eq!(media_bytes("QU*D"), Err(TwilioError::BadPayload)); - // A real 20 ms µ-law frame (160 bytes, every value) survives the carrier's own encoder. - let mulaw: Vec = (0..160u8).collect(); - match twilio::decode(&twilio::encode_media("CA-real-sid", &mulaw)) { - Ok(TwilioEvent::Media { payload, .. }) => assert_eq!(payload.to_vec(), mulaw), - other => panic!("a round-tripped media frame decoded as {other:?}"), - } -} - -#[test] -fn a_padding_resumed_media_frame_is_refused_at_the_plane_entrypoint_and_opens_no_turn() { - let plane = StreamingPlane::EMPTY; - let mut state = fresh_twilio_state(); - decode_one( - &plane, - &mut state, - &start_json("CA-real-sid", "audio/x-mulaw", 8_000, 1), - ) - .expect("a well-formed start must be accepted"); - let result = decode_one( - &plane, - &mut state, - &media_json_with("CA-real-sid", &"QQ==".repeat(2_000)), - ); - assert!( - result.is_err(), - "a media payload resuming after its padding must be refused, not metered: got {result:?}" - ); -} - -/// Item 419: the comment justifying the local base64 copy once told a reviewer it was the SAME -/// algorithm `crate::codec::topology::twilio` carries — that module carries none (it imports -/// the substrate decoder), and the two differed on padding, which is how the billed-byte drift above -/// survived review. The justification must name the real single source and must not claim an -/// equivalence nobody checked; the decode-contract tests above are what check it. -#[test] -fn the_local_base64_copy_names_the_one_decoder_it_must_match_and_claims_no_unchecked_equivalence() { - let src = include_str!("../twilio.rs"); - assert!( - !src.contains("identical standard algorithm"), - "twilio.rs still claims an unchecked algorithm equivalence for its private base64" - ); - assert!( - // Spelled in two halves so this line does not itself name the substrate crate the plane's - // purity test (`tests/purity.rs`) forbids in non-comment source. - src.contains(concat!("busbar_sub", "strate_values::media")), - "twilio.rs's base64 comment must name the workspace's one decoder it stands in for" - ); -} diff --git a/crates/busbar-plane-streaming/src/tests/ulaw.rs b/crates/busbar-plane-streaming/src/tests/ulaw.rs deleted file mode 100644 index 0b595b5265..0000000000 --- a/crates/busbar-plane-streaming/src/tests/ulaw.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! µ-law ↔ PCM16 tests against known reference values, not just round-trip fuzz. - -use crate::ulaw::{decode_frame, encode_frame, pcm16_to_ulaw_byte, ulaw_byte_to_pcm16}; - -#[test] -fn positive_zero_ulaw_byte_decodes_to_pcm_zero() { - // The ITU-T G.711 reference: 0xFF is µ-law's "positive zero" code. - assert_eq!(ulaw_byte_to_pcm16(0xFF), 0); -} - -#[test] -fn pcm_zero_encodes_to_the_positive_zero_ulaw_byte() { - assert_eq!(pcm16_to_ulaw_byte(0), 0xFF); -} - -#[test] -fn ulaw_zero_byte_decodes_to_a_large_negative_sample() { - // 0x00 is µ-law's most-negative code; the ITU-T reference table places it at -32124. - assert_eq!(ulaw_byte_to_pcm16(0x00), -32124); -} - -#[test] -fn the_most_negative_decoded_sample_encodes_back_to_the_ulaw_zero_byte() { - assert_eq!(pcm16_to_ulaw_byte(-32124), 0x00); -} - -#[test] -fn every_ulaw_byte_round_trips_within_the_algorithm_s_own_quantization() { - // µ-law is lossy by construction (14-bit magnitude compressed to a 4-bit mantissa within a - // segment), so decode→encode does not reproduce every byte exactly; it reproduces byte VALUES - // that are already legal µ-law codes for a segment's canonical representative. What must hold - // for every one of the 256 possible bytes is that decoding it and then re-encoding the result - // lands on a byte that decodes to the SAME sample the first decode produced — the round trip - // is stable at the PCM value, which is the property the plane's transcode boundary depends on. - for byte in 0u8..=255 { - let sample = ulaw_byte_to_pcm16(byte); - let re_encoded = pcm16_to_ulaw_byte(sample); - let re_decoded = ulaw_byte_to_pcm16(re_encoded); - assert_eq!( - sample, re_decoded, - "byte {byte:#04x} decoded to {sample}, round-tripped to a byte decoding to {re_decoded}" - ); - } -} - -#[test] -fn frame_helpers_agree_with_the_per_sample_functions() { - let ulaw = [0xFFu8, 0x00, 0x7F, 0x80]; - let pcm = decode_frame(&ulaw); - assert_eq!(pcm.len(), ulaw.len() * 2); - for (i, &byte) in ulaw.iter().enumerate() { - let sample = i16::from_le_bytes([pcm[i * 2], pcm[i * 2 + 1]]); - assert_eq!(sample, ulaw_byte_to_pcm16(byte)); - } - let back = encode_frame(&pcm); - assert_eq!(back.len(), ulaw.len()); -} - -#[test] -fn an_odd_trailing_byte_is_dropped_rather_than_guessed_at() { - let pcm = [0u8, 0, 1]; // one whole sample plus one stray byte - assert_eq!(encode_frame(&pcm).len(), 1); -} diff --git a/crates/busbar-plane-streaming/src/tools.rs b/crates/busbar-plane-streaming/src/tools.rs deleted file mode 100644 index 9fbc726bc0..0000000000 --- a/crates/busbar-plane-streaming/src/tools.rs +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SERVER-SIDE TOOL EXECUTOR PORT (the tool-moat section of `BUSBAR-1.6.0.md` #18/#45). -//! -//! The whole reason a governed plane beats a dumb WS pipe: tool calls execute SERVER-SIDE, under -//! governance, and the browser is never trusted to author them. The runtime correlates a call by its -//! [`crate::codec::ir::tool::CallRef`], accumulates the streamed argument bytes, and on close hands the -//! `(name, arguments)` to this port for execution — never to the client. The port is plane-local and -//! dependency-inverted so the composition root binds the real tool registry while tests bind a fake. - -use async_trait::async_trait; - -/// EXECUTES ONE correlated tool call server-side and returns its opaque output payload — the bytes the -/// runtime frames back upstream as a `function_call_output`. `Send + Sync` so it is shared across the -/// concurrent per-frame handlers. -#[async_trait] -pub trait ToolExecutor: Send + Sync { - /// Whether THIS NODE is the one that answers `name`. - /// - /// The two halves of the tool loop divide here. `true` — the default, and every executor's - /// answer until one says otherwise — is the moat above: the runtime accumulates the arguments and - /// calls [`Self::execute`], and the client never authors the result. `false` says the answer can - /// only come from the client, which makes the call's reply leg a governed WAIT held by the node's - /// own table ([`crate::governed::GovernedCalls`]) rather than an execution held here. - /// - /// Defaulted to `true` deliberately: an executor that has not thought about the question serves - /// what it is asked, which is the safe end. The unsafe end would be a node that quietly stopped - /// running its own tools and waited on a client that was never going to answer. - fn executes_here(&self, _name: &str) -> bool { - true - } - - /// Run the tool named `name` with the accumulated `arguments` (opaque JSON bytes) and return the - /// opaque result payload. An executor that does not recognize `name` returns an error-shaped - /// payload rather than panicking — the session survives one bad tool call. - async fn execute(&self, name: &str, arguments: &[u8]) -> Vec; -} - -/// A TEST/DEV executor that echoes the call back as a JSON object — enough to prove correlation -/// (the right `name` + `arguments` reached the right call) without a real tool registry. -#[derive(Debug, Default, Clone, Copy)] -pub struct EchoToolExecutor; - -#[async_trait] -impl ToolExecutor for EchoToolExecutor { - async fn execute(&self, name: &str, arguments: &[u8]) -> Vec { - let args = String::from_utf8_lossy(arguments); - format!(r#"{{"tool":"{name}","echo":{args}}}"#).into_bytes() - } -} diff --git a/crates/busbar-plane-streaming/src/twilio.rs b/crates/busbar-plane-streaming/src/twilio.rs deleted file mode 100644 index 64ddc113ae..0000000000 --- a/crates/busbar-plane-streaming/src/twilio.rs +++ /dev/null @@ -1,357 +0,0 @@ -//! A minimal Twilio Media Streams wire reader/writer, written independently for this crate. -//! -//! Twilio's Media Streams protocol is a JSON-framed message-per-WS-frame wire: lifecycle events -//! (`connected`, `start`, `mark`, `dtmf`, `stop`) and a per-chunk `media` event whose `payload` is -//! base64 8 kHz G.711 µ-law audio — `{"event":"media","media":{"payload":""},"streamSid":"..."}`. -//! -//! No duplex dialect codec covers this wire (it is not one of the plane's two duplex dialects). -//! The one Twilio-shaped module in this crate's `codec` module (`crate::codec::topology::twilio`) -//! is the legacy engine's, and this module does not use it: it is written from the wire shape alone -//! (confirmed against `docs/design/BUSBAR-1.6.0.md` #18/#45 and the public Twilio Media -//! Streams reference, both cited in this crate's design notes) rather than adapted from that -//! runtime-gated module; any structural resemblance is the two independently converging on the same -//! public wire format, not a copy. -//! -//! The events this module models onto the shared [`crate::codec::ir`] vocabulary: a `media` event -//! becomes an [`crate::codec::ir::media::IrAudioFrame`] (direction `Up`, format -//! [`crate::codec::ir::media::AudioFormat::G711Ulaw`]) carrying the base64-decoded µ-law bytes -//! verbatim — the µ-law↔PCM16 transform happens at the plane's `encode_ingress_frame` seam -//! ([`crate::plane`]), never here. The lifecycle events carry no audio and are surfaced as their own -//! variant so the plane can track (or ignore) them without guessing at a synthetic IR event for a -//! wire message with no IR home. - -use bytes::Bytes; - -/// One decoded Twilio Media Streams event. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TwilioEvent { - /// The one-time handshake event, first on the socket. Carries nothing. - Connected, - /// The stream opened. Carries the identifiers later frames repeat and the negotiated format. - Start { - /// The `streamSid` Twilio minted for this connection, repeated on every later frame. - stream_sid: String, - /// Twilio's `callSid` for the underlying PSTN call. - call_sid: String, - /// The negotiated media encoding string (`audio/x-mulaw` for the passthrough carrier). - encoding: String, - /// The negotiated sample rate in Hz (`8000` for the passthrough carrier). - sample_rate: u64, - /// The negotiated channel count (`1`, mono). - channels: u64, - }, - /// A ~20 ms audio chunk. `payload` is the base64-decoded, still µ-law-encoded audio. - Media { - /// The connection's `streamSid`, to check against the value bound at admission. - stream_sid: String, - /// The raw µ-law bytes. - payload: Bytes, - }, - /// A playback-position acknowledgement Twilio echoes back for a `mark` this plane sent. - Mark { - /// The connection's `streamSid`. - stream_sid: String, - /// The mark name the outbound side chose. - name: String, - }, - /// A touch-tone keypress heard on the inbound track — sent only when the stream has DTMF - /// enabled. It carries no session audio and is discarded exactly like [`Self::Mark`]. - Dtmf { - /// The connection's `streamSid`. - stream_sid: String, - /// The key that was pressed (`0`-`9`, `*`, `#`, `A`-`D`), or empty when Twilio omits it. - digit: String, - }, - /// The terminal event. - Stop, -} - -/// THE ASSUMED NEGOTIATED FORMAT — the one this reader hands `media` payloads on as (raw µ-law -/// bytes, see the module doc) and the one every later consumer treats them as: the µ-law→PCM16 -/// widen ([`crate::ulaw::decode_frame`]) and the duration [`crate::plane`] derives from a byte -/// count (`AudioFormat::G711Ulaw::bytes_to_ms`, which IS a LEDGER quantity — a call's billed -/// duration rolls up from exactly this count). Twilio's own default and near-universal choice for -/// this carrier, and the only shape the rest of this crate's Twilio path is written against. -const ASSUMED_ENCODING: &str = "audio/x-mulaw"; -const ASSUMED_SAMPLE_RATE_HZ: u64 = 8_000; -const ASSUMED_CHANNELS: u64 = 1; - -/// Why an inbound Twilio frame could not be read. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TwilioError { - /// The frame was not well-formed JSON, or a field this reader requires was absent. - Malformed, - /// The frame named an `event` this reader does not model. - UnknownEvent(String), - /// A `media` payload was not valid base64. - BadPayload, - /// `start` negotiated a `mediaFormat` other than [`ASSUMED_ENCODING`] / - /// [`ASSUMED_SAMPLE_RATE_HZ`] / [`ASSUMED_CHANNELS`] — the format every later frame on this - /// stream would otherwise be decoded and TIMED under. Refused rather than served on a wrong - /// assumption: a duration derived from a mismatched format is a wrong ledger entry, silently, - /// since nothing about a mis-decoded byte count looks broken on its own. - UnsupportedMediaFormat, -} - -/// Decode one inbound Twilio Media Streams WS frame. -/// -/// # Errors -/// Returns [`TwilioError`] when the frame is not well-formed Twilio JSON, names an event this -/// reader does not model, carries a `media` payload that is not valid base64, binds an empty or -/// absent `streamSid` (which cannot BIND anything — see [`TwilioEvent::Start`]), or negotiates a -/// media format other than the one this crate's Twilio path assumes throughout. -pub fn decode(frame: &[u8]) -> Result { - let v: serde_json::Value = serde_json::from_slice(frame).map_err(|_| TwilioError::Malformed)?; - let event = v - .get("event") - .and_then(serde_json::Value::as_str) - .ok_or(TwilioError::Malformed)?; - match event { - "connected" => Ok(TwilioEvent::Connected), - "start" => { - let start = v.get("start").ok_or(TwilioError::Malformed)?; - let stream_sid = str_field(start, "streamSid") - .or_else(|| str_field(&v, "streamSid")) - .ok_or(TwilioError::Malformed)?; - // AN EMPTY ID IS NOT A BINDING. `stream_sid` is what every later `media` frame is - // checked against (`crate::plane`'s per-connection anti-forgery guard). If this reader - // ever handed one back empty, an attacker's own `media` frame carrying an equally empty - // (or simply omitted — the same empty default) `streamSid` would compare EQUAL to the - // "bound" value and the check that exists to refuse an unbound source would pass - // vacuously. Refused HERE, at the one place that can still say why, rather than let a - // binding that cannot bind reach `decode_twilio_frame`. - if stream_sid.is_empty() { - return Err(TwilioError::Malformed); - } - let call_sid = str_field(start, "callSid").unwrap_or_default(); - let mf = start.get("mediaFormat").ok_or(TwilioError::Malformed)?; - let encoding = str_field(mf, "encoding").unwrap_or_default(); - let sample_rate = mf - .get("sampleRate") - .and_then(serde_json::Value::as_u64) - .unwrap_or_default(); - let channels = mf - .get("channels") - .and_then(serde_json::Value::as_u64) - .unwrap_or_default(); - if encoding != ASSUMED_ENCODING - || sample_rate != ASSUMED_SAMPLE_RATE_HZ - || channels != ASSUMED_CHANNELS - { - return Err(TwilioError::UnsupportedMediaFormat); - } - Ok(TwilioEvent::Start { - stream_sid, - call_sid, - encoding, - sample_rate, - channels, - }) - } - "media" => { - let stream_sid = str_field(&v, "streamSid").unwrap_or_default(); - let payload_b64 = v - .get("media") - .and_then(|m| m.get("payload")) - .and_then(serde_json::Value::as_str) - .ok_or(TwilioError::Malformed)?; - let payload = base64_decode(payload_b64).ok_or(TwilioError::BadPayload)?; - Ok(TwilioEvent::Media { - stream_sid, - payload: Bytes::from(payload), - }) - } - "mark" => Ok(TwilioEvent::Mark { - stream_sid: str_field(&v, "streamSid").unwrap_or_default(), - name: v - .get("mark") - .and_then(|m| m.get("name")) - .and_then(serde_json::Value::as_str) - .unwrap_or_default() - .to_string(), - }), - "dtmf" => Ok(TwilioEvent::Dtmf { - stream_sid: str_field(&v, "streamSid").unwrap_or_default(), - digit: v - .get("dtmf") - .and_then(|d| d.get("digit")) - .and_then(serde_json::Value::as_str) - .unwrap_or_default() - .to_string(), - }), - "stop" => Ok(TwilioEvent::Stop), - other => Err(TwilioError::UnknownEvent(other.to_string())), - } -} - -fn str_field(v: &serde_json::Value, key: &str) -> Option { - v.get(key) - .and_then(serde_json::Value::as_str) - .map(str::to_string) -} - -/// Encode raw µ-law bytes into a Twilio outbound `media` envelope for a given `stream_sid`. -#[must_use] -pub fn encode_media(stream_sid: &str, mulaw: &[u8]) -> Vec { - let mut out = Vec::new(); - encode_media_into(&mut out, stream_sid, mulaw); - out -} - -/// The same envelope, rendered into a buffer the caller already has. -/// -/// This is the shape the downlink writes in. The envelope is FIXED — three members, in the one -/// order this dialect's documents are written in — and everything about it except the identifier -/// and the payload was known when the crate was compiled. Building a document to describe it, and -/// a string to hold the payload, and then serializing the document, spends three allocations per -/// AUDIO FRAME to reach bytes that could have been appended. A call carries fifty frames a second. -/// -/// The buffer is CLEARED, not appended to, so a caller may hand the same one back frame after frame -/// and pay for its growth once. -pub fn encode_media_into(out: &mut Vec, stream_sid: &str, mulaw: &[u8]) { - out.clear(); - // An identifier that would have to be escaped is not one this dialect mints, but it is one this - // function may be handed. Rather than carry an escaper that would almost never run — and would - // be the one part of this rendering nothing exercises — the rare case goes back through the - // serializer, which is the authority on what those bytes are. - if !is_bare_json_string(stream_sid) { - let doc = serde_json::json!({ - "event": "media", - "streamSid": stream_sid, - "media": { "payload": base64_encode(mulaw) }, - }); - out.extend_from_slice(&serde_json::to_vec(&doc).unwrap_or_default()); - return; - } - const HEAD: &[u8] = br#"{"event":"media","media":{"payload":""#; - const MIDDLE: &[u8] = br#""},"streamSid":""#; - const TAIL: &[u8] = br#""}"#; - out.reserve( - HEAD.len() + base64_len(mulaw.len()) + MIDDLE.len() + stream_sid.len() + TAIL.len(), - ); - out.extend_from_slice(HEAD); - base64_encode_into(mulaw, out); - out.extend_from_slice(MIDDLE); - out.extend_from_slice(stream_sid.as_bytes()); - out.extend_from_slice(TAIL); -} - -/// Whether a string is its own JSON body — nothing in it a serializer would rewrite. -/// -/// Printable ASCII with neither of the two characters a JSON string cannot carry raw. Everything -/// else, including every non-ASCII byte, is left to the serializer. -fn is_bare_json_string(s: &str) -> bool { - s.bytes() - .all(|b| (0x20..0x7f).contains(&b) && b != b'"' && b != b'\\') -} - -/// Encode a named playback-position mark for a given `stream_sid`. -/// -/// Used as the provisional wire rendering for a server event this dialect's own vocabulary has no -/// audio-bearing counterpart for (see `crate::plane`'s `encode_response` documentation) — an -/// acknowledged no-op frame rather than silently dropping the event. -#[must_use] -pub fn encode_mark(stream_sid: &str, name: &str) -> Vec { - let out = serde_json::json!({ - "event": "mark", - "streamSid": stream_sid, - "mark": { "name": name }, - }); - serde_json::to_vec(&out).unwrap_or_default() -} - -// ── standard base64 (RFC 4648), a LOCAL copy of the workspace's one decoder contract ───────────── -// -// The workspace's one base64 implementation is `busbar_substrate_values::media`; every other reader -// (`crate::codec::topology::twilio` and `ir::codec` among them) imports it and carries no -// algorithm of its own. It is in this crate's closure (through `busbar-voice-codec`), but a plane -// names no substrate crate (`tests/purity.rs`, `the_plane_names_no_kernel_side_crate`), so this copy -// stands in for it. It is NOT a second algorithm: its decode contract must match that one byte for -// byte, because the byte count it returns is billed — `crate::plane` derives a call's audio duration -// from it. The contract, all of it: -// whitespace is ignored; `=` padding is TERMINAL (only whitespace or more `=` may follow it — data -// resuming after padding, `QQ==QQ==`, is malformed, never one longer payload); a lone trailing -// sextet, or any byte outside the alphabet, refuses the payload. `tests::twilio` pins each clause. - -const B64_ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -/// How many characters this many bytes encode to. -const fn base64_len(bytes: usize) -> usize { - bytes.div_ceil(3) * 4 -} - -fn base64_encode(data: &[u8]) -> String { - let mut out = Vec::with_capacity(base64_len(data.len())); - base64_encode_into(data, &mut out); - // Every byte appended below is one of the alphabet's, `=` included, so this is ASCII. - String::from_utf8(out).unwrap_or_default() -} - -/// The same encoding, appended to a buffer the caller already has. -fn base64_encode_into(data: &[u8], out: &mut Vec) { - for chunk in data.chunks(3) { - let b0 = u32::from(chunk[0]); - let b1 = u32::from(*chunk.get(1).unwrap_or(&0)); - let b2 = u32::from(*chunk.get(2).unwrap_or(&0)); - let n = (b0 << 16) | (b1 << 8) | b2; - out.push(B64_ALPHABET[((n >> 18) & 63) as usize]); - out.push(B64_ALPHABET[((n >> 12) & 63) as usize]); - out.push(if chunk.len() > 1 { - B64_ALPHABET[((n >> 6) & 63) as usize] - } else { - b'=' - }); - out.push(if chunk.len() > 2 { - B64_ALPHABET[(n & 63) as usize] - } else { - b'=' - }); - } -} - -fn base64_sextet(c: u8) -> Option { - match c { - b'A'..=b'Z' => Some(u32::from(c - b'A')), - b'a'..=b'z' => Some(u32::from(c - b'a') + 26), - b'0'..=b'9' => Some(u32::from(c - b'0') + 52), - b'+' => Some(62), - b'/' => Some(63), - _ => None, - } -} - -fn base64_decode(s: &str) -> Option> { - let mut sextets: Vec = Vec::with_capacity(s.len()); - // Padding is TERMINAL: once `=` is seen, encoded data resuming is a malformed payload, refused. - let mut padded = false; - for b in s.bytes() { - if b.is_ascii_whitespace() { - continue; - } - if b == b'=' { - padded = true; - continue; - } - if padded { - return None; - } - sextets.push(b); - } - let mut out = Vec::with_capacity(sextets.len() / 4 * 3); - for chunk in sextets.chunks(4) { - if chunk.len() < 2 { - return None; - } - let mut n = 0u32; - for &c in chunk { - n = (n << 6) | base64_sextet(c)?; - } - let missing = 4 - chunk.len(); - n <<= 6 * missing as u32; - let be = n.to_be_bytes(); - for byte in be.iter().take(1 + (chunk.len() - 1)).skip(1) { - out.push(*byte); - } - } - Some(out) -} diff --git a/crates/busbar-plane-streaming/src/ulaw.rs b/crates/busbar-plane-streaming/src/ulaw.rs deleted file mode 100644 index 09bf391bbc..0000000000 --- a/crates/busbar-plane-streaming/src/ulaw.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! G.711 µ-law ↔ PCM16 — implemented from the ITU-T G.711 bias/segment definition, by this crate. -//! -//! `busbar-voice` does not implement this transform: its own `AudioFormat` enum carries only the -//! byte-rate bookkeeping (`bytes_per_ms`) the barge-in truncate arithmetic needs, and its doc -//! comments name the actual sample transcode an unimplemented "seam...armed only when a lane -//! declares it" (`crate::codec::ir::media`). The `twilio-media-streams` dialect is that lane: Twilio -//! carries 8 kHz G.711 µ-law, and every upstream this plane can dial (OpenAI Realtime, Gemini Live) -//! speaks PCM16. This module is the transform, written independently against the standard, with its -//! own unit tests against known reference values. -//! -//! ## The algorithm -//! -//! µ-law encodes a 14-bit-magnitude linear sample into an 8-bit sign/exponent/mantissa byte: one -//! sign bit, a 3-bit exponent selecting one of eight logarithmic segments, and a 4-bit mantissa -//! giving the position within the segment. The constants below (`BIAS = 0x84`, `CLIP = 8159`) and -//! the segment table are the values the ITU-T G.711 reference algorithm defines; a byte is always -//! bitwise-inverted on the wire (`!ulawbyte`), which is why both directions below start by -//! inverting. -//! -//! Decode uses the closed 8-entry segment-base table directly. Encode searches for the same segment -//! by shifting a probe mask down from the top bit the 14-bit magnitude range can set; this is the -//! same search the reference algorithm performs, written as a loop instead of a literal 256-entry -//! lookup table so the two directions can be read and checked against each other by eye. - -/// The bias added to a linear magnitude before segment/mantissa extraction, and subtracted back out -/// on decode. The ITU-T G.711 reference value. -const BIAS: i32 = 0x84; - -/// The largest linear magnitude (after removing the sign) a sample is clamped to before encoding. -/// The ITU-T G.711 reference value; every larger magnitude quantizes to this segment's top code. -const CLIP: i32 = 32635; - -/// The eight segment base offsets a decoded exponent selects among, from the ITU-T G.711 reference -/// table. `PCM = SEGMENT_BASE[exponent] + (mantissa << (exponent + 3))`, minus the bias, sign-applied. -const SEGMENT_BASE: [i32; 8] = [0, 132, 396, 924, 1980, 4092, 8316, 16764]; - -/// Decode one µ-law byte to a 16-bit signed linear PCM sample. -/// -/// Reference vector: `0xFF` (µ-law "positive zero") decodes to `0`. -#[must_use] -pub fn ulaw_byte_to_pcm16(ulaw_byte: u8) -> i16 { - let inverted = !ulaw_byte; - let sign_bit = inverted & 0x80; - let exponent = usize::from((inverted >> 4) & 0x07); - let mantissa = i32::from(inverted & 0x0F); - let magnitude = SEGMENT_BASE[exponent] + (mantissa << (exponent + 3)); - let signed = if sign_bit != 0 { -magnitude } else { magnitude }; - signed.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16 -} - -/// Encode one 16-bit signed linear PCM sample to a µ-law byte. -/// -/// Reference vector: `0` encodes to `0xFF`, the inverse of [`ulaw_byte_to_pcm16`]'s `0xFF → 0`. -#[must_use] -pub fn pcm16_to_ulaw_byte(sample: i16) -> u8 { - let sign: u8 = if sample < 0 { 0x80 } else { 0x00 }; - let mut magnitude = i32::from(sample).abs(); - if magnitude > CLIP { - magnitude = CLIP; - } - magnitude += BIAS; - - // Find the segment: the largest exponent whose bit (starting at bit 14, the top bit a - // biased+clipped magnitude can set) is present in the magnitude. - let mut exponent: i32 = 7; - let mut probe: i32 = 0x4000; - while exponent > 0 && (magnitude & probe) == 0 { - exponent -= 1; - probe >>= 1; - } - let mantissa = (magnitude >> (exponent + 3)) & 0x0F; - let byte = sign | ((exponent as u8) << 4) | (mantissa as u8); - !byte -} - -/// Decode a whole frame of µ-law bytes into little-endian PCM16 bytes (two bytes per input byte). -#[must_use] -pub fn decode_frame(ulaw: &[u8]) -> Vec { - let mut out = Vec::with_capacity(ulaw.len() * 2); - for &b in ulaw { - out.extend_from_slice(&ulaw_byte_to_pcm16(b).to_le_bytes()); - } - out -} - -/// Encode a whole frame of little-endian PCM16 bytes into µ-law bytes (one byte per input sample). -/// -/// A trailing odd byte (an incomplete final sample) is dropped rather than guessed at. -#[must_use] -pub fn encode_frame(pcm16_le: &[u8]) -> Vec { - let mut out = Vec::with_capacity(pcm16_le.len() / 2); - for chunk in pcm16_le.as_chunks::<2>().0 { - let sample = i16::from_le_bytes(*chunk); - out.push(pcm16_to_ulaw_byte(sample)); - } - out -} diff --git a/crates/busbar-plane-streaming/tests/alloc_gate.rs b/crates/busbar-plane-streaming/tests/alloc_gate.rs index 314c3f9b81..63fcf78d6a 100644 --- a/crates/busbar-plane-streaming/tests/alloc_gate.rs +++ b/crates/busbar-plane-streaming/tests/alloc_gate.rs @@ -12,7 +12,7 @@ //! allowed to fall on the far side of a byte-for-byte comparison against the serializer that used //! to produce it. -use busbar_plane_streaming::twilio; +use busbar_plane_streaming::codec::topology::twilio::TwilioEnvelope; use std::alloc::{GlobalAlloc, Layout, System}; use std::cell::Cell; @@ -61,16 +61,28 @@ fn frame_bytes() -> Vec { /// The bytes the serializer produced, restated as the reference the renderer is held to. /// -/// The member ORDER is the serializer's, not a preference: this document's members are written in -/// the order a JSON object's keys sort in, because that is the order the old renderer emitted and -/// every recorded conformance answer was taken against it. +/// The member ORDER is the envelope's own (`event`, `streamSid`, `media`), the order Twilio's Media +/// Streams documents write it in and the codec's serializer emits. fn reference(stream_sid: &str, mulaw: &[u8]) -> Vec { - let doc = serde_json::json!({ - "event": "media", - "streamSid": stream_sid, - "media": { "payload": base64(mulaw) }, - }); - serde_json::to_vec(&doc).expect("the document serializes") + #[derive(serde::Serialize)] + struct Payload { + payload: String, + } + #[derive(serde::Serialize)] + struct Media<'a> { + event: &'static str, + #[serde(rename = "streamSid")] + stream_sid: &'a str, + media: Payload, + } + serde_json::to_vec(&Media { + event: "media", + stream_sid, + media: Payload { + payload: base64(mulaw), + }, + }) + .expect("the document serializes") } /// Standard base64, spelled out here so the reference owes the renderer nothing. @@ -113,7 +125,7 @@ fn the_rendered_envelope_is_the_one_the_serializer_produced() { for sid in [SID, "", "a\"b\\c", "sid with spaces", "sid-ünïcode"] { for payload in &payloads { let mut out = Vec::new(); - twilio::encode_media_into(&mut out, sid, payload); + TwilioEnvelope::encode_media_into(&mut out, sid, payload); assert_eq!( out, reference(sid, payload), @@ -121,7 +133,7 @@ fn the_rendered_envelope_is_the_one_the_serializer_produced() { payload.len() ); assert_eq!( - twilio::encode_media(sid, payload), + TwilioEnvelope::encode_media(sid, payload), out, "the owning form and the rendering form disagree for {sid:?}" ); @@ -135,12 +147,8 @@ fn the_rendered_envelope_is_the_one_the_serializer_produced() { /// Zero. The envelope is fixed, the buffer is the caller's, and the only two things that vary from /// frame to frame — the payload and the identifier — are appended, not built. /// -/// A buffer that has already carried a frame is not an assumption this number makes about a caller -/// it cannot see: the one caller in this crate — the downlink renderer in `plane.rs` — holds its -/// buffer on the session (`VoiceSessionState::render_buf`) and hands the same one back frame after -/// frame. A caller that built a fresh vector per frame would pay an allocation per frame no matter -/// what this renderer costs, which is what made the earlier form of this number true of the -/// function and false of the path it was quoted for. +/// A buffer that has already carried a frame is the caller's to hold: a caller that built a fresh +/// vector per frame would pay an allocation per frame no matter what this renderer costs. const RENDER_ALLOCS: u64 = 0; #[test] @@ -150,11 +158,11 @@ fn rendering_a_downlink_frame_into_a_held_buffer_allocates_nothing() { // One warm call outside the window: the buffer takes its capacity here, which is the whole // point of handing the same one back frame after frame. - twilio::encode_media_into(&mut out, SID, &mulaw); + TwilioEnvelope::encode_media_into(&mut out, SID, &mulaw); let expected = out.clone(); let count = allocations_of(|| { - twilio::encode_media_into(&mut out, SID, &mulaw); + TwilioEnvelope::encode_media_into(&mut out, SID, &mulaw); }); // The serializer route, measured rather than remembered. `reference` is the renderer this diff --git a/crates/busbar-plane-streaming/tests/conformance.rs b/crates/busbar-plane-streaming/tests/conformance.rs index b2913afdda..4446405229 100644 --- a/crates/busbar-plane-streaming/tests/conformance.rs +++ b/crates/busbar-plane-streaming/tests/conformance.rs @@ -1,343 +1,1006 @@ -//! The streaming-plane conformance rig: one live realtime-voice session, end to end. -//! -//! This drives [`busbar_plane_streaming::StreamingPlane`] — the STREAMING plane, registered under its -//! own key — through the whole shape a governed streaming turn takes: RESERVE (a session opens, a turn -//! is drafted, the plane names what the admit step reserves a hold against), TURNS (audio relays under -//! the open turn's hold; a barge-in supersedes it; a provider tool call opens a sub-unit), and SETTLE -//! (the turn's `response.done` carries usage, and the plane's meter names every per-turn class and the -//! quantity the money-book settles END-OF-TURN against). -//! -//! The dialect exercised is `openai-realtime` — the live realtime VOICE dialect, which is ONE dialect of -//! the streaming plane, not the plane's name. The wire fixtures are the same ones the underlying voice -//! codec proves; the point of this rig is that the STREAMING plane attributes and meters them, so a -//! deployment that registers `"streaming"` gets a byte-identical duplex session to the one the voice -//! adapter proves — the reframe changes the plane, not the bytes. - -mod harness; - -use busbar_contract::bounded::{FactValue, Facts, Labels}; -use busbar_contract::ids::{LaneId, OpClassId}; -use busbar_contract::plane::{Ingress, Plane, PlaneMeta, Progress, SessionPlane}; -use busbar_contract::wire::FrameCursor; -use serde_json::json; - -use busbar_plane_streaming::{Dialect, StreamingPlane, Upstream}; -use harness::{ctx, destination, frame, EmptyConfig, LeakPlaneAlloc, WsStack}; - -/// A streaming plane configured with one openai-realtime (voice-dialect) upstream. -fn streaming_plane() -> StreamingPlane { - static UPSTREAMS: &[Upstream] = &[Upstream { - lane: LaneId::new("realtime"), - host: "api.openai.com", - dialect: Dialect::OpenaiRealtime, - }]; - StreamingPlane::new(UPSTREAMS) -} - -fn session_update_fixture() -> Vec { - serde_json::to_vec(&json!({ - "type": "session.update", - "session": { - "modalities": ["audio", "text"], - "instructions": "You are a helpful voice agent.", - "voice": "marin", - "input_audio_format": "pcm16", - "output_audio_format": "g711_ulaw", - "turn_detection": { - "type": "server_vad", - "threshold": 0.5, - "prefix_padding_ms": 300, - "silence_duration_ms": 200, - "create_response": true, - "interrupt_response": true - }, - "tools": [{ "type": "function", "name": "lookup", "parameters": { "type": "object" } }], - "tool_choice": "auto", - "max_output_tokens": 4096 - } - })) - .expect("fixture serializes") +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE STREAMING PLANE'S DOOR, BOTH WAYS (#2 (4)/(5): the plugin's own both-ways witness, over the +//! real loader): the same door compiled in (`busbar_plane_streaming::door::door`, through +//! `load_linked`) and dropped in (this crate's `streaming_door` example `cdylib`, which `cargo test` +//! builds, through `load_dropped`), driven by ONE script over the plane kind. Every answer is judged +//! by the kind's own checks on the way; the two transcripts must be identical, and each is pinned. + +use std::mem::zeroed; +use std::sync::Arc; + +use busbar_contract::abi::mechanism::call::{ + AbiStr, Blob, Field, Outcome, Span, BLOB_JSON, BLOB_OCTETS, +}; +use busbar_contract::abi::mechanism::lifecycle::{ + slot as life, GenIn, RefreshIn, TickIn, TickOut, ValidateIn, +}; +use busbar_contract::abi::plane::{ + slot, ArriveIn, ArriveOut, OnPieceIn, OnPieceOut, OutField, PlaneOpenIn, PlaneOpenOut, + PlaneRefreshOut, PlaneSnapshot, UnitCount, CANCEL_FAILED, CLAIM_EXACT, CLAIM_OPEN, EMIT_DONE, + EMIT_TO_FAR_END, FROM_CALLER, FROM_FAR_END, FROM_KERNEL, PIECE_HAS_STATUS, PIECE_LAST, +}; +use busbar_plane_streaming::{door, driven}; +use busbar_plugin_loader::dispatch::kinds::plane::Plane; +use busbar_plugin_loader::dispatch::{ + in_head, load_dropped, load_linked, out_head, Bind, DispatchConfig, Dispatcher, Frame, + LinkedRow, NoSink, Plugin, +}; + +fn z() -> T { + // SAFETY: every `in`/`out` here is plain C data; all-zero is a valid value of each. + unsafe { zeroed() } } -/// THE WHOLE SHAPE, in one session: reserve, turns (relay + barge-in + tool call), settle. -#[test] -fn a_realtime_voice_session_reserves_takes_turns_and_settles() { - let plane = streaming_plane(); +/// The bind for one instance, labelled `instance` (two instances never share a label). +fn bind(d: &Dispatcher, instance: &str) -> Bind { + Bind { + instance: Arc::from(instance), + max_inflight_cap: 8, + sink: Arc::new(NoSink), + dispatcher: d.adopter(), + // These rows drive the door's slots and never dial: its needs are not declared, bound + // as a probe. + conns: busbar_plugin_loader::dispatch::ConnTable::Probe, + } +} - // The plane is STREAMING, and the realtime voice dialect is one of its dialects — asserted at the - // identity level so the reframe is a fact the rig checks, not a comment. - assert_eq!( - ::KEY, - "streaming", - "the plane registers under its own streaming key, never 'voice'" - ); - assert_eq!(Dialect::OpenaiRealtime.name(), "openai-realtime"); - - let arena = LeakPlaneAlloc; - let config = EmptyConfig; - let transport = WsStack::new("/v1/realtime"); - let labels = Labels::new(); - let c = ctx(&arena, &config, &transport, &labels); - - // ── RESERVE ──────────────────────────────────────────────────────────────────────────────── - // The client half of the session's codec state is opened by the plane, as the kernel opens it at - // session admit; the first client frame drafts the turn the hold is reserved against. - let mut state = SessionPlane::open_session(&plane, &c); - let open_bytes = session_update_fixture(); - let open_frames = [frame(&open_bytes)]; - let mut open_cursor = FrameCursor::new(&open_frames); - let ingress = plane - .decode_ingress(&mut open_cursor, Some(&mut state), &c) - .expect("session.update decodes"); - let Ingress::Open(draft) = ingress else { - panic!("expected Ingress::Open (a turn opens), got {ingress:?}"); - }; - assert_eq!(draft.op.as_str(), "duplex_turn"); - assert_eq!( - draft.facts.get(busbar_plane_streaming::meta::FACT_DIALECT), - Some(FactValue::Str("openai-realtime")), - "the opened turn names the realtime voice dialect" - ); +/// The compiled-in row: the door and the Statement rendering it states. +fn row() -> LinkedRow { + LinkedRow::of(door::door).expect("the streaming door states itself") +} + +fn json(b: &'static [u8]) -> Blob { + Blob { + ptr: b.as_ptr(), + len: b.len(), + fmt: BLOB_JSON, + flags: 0, + } +} + +fn text(s: &'static str) -> AbiStr { + AbiStr { + ptr: s.as_ptr(), + len: s.len(), + } +} + +fn read(s: AbiStr) -> String { + if s.ptr.is_null() { + return String::new(); + } + // SAFETY: the plugin's text, valid while its generation is live. + String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(s.ptr, s.len) }).into_owned() +} + +/// A published snapshot, read while its generation is live: its generation, its claims +/// (`verb target carrier exact?`) and the audience and metadata it binds. +fn snapshot(p: *const PlaneSnapshot) -> String { + assert!(!p.is_null(), "a READY open/refresh publishes a snapshot"); + // SAFETY: the plugin's generation data, valid until `retire` of its generation. + let s = unsafe { &*p }; + let claims: Vec = (0..s.claims_len) + .map(|i| { + // SAFETY: the snapshot names `claims_len` claims. + let c = unsafe { &*s.claims.add(i) }; + format!( + "{} {} {}{}{}", + read(c.verb), + read(c.target), + read(c.carrier), + if c.flags & CLAIM_EXACT != 0 { + " exact" + } else { + "" + }, + if c.flags & CLAIM_OPEN != 0 { + " open" + } else { + "" + } + ) + }) + .collect(); + format!( + "gen={} claims=[{}] audience={} metadata={} routes={}", + s.generation, + claims.join(", "), + read(s.audience), + read(s.resource_metadata), + s.admin_routes_len + ) +} + +fn linked(d: &Dispatcher) -> Plugin { + load_linked::(&row(), bind(d, "linked")).expect("the linked streaming door loads") +} + +/// The example `cdylib` in this target dir (`cargo test` builds examples). Under CI a missing +/// artifact is a failure, never a skip. +fn dropped(d: &Dispatcher) -> Option> { + let exe = std::env::current_exe().ok()?; + let path = exe.parent()?.parent()?.join("examples").join(format!( + "{}streaming_door{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + )); assert!( - draft.correlation_out.is_some(), - "the turn carries the correlation an answer rides back on" + path.exists() || std::env::var_os("CI").is_none(), + "the streaming_door example cdylib is not built under CI; a both-ways proof must not skip" ); + let stated = row().statement; + path.exists().then(|| { + load_dropped::(&path, &stated, bind(d, "dropped")) + .expect("the dropped streaming door loads") + }) +} - // ── TURNS ───────────────────────────────────────────────────────────────────────────────── - // (1) Audio relays UNDER the open turn's hold — a second frame does not reopen a turn. - let audio = serde_json::to_vec(&json!({ - "type": "input_audio_buffer.append", - "audio": "AAAA", - })) - .expect("audio fixture serializes"); - let audio_frames = [frame(&audio)]; - let mut audio_cursor = FrameCursor::new(&audio_frames); - let relayed = plane - .decode_ingress(&mut audio_cursor, Some(&mut state), &c) - .expect("audio frame decodes"); - let Ingress::Frame { for_, .. } = relayed else { - panic!("expected Ingress::Frame (audio relays under the turn), got {relayed:?}"); - }; - assert!( - for_.is_some(), - "the relay frame carries the turn's correlation" +fn validate(p: &Plugin, settings: &'static [u8]) -> Outcome { + let mut v = Frame::new( + ValidateIn { + head: in_head(), + settings: json(settings), + err_buf: std::ptr::null_mut(), + err_cap: 0, + }, + out_head(), ); + p.call(life::VALIDATE, &mut v).outcome +} - // (2) BARGE-IN: a client truncation supersedes the open turn, and the plane writes the declared - // interrupt fact carrying the playback position — the kernel's teller loop reads this to truncate. - let truncate = serde_json::to_vec(&json!({ - "type": "conversation.item.truncate", - "item_id": "item_1", - "content_index": 0, - "audio_end_ms": 640, - })) - .expect("truncate fixture serializes"); - let truncate_frames = [frame(&truncate)]; - let mut truncate_cursor = FrameCursor::new(&truncate_frames); - let barge_in = plane - .decode_ingress(&mut truncate_cursor, Some(&mut state), &c) - .expect("truncate decodes"); - let Ingress::Open(bi_draft) = barge_in else { - panic!("expected Ingress::Open (barge-in opens the superseding turn), got {barge_in:?}"); +fn open( + p: &Plugin, + generation: u64, + settings: &'static [u8], + public_url: Option<&'static str>, +) -> (Outcome, String) { + let mut i: PlaneOpenIn = z(); + i.open.head = in_head(); + i.open.generation = generation; + i.open.settings = json(settings); + if let Some(u) = public_url { + i.public_url = text(u); + } + let mut o: PlaneOpenOut = z(); + o.open.head = out_head(); + let mut f = Frame::new(i, o); + let c = p.call(life::OPEN, &mut f); + let snap = if c.outcome == Outcome::Ready { + snapshot(f.out.snapshot) + } else { + String::new() }; - let interrupt_fact = ::INTERRUPT_FACT - .expect("the streaming plane declares an interrupt fact"); - assert_eq!( - bi_draft.facts.get(interrupt_fact), - Some(FactValue::Int(640)), - "barge-in carries the playback position the truncation is established at" - ); + (c.outcome, snap) +} + +/// THE SCRIPT: the lifecycle over two generations, then one unit offered to the door. +fn script(p: &Plugin, public_url: Option<&'static str>) -> Vec { + let mut t = Vec::new(); + t.push(format!( + "validate unknown key {:?}", + validate(p, br#"{"nonsense_key":1}"#) + )); + t.push(format!( + "validate zero ceiling {:?}", + validate(p, br#"{"session_max_secs":0}"#) + )); + t.push(format!("validate empty {:?}", validate(p, b""))); + t.push(format!( + "validate ceiling {:?}", + validate(p, br#"{"session_max_secs":1800}"#) + )); + // The stage-3g deal's shape, `{streams:
}` (the boot's validate hands this). + t.push(format!( + "validate dealt {:?}", + validate(p, br#"{"streams":{"session":{"model":"m-cap"}}}"#) + )); + t.push(format!( + "validate dealt unknown key {:?}", + validate(p, br#"{"streams":{"nonsense_key":1}}"#) + )); - // (3) TOOL LOOP: a provider tool call arrives on the upstream half and surfaces as a one-shot - // sub-unit the kernel drives the tool loop over — priced as its own `tool_call` op class. The - // ANNOUNCEMENT mints nothing; the call becomes its own unit on CLOSE, when its arguments are - // complete, because a tool call dispatched with no arguments is a different call. - let mut upstream_state = SessionPlane::open_upstream( - &plane, - &destination("api.openai.com", LaneId::new("realtime")), - &c, + let (outcome, snap) = open(p, 1, br#"{"session_max_secs":1800}"#, public_url); + t.push(format!("open {outcome:?} {snap}")); + + let mut r = Frame::new( + RefreshIn { + head: in_head(), + generation: 2, + settings: json(b"{}"), + secrets: std::ptr::null(), + secrets_len: 0, + }, + PlaneRefreshOut { + head: out_head(), + snapshot: std::ptr::null(), + }, ); - let tool_open = serde_json::to_vec(&json!({ - "type": "response.output_item.added", - "item": { "type": "function_call", "call_id": "call_1", "name": "lookup" }, - })) - .expect("tool-call fixture serializes"); - let tool_frames = [frame(&tool_open)]; - let mut tool_cursor = FrameCursor::new(&tool_frames); - let announced = plane - .decode_response( - &mut tool_cursor, - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut upstream_state), - &c, - ) - .expect("tool-call open decodes"); - assert!( - matches!(announced, Progress::Discard { .. }), - "an announcement accumulates; it mints no unit on its own, got {announced:?}" + let c = p.call(life::REFRESH, &mut r); + t.push(format!( + "refresh {:?} {}", + c.outcome, + snapshot(r.out.snapshot) + )); + + let mut g = Frame::new( + GenIn { + head: in_head(), + generation: 1, + }, + out_head(), ); + t.push(format!( + "retire 1 {:?}", + p.call(life::RETIRE, &mut g).outcome + )); + + for claim in [3, 5, 9] { + let mut a: Frame = Frame::new(z(), z()); + a.input.head = in_head(); + a.input.claim = claim; + a.out.head = out_head(); + let c = p.call(slot::ARRIVE, &mut a); + t.push(format!( + "arrive {claim} {:?} dialect={} op={} principal={}", + c.outcome, a.out.dialect, a.out.op_class, a.out.principal_need + )); + } - let tool_close = serde_json::to_vec(&json!({ - "type": "response.function_call_arguments.done", - "call_id": "call_1", - "name": "lookup", - "arguments": "{\"city\":\"Paris\"}", - })) - .expect("tool-call close fixture serializes"); - let tool_frames = [frame(&tool_close)]; - let mut tool_cursor = FrameCursor::new(&tool_frames); - let tool = plane - .decode_response( - &mut tool_cursor, - &destination("api.openai.com", LaneId::new("realtime")), - Some(&mut upstream_state), - &c, + let mut k: Frame = Frame::new(z(), z()); + k.input.head = in_head(); + k.input.now_ns = 5; + k.out.head = out_head(); + let c = p.call(life::TICK, &mut k); + t.push(format!("tick {:?} next={}", c.outcome, k.out.next_tick_ns)); + + let mut x: Frame< + busbar_contract::abi::plane::PlaneCancelIn, + busbar_contract::abi::plane::PlaneCancelOut, + > = Frame::new(z(), z()); + x.input.cancel.head = in_head(); + x.out.cancel.head = out_head(); + let c = p.call(life::CANCEL, &mut x); + t.push(format!( + "cancel {:?} failed={}", + c.outcome, + x.out.cancel.disposition == CANCEL_FAILED + )); + t +} + +const PUBLIC: &str = "https://gw.example.com/ignored?q=1#f"; + +fn expected(public: bool) -> Vec { + let doors = "POST /v1/realtime/client_secrets http exact, POST /v1/realtime/calls http exact, \ + GET /v1/realtime/sideband/{call_id} ws, GET /v1/realtime/gemini/{call_id} ws, \ + GET /twilio/{call_id} ws"; + let (claims, audience, metadata) = if public { + ( + doors, + "https://gw.example.com/v1/realtime", + "https://gw.example.com/.well-known/oauth-protected-resource/v1/realtime", ) - .expect("tool-call close decodes"); - let Progress::OneShot(tool_draft) = tool else { - panic!("expected Progress::OneShot (a completed tool call opens), got {tool:?}"); + } else { + ("", "", "") }; - assert_eq!(tool_draft.op.as_str(), "tool_call"); - assert_eq!( - tool_draft - .facts - .get(busbar_plane_streaming::meta::FACT_TOOL_NAME), - Some(FactValue::Str("lookup")), - "the tool-call sub-unit names the tool the loop invokes" - ); - let tool_args: serde_json::Value = serde_json::from_slice(tool_draft.body_ir.body()) - .expect("the tool-call sub-unit body carries the arguments the model asked for"); + let snap = |g: u64| { + format!("gen={g} claims=[{claims}] audience={audience} metadata={metadata} routes=0") + }; + vec![ + "validate unknown key Refused".into(), + "validate zero ceiling Refused".into(), + "validate empty Ready".into(), + "validate ceiling Ready".into(), + "validate dealt Ready".into(), + "validate dealt unknown key Refused".into(), + format!("open Ready {}", snap(1)), + format!("refresh Ready {}", snap(2)), + "retire 1 Ready".into(), + "arrive 3 Ready dialect=1 op=0 principal=1".into(), + "arrive 5 Refused dialect=0 op=0 principal=0".into(), + "arrive 9 Refused dialect=0 op=0 principal=0".into(), + "tick Ready next=0".into(), + "cancel Ready failed=true".into(), + ] +} + +#[test] +fn the_linked_door_states_the_planes_doors_under_its_public_url() { + let d = Dispatcher::new(DispatchConfig::default()); + assert_eq!(script(&linked(&d), Some(PUBLIC)), expected(true)); +} + +#[test] +fn a_deployment_with_no_public_url_is_claimed_by_no_door() { + let d = Dispatcher::new(DispatchConfig::default()); + assert_eq!(script(&linked(&d), None), expected(false)); +} + +#[test] +fn the_dropped_door_answers_as_the_linked_door_does() { + let d = Dispatcher::new(DispatchConfig::default()); + for url in [Some(PUBLIC), None] { + // A fresh instance of each door per script: the script opens its instance, and an open + // instance refuses a second open. + let Some(dropped) = dropped(&d) else { + eprintln!("skip: the streaming_door example cdylib is not built"); + return; + }; + assert_eq!( + script(&dropped, url), + script(&linked(&d), url), + "the two doors must answer identically" + ); + } +} + +#[test] +fn the_tail_states_what_the_served_plane_declares() { + // The billable classes a session counts in, the per-session fee and the `session` grant kind: + // the served plane's own declaration, read off the tail the door states. + let t = door::TAIL; + assert_eq!(read(t.scope), "session"); + assert_eq!(read(t.audit_kind), "streaming_session"); + // SAFETY: the tail's `'static` lists. + let classes: Vec = + unsafe { std::slice::from_raw_parts(t.billable_classes, t.billable_classes_len) } + .iter() + .map(|c| format!("{}/{}", read(c.class), read(c.family))) + .collect(); assert_eq!( - tool_args["city"], "Paris", - "the arguments reach the executor, not an empty body" + classes, + [ + "audio_tokens_in/token", + "audio_tokens_out/token", + "text_tokens_in/token", + "text_tokens_out/token", + "audio_seconds_in/duration", + "tool_calls/count", + "per_session/count", + ] ); + // SAFETY: as above. + let fees: Vec = unsafe { std::slice::from_raw_parts(t.fee_units, t.fee_units_len) } + .iter() + .map(|f| read(*f)) + .collect(); + assert_eq!(fees, ["per_session"]); + busbar_contract::abi::plane::check::check_tail(t) + .expect("the kind's own check admits the tail"); + // The fee unit is one of the billable classes (ARCHITECT Q-L5-FEE (A)+(C)): the plane can + // report it, and the loader's bind-time check admits it. + // SAFETY: as above. + let (fee_units, billable) = unsafe { + ( + std::slice::from_raw_parts(t.fee_units, t.fee_units_len), + std::slice::from_raw_parts(t.billable_classes, t.billable_classes_len), + ) + }; + busbar_contract::abi::plane::check::check_fee_units(fee_units, billable) + .expect("every fee unit is a billable class"); +} + +// ── THE ONE-REQUEST DOORS' PIECES ─────────────────────────────────────────────────────────────── + +/// The section the unit script opens under: the session params a mint locks the browser to. +const SESSION: &[u8] = br#"{"session":{"model":"m-cap"}}"#; +/// The kernel's reference for the caller. +const CALLER_REF: &str = "c0ffee"; +/// The body the served plane sent its far end for that mint (the recorded +/// `streams|mint|client-secrets` cell, `golden/1.6.0-pre`). +const MINT_BODY: &str = + r#"{"expires_after":{"anchor":"created_at","seconds":600},"session":{"model":"m-cap"}}"#; +/// The far end's mint answer. +const MINTED: &[u8] = br#"{"value":"ek_oracle_0001","expires_at":1767225600}"#; +/// The caller's SDP offer, and the far end's answer and where its call lives. +const OFFER: &[u8] = b"v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=oracle-offer\r\nt=0 0\r\n"; +const SDP_ANSWER: &[u8] = b"v=0\r\no=- 2 2 IN IP4 127.0.0.1\r\ns=oracle-answer\r\nt=0 0\r\n"; +const LOCATION: &str = "/v1/realtime/calls/rtc_oracle0001"; + +fn octets(b: &[u8]) -> Blob { + Blob { + ptr: b.as_ptr(), + len: b.len(), + fmt: BLOB_OCTETS, + flags: 0, + } +} + +fn at(buf: &[u8], s: Span) -> String { + String::from_utf8_lossy(&buf[s.offset as usize..(s.offset + s.len) as usize]).into_owned() +} + +/// The host's buffers for one `on_piece`. +struct Bufs { + reply: Vec, + units: Vec, + fields: [OutField; 4], + arena: [u8; 256], + /// The units the last answer wrote (class, source, amount), or, short, the room it needed. + last_units: String, + /// The need the last answer named for its far request (`0` = none). + last_need: u32, +} + +/// One piece pushed to a unit: whose it is, its flags, bytes, status and kept head fields. +struct Push<'a> { + unit: u64, + /// A live session's stream (`0` = a request unit). + stream: u64, + claim: u32, + from: u32, + flags: u32, + attempt_no: u32, + bytes: &'a [u8], + status: u32, + head: &'a [Field], +} + +impl Bufs { + fn new(reply_cap: usize) -> Self { + Self::with_units(reply_cap, 2) + } + + fn with_units(reply_cap: usize, units: usize) -> Self { + Self { + reply: vec![0; reply_cap], + units: vec![z(); units], + fields: [z(); 4], + arena: [0; 256], + last_units: String::new(), + last_need: 0, + } + } - // ── SETTLE (END-OF-TURN) ──────────────────────────────────────────────────────────────────── - // The turn's `response.done` carries usage; the plane decodes it as the terminal frame and the - // meter names every per-turn class with the reported quantity. This is the evidence the money-book - // settles the reserved hold against, once per completed turn. - let done = serde_json::to_vec(&json!({ - "type": "response.done", - "response": { - "usage": { - "input_token_details": { "audio_tokens": 10, "text_tokens": 3, "cached_tokens": 1 }, - "output_token_details": { "audio_tokens": 20, "text_tokens": 4 }, - } + /// `push` answered: the outcome and what it wrote, as text. + fn call(&mut self, p: &Plugin, push: &Push<'_>) -> String { + let mut i: OnPieceIn = z(); + i.head = in_head(); + (i.unit, i.claim, i.from, i.flags) = (push.unit, push.claim, push.from, push.flags); + i.stream = push.stream; + (i.attempt_no, i.bytes, i.status_code) = (push.attempt_no, octets(push.bytes), push.status); + i.caller_ref = text(CALLER_REF); + if !push.head.is_empty() { + (i.head_fields, i.head_fields_len) = (push.head.as_ptr(), push.head.len()); } - })) - .expect("usage fixture serializes"); - let done_frames = [frame(&done)]; - let mut done_cursor = FrameCursor::new(&done_frames); - let settle_dest = destination("api.openai.com", LaneId::new("realtime")); - let progress = plane - .decode_response( - &mut done_cursor, - &settle_dest, - Some(&mut upstream_state), - &c, + (i.reply_buf, i.reply_cap) = (self.reply.as_mut_ptr(), self.reply.len()); + (i.units_buf, i.units_cap) = (self.units.as_mut_ptr(), self.units.len()); + (i.fields_buf, i.fields_cap) = (self.fields.as_mut_ptr(), self.fields.len()); + (i.arena_buf, i.arena_cap) = (self.arena.as_mut_ptr(), self.arena.len()); + let mut o: OnPieceOut = z(); + o.head = out_head(); + let mut f = Frame::new(i, o); + let c = p.call(slot::ON_PIECE, &mut f); + let o = f.out; + self.last_need = o.need; + self.last_units = if o.units_needed != 0 { + format!("needed {}", o.units_needed) + } else { + self.units[..o.units_written as usize] + .iter() + .map(|u| format!("{}:{}={}", u.class, u.source, u.amount)) + .collect::>() + .join(" ") + }; + let fields: Vec = self.fields[..o.fields_written as usize] + .iter() + .map(|f| format!("{}={}", at(&self.arena, f.name), at(&self.arena, f.value))) + .collect(); + format!( + "{:?} emitted={} more={} far={} done={} status={} verb={} target={} fields={fields:?}", + c.outcome, + String::from_utf8_lossy(&self.reply[..o.emitted as usize]), + o.more, + o.flags & EMIT_TO_FAR_END != 0, + o.flags & EMIT_DONE != 0, + o.reply_status, + at(&self.arena, o.verb), + at(&self.arena, o.target), ) - .expect("response.done decodes"); - let Progress::Terminal { r, .. } = progress else { - panic!("expected Progress::Terminal (the turn completes), got {progress:?}"); + } +} + +/// A piece of `unit` on `claim`. +const fn push(unit: u64, claim: u32, from: u32, flags: u32, bytes: &[u8]) -> Push<'_> { + Push { + unit, + stream: 0, + claim, + from, + flags, + attempt_no: 0, + bytes, + status: 0, + head: &[], + } +} + +/// The ATTEMPT that opens `unit`'s first attempt. +const fn attempt(unit: u64, claim: u32) -> Push<'static> { + Push { + attempt_no: 1, + ..push(unit, claim, FROM_KERNEL, 0, &[]) + } +} + +/// THE UNIT SCRIPT: a mint, an SDP offer, a session door's piece and an unpublished claim's (the +/// metadata path among them: it is no claim, 1.5.5 parity), in the order the route pump pushes them. +fn unit_script(p: &Plugin) -> Vec { + let mut t = vec![format!("open {:?}", open(p, 1, SESSION, Some(PUBLIC)).0)]; + let mut b = Bufs::new(256); + let whole = PIECE_HAS_STATUS | PIECE_LAST; + + t.push(format!("mint attempt {}", b.call(p, &attempt(7, 0)))); + let caller = push(7, 0, FROM_CALLER, PIECE_LAST, &[]); + t.push(format!("mint caller {}", b.call(p, &caller))); + let far = Push { + status: 200, + ..push(7, 0, FROM_FAR_END, whole, MINTED) }; - assert_eq!(r.finish, busbar_contract::unit::FinishClass::TurnComplete); - - let settled_unit = harness::unit(OpClassId::new("duplex_turn"), r.ir, Facts::new()); - let locators = plane.meter(&settled_unit, &r, &c); - let quantity = |class: &str| { - locators - .lines - .as_slice() - .iter() - .find(|l| l.class.as_str() == class) - .and_then(|l| l.quantity) + t.push(format!("mint far_end {}", b.call(p, &far))); + + t.push(format!("sdp attempt {}", b.call(p, &attempt(8, 1)))); + let offer = push(8, 1, FROM_CALLER, PIECE_LAST, OFFER); + t.push(format!("sdp caller {}", b.call(p, &offer))); + let location = [Field { + name: text("location"), + value: text(LOCATION), + }]; + let far = Push { + status: 201, + head: &location, + ..push(8, 1, FROM_FAR_END, whole, SDP_ANSWER) }; - // Every class the money-book settles the turn against, at the quantity the usage reports. The - // in/out split is the money-relevant part: swapping a direction reprices the turn. - assert_eq!(quantity("audio_tokens_in"), Some(10)); - assert_eq!(quantity("audio_tokens_out"), Some(20)); - assert_eq!(quantity("text_tokens_in"), Some(3)); - assert_eq!(quantity("text_tokens_out"), Some(4)); - // #71: cached_tokens is a SUBSET of audio_tokens_in/text_tokens_in, not a class beside them, so - // it never reaches the meter as its own line even though the usage report names it — pricing it - // too would double-bill the same cached input. - assert_eq!(quantity("cached_tokens"), None); - // The duration class the architecture's own inventory names for this plane is present too. - assert!( - locators - .lines - .as_slice() - .iter() - .any(|l| l.class.as_str() == "audio_seconds_in"), - "the per-turn meter names the audio-seconds class the money-book settles duration against" - ); + t.push(format!("sdp far_end {}", b.call(p, &far))); + + t.push(format!("sideband {}", b.call(p, &attempt(10, 2)))); + t.push(format!("unpublished {}", b.call(p, &attempt(11, 9)))); + t.push(format!("metadata path {}", b.call(p, &attempt(12, 5)))); + t +} + +/// What the unit script must read: the served plane's answers, through the door. +fn unit_expected() -> Vec { + let none = "verb= target= fields=[]"; + let minted = driven::mint_reply(200, MINTED); + let mut t = vec![ + "open Ready".to_string(), + format!( + "mint attempt Ready emitted={MINT_BODY} more=0 far=true done=false status=0 \ + verb=POST target=/v1/realtime/client_secrets \ + fields=[\"content-type=application/json\", \"OpenAI-Safety-Identifier={CALLER_REF}\"]" + ), + format!("mint caller Ready emitted= more=0 far=false done=false status=0 {none}"), + format!( + "mint far_end Ready emitted={} more=0 far=false done=true status=200 verb= target= \ + fields=[\"content-type=application/json\", \"content-length={}\"]", + String::from_utf8_lossy(&minted.body), + minted.body.len() + ), + "sdp attempt Ready emitted= more=0 far=true done=false status=0 verb=POST \ + target=/v1/realtime/calls fields=[\"content-type=application/sdp\"]" + .to_string(), + format!( + "sdp caller Ready emitted={} more=0 far=true done=false status=0 {none}", + String::from_utf8_lossy(OFFER) + ), + format!( + "sdp far_end Ready emitted={} more=0 far=false done=true status=201 verb= target= \ + fields=[\"content-type=application/sdp\", \"location={LOCATION}\", \ + \"content-length={}\"]", + String::from_utf8_lossy(SDP_ANSWER), + SDP_ANSWER.len() + ), + ]; + t.push(format!( + "sideband Refused emitted= more=0 far=false done=false status=0 {none}" + )); + t.push(format!( + "unpublished Refused emitted= more=0 far=false done=false status=0 {none}" + )); + t.push(format!( + "metadata path Refused emitted= more=0 far=false done=false status=0 {none}" + )); + t +} + +#[test] +fn the_one_request_doors_answer_their_pieces_through_the_door() { + let d = Dispatcher::new(DispatchConfig::default()); + assert_eq!(unit_script(&linked(&d)), unit_expected()); } -/// The streaming plane speaks exactly the dialect roster the proven voice adapter does — the reframe -/// changes the plane's identity, never the claims the boot's overlap check reads. #[test] -fn the_streaming_plane_declares_its_full_dialect_and_meter_roster() { - // Three WebSocket claims (openai-realtime + gemini-live + twilio-media-streams) and two one-shot - // claims (transcribe + tts). +fn the_dropped_door_answers_the_one_request_doors_as_the_linked_door_does() { + let d = Dispatcher::new(DispatchConfig::default()); + let Some(dropped) = dropped(&d) else { + eprintln!("skip: the streaming_door example cdylib is not built"); + return; + }; assert_eq!( - ::CLAIMS.len(), - 5, - "streaming claims its five live dialect surfaces" + unit_script(&dropped), + unit_script(&linked(&d)), + "the two doors must answer identically" ); - // The six BILLABLE per-turn meter classes this plane declares (architect ruling #71: a plane's - // declared billable classes must be pairwise disjoint, so `cached_tokens` — a subset of the input - // token classes, not a class beside them — is carried only as an unbilled attribution fact, not a - // meter class; see `busbar_plane_streaming::meta::CLASS_CACHED_TOKENS`'s doc). - assert_eq!( - ::METER_CLASSES.len(), - 6, - "streaming meters its six billable per-turn classes" +} + +// ── LIVE SESSIONS THROUGH THE DOOR (K6) ────────────────────────────────────────────────────────── + +/// A piece of the live session on stream `stream` (claim 2, the sideband socket). +const fn session(stream: u64, from: u32, flags: u32, bytes: &[u8]) -> Push<'_> { + Push { + stream, + ..push(stream, 2, from, flags, bytes) + } +} + +/// `ms` milliseconds of 24 kHz PCM16 uplink audio, as the caller's realtime frame. +fn uplink(ms: usize) -> Vec { + serde_json::to_vec(&serde_json::json!({"type":"input_audio_buffer.append", + "audio": busbar_contract::media::base64_encode(&vec![0u8; ms * 48])})) + .expect("json") +} + +const USAGE_DONE: &[u8] = br#"{"type":"response.done","response":{"usage":{"input_token_details":{"audio_tokens":10,"text_tokens":3},"output_token_details":{"audio_tokens":20,"text_tokens":4}}}}"#; + +/// THE SESSION SCRIPT, in the order the driver pushes a session's pieces: the caller's audio (a +/// turn toward the far end), the turn's ATTEMPT, the far end's usage, a collection with nothing +/// owed, an answer short of unit room and its re-call, and the caller's end. +fn session_script(p: &Plugin) -> Vec { + let mut t = vec![format!("open {:?}", open(p, 1, SESSION, Some(PUBLIC)).0)]; + let mut b = Bufs::with_units(1 << 18, 6); + let line = |t: &mut Vec, b: &mut Bufs, what: &str, push: &Push<'_>| { + let answer = b.call(p, push); + t.push(format!("{what} {answer} units=[{}]", b.last_units)); + }; + let audio = uplink(2000); + line( + &mut t, + &mut b, + "caller audio", + &session(20, FROM_CALLER, 0, &audio), ); - // The two duplex dialects a streaming session may DIAL, with voice among them. - let plane = streaming_plane(); - assert!(plane - .upstream_for_dialect(Dialect::OpenaiRealtime) - .is_some()); - assert!(plane.upstream_for_dialect(Dialect::GeminiLive).is_none()); -} - -/// Twilio Media Streams is a streaming DIALECT carried by the WebSocket transport, not a transport -/// of its own (the design lists five transports and no telephony one). Its -/// claim is on the WebSocket transport, one path level under `/twilio`, and authenticates under the -/// `webhook-signature` alternative alone — Twilio holds no busbar bearer or API key. Without the -/// claim the dialect's reader, codec and µ-law transform are unreachable: no arrival ever names it. -#[test] -fn twilio_media_streams_is_claimed_on_the_websocket_carrier_under_the_webhook_signature() { - use busbar_contract::grammar::Selector; - use busbar_plane_streaming::claims::{dialect_for, DIALECT_CLAIMS, WS_TRANSPORT}; + let turn = Push { + attempt_no: 1, + ..session(20, FROM_KERNEL, 0, &[]) + }; + line(&mut t, &mut b, "turn attempt", &turn); + let far = Push { + status: 101, + ..session(20, FROM_FAR_END, PIECE_HAS_STATUS, USAGE_DONE) + }; + line(&mut t, &mut b, "far usage", &far); + line(&mut t, &mut b, "collect", &session(20, FROM_KERNEL, 0, &[])); + let mut narrow = Bufs::with_units(1 << 18, 2); + let short = session(20, FROM_CALLER, 0, &audio); + let answer = narrow.call(p, &short); + t.push(format!("short {answer} units=[{}]", narrow.last_units)); + line(&mut t, &mut b, "short re-call", &short); + line( + &mut t, + &mut b, + "caller end", + &session(20, FROM_CALLER, PIECE_LAST, &[]), + ); + line( + &mut t, + &mut b, + "request unit on a session door", + &attempt(21, 2), + ); + t +} - let twilio: Vec<_> = DIALECT_CLAIMS - .iter() - .filter(|c| c.dialect == Dialect::TwilioMediaStreams) - .collect(); - assert_eq!( - twilio.len(), - 1, - "exactly one claim names the Twilio dialect" +#[test] +fn a_live_session_is_answered_through_the_door() { + let d = Dispatcher::new(DispatchConfig::default()); + let t = session_script(&linked(&d)); + let payload = busbar_contract::media::base64_encode(&vec![0u8; 2000 * 48]); + let line = |what: &str| { + t.iter() + .find(|l| l.starts_with(what)) + .unwrap_or_else(|| panic!("no `{what}` line in {t:#?}")) + .clone() + }; + assert_eq!(t[0], "open Ready"); + let caller = line("caller audio"); + assert!( + caller.contains("far=true") + && caller.contains("verb=GET target=/v1/realtime ") + && caller.contains("input_audio_buffer.append") + && caller.contains(&payload) + && caller.contains("more=0") + && caller.ends_with("units=[]"), + "the caller's frame is one turn toward the far end, its audio unchanged: {caller}" ); - let claim = &twilio[0].claim; assert_eq!( - claim.transport, WS_TRANSPORT, - "Twilio rides the WebSocket transport" + line("turn attempt"), + "turn attempt Ready emitted= more=0 far=false done=false status=0 verb= target= \ + fields=[] units=[]", + "the turn's request is the caller side's answer" + ); + let far = line("far usage"); + assert!( + far.contains("far=false done=false") + && far.ends_with("units=[0:1=10 1:1=20 2:1=3 3:1=4 4:1=2 6:1=1]"), + "the closed turn's tokens, its two admitted seconds and the answered session's fee \ + unit, reported: {far}" + ); + assert!(line("collect ").contains("emitted= more=0 far=false done=false")); + assert!( + line("short ").starts_with("short Failed") && line("short ").ends_with("units=[needed 6]"), + "{}", + line("short ") + ); + let recall = line("short re-call"); + assert!( + recall.contains("far=true") + && recall.ends_with("units=[0:1=10 1:1=20 2:1=3 3:1=4 4:1=2 6:1=1]"), + "the re-call answers the same piece once, its audio not counted twice: {recall}" ); - assert_eq!(claim.selector, Selector::PrefixOneLevel("/twilio")); - assert_eq!(claim.scheme_alternatives, &["webhook-signature"]); + let end = line("caller end"); assert!( - ::CLAIMS.contains(claim), - "the declared claim is one the boot seal reads" + end.contains("done=true") && end.ends_with("units=[0:1=10 1:1=20 2:1=3 3:1=4 4:1=4 6:1=1]"), + "the caller's end settles the open turn's two seconds once: {end}" + ); + assert!(line("request unit on a session door") + .starts_with("request unit on a session door Refused")); +} + +#[test] +fn the_dropped_door_answers_a_live_session_as_the_linked_door_does() { + let d = Dispatcher::new(DispatchConfig::default()); + let Some(dropped) = dropped(&d) else { + eprintln!("skip: the streaming_door example cdylib is not built"); + return; + }; + assert_eq!( + session_script(&dropped), + session_script(&linked(&d)), + "the two doors must answer a session identically" ); - // An arrival one level under `/twilio` names the dialect; the prefix alone and a deeper path do - // not, and neither does another dialect's path. +} + +// ── THE ROUTE A DOOR NAMES (ARCHITECT Q-L5B-ROUTE) ─────────────────────────────────────────────── + +/// `arrive` on `claim`: its outcome, the route class and the entry it names. +fn arrive_route(p: &Plugin, claim: u32) -> (Outcome, u8, String) { + let mut a: Frame = Frame::new(z(), z()); + a.input.head = in_head(); + a.input.claim = claim; + a.out.head = out_head(); + let c = p.call(slot::ARRIVE, &mut a); + (c.outcome, a.out.route, read(a.out.pool)) +} + +/// Every door that reaches the far end names the DIRECT route `streams.session.model` (the entry the +/// composition folds from the top-level models catalog); claim 5 (the metadata path) is no claim +/// and arrives nowhere; with no model configured nothing is named (the door steps refuse an unnamed keyed route). +/// Both ways. +#[test] +fn a_door_that_reaches_the_far_end_names_the_session_model_as_its_direct_route() { + use busbar_contract::abi::plane::{ROUTE_DIRECT, ROUTE_POOL}; + let d = Dispatcher::new(DispatchConfig::default()); + let mut doors = vec![linked(&d)]; + doors.extend(dropped(&d)); + for p in &doors { + assert_eq!(open(p, 1, SESSION, Some(PUBLIC)).0, Outcome::Ready); + for claim in [0, 1, 2, 3, 4] { + assert_eq!( + arrive_route(p, claim), + (Outcome::Ready, ROUTE_DIRECT, "m-cap".to_string()), + "claim {claim}" + ); + } + assert_ne!( + arrive_route(p, 5).0, + Outcome::Ready, + "the metadata path is no claim of the plane (1.5.5 parity)" + ); + } + let bare = linked(&d); + assert_eq!(open(&bare, 1, b"{}", Some(PUBLIC)).0, Outcome::Ready); assert_eq!( - dialect_for("/twilio/inbound"), - Some(Dialect::TwilioMediaStreams) + arrive_route(&bare, 2), + (Outcome::Ready, ROUTE_POOL, String::new()), + "no model, no route named" + ); +} + +// ── THE SESSION OPEN'S HOOK VIEW (ARCHITECT Q-L5B-PROJECT; K5's `project`) ─────────────────────── + +/// `project` of unit 31 on `claim` with `rewrite` (empty = none): its outcome, the projected body +/// and whether it was answered as rewritten. +fn project(p: &Plugin, claim: u32, rewrite: &'static [u8]) -> (Outcome, String, bool) { + use busbar_contract::abi::hook::{MessageView, SignalEntry}; + use busbar_contract::abi::plane::{ProjectIn, ProjectOut, SPAN_ABSENT}; + let mut arena = vec![0u8; 4096]; + let mut signals: Vec = vec![z(); 4]; + let mut turns: Vec = vec![z(); 4]; + let mut j: Frame = Frame::new(z(), z()); + (j.input.head, j.out.head) = (in_head(), out_head()); + j.input.claim = claim; + j.input.unit = 31; + j.input.arena_buf = arena.as_mut_ptr(); + j.input.arena_cap = arena.len(); + j.input.signals_buf = signals.as_mut_ptr(); + j.input.signals_cap = signals.len(); + j.input.messages_buf = turns.as_mut_ptr(); + j.input.messages_cap = turns.len(); + if !rewrite.is_empty() { + j.input.rewrite = json(rewrite); + } + let c = p.call(slot::PROJECT, &mut j); + let body = if j.out.body.offset == SPAN_ABSENT { + String::new() + } else { + at(&arena, j.out.body) + }; + (c.outcome, body, j.out.rewritten.offset != SPAN_ABSENT) +} + +/// A session door projects the session-open params it locks as the view's body; a hook's rewrite is +/// patched over them and THAT is answered (kept for the session the unit opens); a rewrite that does +/// not read back refuses; a one-request door projects no body and takes no rewrite. Both ways. +#[test] +fn a_session_open_is_projected_for_the_hooks_and_a_rewrite_patches_it() { + let d = Dispatcher::new(DispatchConfig::default()); + let mut doors = vec![linked(&d)]; + doors.extend(dropped(&d)); + for p in &doors { + assert_eq!(open(p, 1, SESSION, Some(PUBLIC)).0, Outcome::Ready); + let (outcome, body, rewritten) = project(p, 2, b""); + assert_eq!(outcome, Outcome::Ready); + let view: serde_json::Value = serde_json::from_str(&body).expect("the locked params"); + assert_eq!(view["model"], "m-cap", "{body}"); + assert!(!rewritten); + let (outcome, body, rewritten) = project(p, 2, br#"{"voice":"marin"}"#); + assert_eq!(outcome, Outcome::Ready); + let view: serde_json::Value = serde_json::from_str(&body).expect("the patched params"); + assert_eq!(view["voice"], "marin", "the hook's key wins: {body}"); + assert_eq!( + view["model"], "m-cap", + "the unnamed key keeps its value: {body}" + ); + assert!(rewritten, "answered as rewritten"); + assert_eq!(project(p, 2, br#"{"voice":7}"#).0, Outcome::Refused); + assert_eq!(project(p, 0, b""), (Outcome::Ready, String::new(), false)); + assert_eq!(project(p, 0, br#"{"x":1}"#).0, Outcome::Refused); + } +} + +// ── THE NEED A FAR REQUEST RIDES (ARCHITECT Q-L5B-NEEDS) ───────────────────────────────────────── + +/// Every far request names the outbound need it rides: a live session's frames OpenAI Realtime's +/// socket, the mint's request Realtime's one-shot pass (each a declared outbound need under the +/// dialect's style, held by the door's own compile-time check). Both ways. +#[test] +fn every_far_request_names_the_need_it_rides() { + let d = Dispatcher::new(DispatchConfig::default()); + let mut doors = vec![linked(&d)]; + doors.extend(dropped(&d)); + for p in &doors { + assert_eq!(open(p, 1, SESSION, Some(PUBLIC)).0, Outcome::Ready); + let mut b = Bufs::with_units(1 << 18, 8); + let answer = b.call(p, &session(40, FROM_CALLER, 0, &uplink(100))); + assert!(answer.contains("far=true"), "{answer}"); + assert_eq!(b.last_need, door::RIDES_REALTIME_SOCKET, "{answer}"); + let mut b = Bufs::new(1 << 16); + let answer = b.call(p, &attempt(41, 0)); + assert!(answer.contains("far=true"), "{answer}"); + assert_eq!(b.last_need, door::RIDES_REALTIME_PASS, "{answer}"); + } +} + +/// A refused open (a hook's veto, an admission's refusal) is answered in the dialects' error shape: +/// the kernel's status read as the error's type, its words as the message. +#[test] +fn a_refusal_is_rendered_in_the_dialects_error_shape() { + for (status, kind) in [ + (401, "authentication_error"), + (403, "permission_error"), + (429, "rate_limit_error"), + (400, "invalid_request_error"), + (503, "server_error"), + ] { + let body: serde_json::Value = + serde_json::from_slice(&door::refusal_body(status, "no sessions today")).expect("JSON"); + assert_eq!(body["error"]["type"], kind, "{status}"); + assert_eq!(body["error"]["message"], "no sessions today"); + } +} + +/// The kernel's refusal of `reason` at `status`, worded `words`, for a unit-less request on +/// `target`, rendered by the door's `refusal`: the outcome and the body it wrote. +fn refused( + p: &Plugin, + status: u32, + reason: busbar_contract::caps::ReasonCode, + words: &'static str, + target: &'static str, +) -> (Outcome, String) { + use busbar_contract::abi::plane::{reason_code, RefusalIn, RefusalOut, REFUSAL_KERNEL}; + let mut reply = vec![0u8; 1024]; + let mut fields: Vec = vec![z(); 4]; + let mut arena = vec![0u8; 256]; + let mut f: Frame = Frame::new(z(), z()); + (f.input.head, f.out.head) = (in_head(), out_head()); + f.input.cause = REFUSAL_KERNEL; + f.input.status = status; + f.input.reason = reason_code(reason); + f.input.text = text(words); + f.input.target = text(target); + (f.input.reply_buf, f.input.reply_cap) = (reply.as_mut_ptr(), reply.len()); + (f.input.fields_buf, f.input.fields_cap) = (fields.as_mut_ptr(), fields.len()); + (f.input.arena_buf, f.input.arena_cap) = (arena.as_mut_ptr(), arena.len()); + let c = p.call(slot::REFUSAL, &mut f); + let written = usize::try_from(f.out.reply_written).expect("a length"); + ( + c.outcome, + String::from_utf8_lossy(&reply[..written.min(reply.len())]).into_owned(), + ) +} + +/// AN AUTHENTICATION REFUSAL READS IN THE DIALECTS' OWN VENDOR TERMS (spec Part 3, "Refusals": a +/// refusal with no unit is rendered by the plane's path rule, the 1.5.5 residual's answer; oracle +/// cell `streams|mint|no-provider`): the kernel's `Unauthenticated` on the mint door answers as a bad +/// key does at the vendor, never with the kernel's own word, which is a busbar tell. Every other +/// reason keeps the kernel's text, a 401 included (a hook's veto states its own). Both ways. +/// +/// RED ARM: the kernel's word rendered as the message (the door before this rule) is not the +/// vendor's answer. +#[test] +fn an_authentication_refusal_reads_in_the_dialects_vendor_terms() { + use busbar_contract::caps::ReasonCode; + let vendor = serde_json::json!({"error": { + "message": door::AUTH_FAILURE_MESSAGE, + "type": "authentication_error", + "param": null, + "code": door::AUTH_FAILURE_CODE, + }}); + assert_eq!(door::AUTH_FAILURE_MESSAGE, "Incorrect API key provided."); + assert_eq!(door::AUTH_FAILURE_CODE, "invalid_api_key"); + let kernel_word: serde_json::Value = + serde_json::from_slice(&door::refusal_body(401, "unauthenticated")).expect("JSON"); + assert_ne!( + kernel_word, vendor, + "RED ARM: the kernel's word is not the vendor's answer" ); - assert_eq!(dialect_for("/twilio"), None); - assert_eq!(dialect_for("/twilio/a/b"), None); - assert_eq!(dialect_for("/v1/realtime"), Some(Dialect::OpenaiRealtime)); + + let d = Dispatcher::new(DispatchConfig::default()); + let mut doors = vec![linked(&d)]; + doors.extend(dropped(&d)); + for p in &doors { + assert_eq!(open(p, 1, b"{}", Some(PUBLIC)).0, Outcome::Ready); + let (outcome, body) = refused( + p, + 401, + ReasonCode::Unauthenticated, + "unauthenticated", + "/v1/realtime/client_secrets", + ); + assert_eq!(outcome, Outcome::Ready, "{body}"); + let body: serde_json::Value = serde_json::from_str(&body).expect("JSON"); + assert_eq!( + body, vendor, + "the mint door's 401 is the vendor's bad-key answer" + ); + + let (outcome, body) = refused( + p, + 401, + ReasonCode::HookVeto, + "the hook says no", + "/v1/realtime/client_secrets", + ); + assert_eq!(outcome, Outcome::Ready, "{body}"); + let body: serde_json::Value = serde_json::from_str(&body).expect("JSON"); + assert_eq!( + body["error"]["message"], "the hook says no", + "a veto keeps its words" + ); + assert_eq!(body["error"]["code"], serde_json::Value::Null); + } } diff --git a/crates/busbar-plane-streaming/tests/harness/mod.rs b/crates/busbar-plane-streaming/tests/harness/mod.rs deleted file mode 100644 index 79fe0a27b9..0000000000 --- a/crates/busbar-plane-streaming/tests/harness/mod.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! The smallest thing that can call the streaming plane. -//! -//! The same minimum-size harness `busbar-plane-llm` and every other plane crate builds for its tests: -//! an arena, a clock, a configuration view, a transport view and a label set, plus the kernel-built -//! values (a unit, a verified destination) handed through a seal. Nothing here is shipped — it is the -//! test binary's own scaffolding, the same way every plane crate carries one. - -use busbar_contract::bounded::{ - Facts, Ir, Labels, PlaneAlloc, PlaneAllocBudget, ScratchBytes, SlabBytes, Span, -}; -use busbar_contract::dest::{DestinationFacts, VerifiedDestination}; -use busbar_contract::ids::{LaneId, OpClassId, StreamId}; -use busbar_contract::unit::{Clock, ConfigView, Ctx, Origin, TransportView, Unit}; -use busbar_contract::wire::{Direction, Frame, FrameMeta}; -use std::sync::Arc; - -/// An arena that never reuses a byte — fine for a short test process. -#[derive(Debug, Default)] -pub struct LeakPlaneAlloc; - -impl PlaneAlloc for LeakPlaneAlloc { - fn alloc_bytes<'a>(&'a self, src: &[u8]) -> Result, PlaneAllocBudget> { - Ok(ScratchBytes::new(Box::leak( - src.to_vec().into_boxed_slice(), - ))) - } - - fn alloc_str<'a>(&'a self, src: &str) -> Result<&'a str, PlaneAllocBudget> { - Ok(Box::leak(src.to_string().into_boxed_str())) - } - - fn alloc_spans<'a>( - &'a self, - src: &[(&'a str, Span)], - ) -> Result<&'a [(&'a str, Span)], PlaneAllocBudget> { - Ok(Box::leak(src.to_vec().into_boxed_slice())) - } - - fn remaining(&self) -> usize { - usize::MAX - } -} - -/// A configuration block with nothing in it. -#[derive(Debug, Default)] -pub struct EmptyConfig; - -impl ConfigView for EmptyConfig { - fn get_str(&self, _key: &str) -> Option<&str> { - None - } - fn get_int(&self, _key: &str) -> Option { - None - } - fn get_bool(&self, _key: &str) -> Option { - None - } -} - -/// A WebSocket transport stack that publishes a request target as a fact — the convention every -/// plane harness uses for its own `path` fact. -#[derive(Debug)] -pub struct WsStack { - path: String, -} - -impl WsStack { - /// A stack that saw this request target. - #[must_use] - pub fn new(path: &str) -> Self { - Self { - path: path.to_string(), - } - } -} - -impl TransportView for WsStack { - fn key(&self) -> &'static str { - "ws" - } - fn chain(&self) -> &[&'static str] { - &["tcp", "tls", "http", "ws"] - } - fn fact(&self, key: &str) -> Option<&str> { - if key == "path" { - Some(&self.path) - } else { - None - } - } -} - -/// The blessed TEST seal (#65). `KernelSeal` is SEALED — no crate outside `busbar-contract` can -/// implement it — so a fixture names the contract's own `test-seal` type instead of forging one. -/// The type system stops a plugin now, not the manifest allow-list alone. -pub use busbar_contract::plugin::TestKernelSeal as TestSeal; - -/// Build a context over the pieces above. -#[must_use] -pub fn ctx<'u>( - arena: &'u LeakPlaneAlloc, - config: &'u EmptyConfig, - transport: &'u WsStack, - labels: &'u Labels<'u>, -) -> Ctx<'u> { - Ctx::new( - Clock { - unix_secs: 1_772_000_000, - monotonic_nanos: 0, - }, - config, - None, - transport, - labels, - arena, - ) -} - -/// One inbound frame carrying a whole wire event. -#[must_use] -pub fn frame(bytes: &[u8]) -> Frame { - Frame { - direction: Direction::Inbound, - stream: StreamId(0), - bytes: SlabBytes::new(Arc::from(bytes.to_vec().into_boxed_slice())), - meta: FrameMeta::default(), - } -} - -/// A unit built the way the kernel builds one, over a decoded body. -#[must_use] -pub fn unit<'u>(op: OpClassId, body: Ir<'u>, facts: Facts<'u>) -> Unit<'u> { - Unit::new( - &TestSeal, - busbar_contract::UnitKey::new(1), - Origin::Client, - None, - Some(StreamId(0)), - Direction::Inbound, - None, - op, - body, - facts, - None, - ) -} - -/// A destination sealed the way the trust unit seals one. -#[must_use] -pub fn destination(host: &'static str, lane: LaneId) -> VerifiedDestination { - VerifiedDestination::seal( - &TestSeal, - DestinationFacts::Upstream { - transport: "ws", - address: busbar_contract::UpstreamAddress::socket(host), - lane, - }, - "ws", - None, - ) -} diff --git a/crates/busbar-plane-streaming/tests/purity.rs b/crates/busbar-plane-streaming/tests/purity.rs index 7783744782..8647ce97a1 100644 --- a/crates/busbar-plane-streaming/tests/purity.rs +++ b/crates/busbar-plane-streaming/tests/purity.rs @@ -93,8 +93,7 @@ fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) { /// /// LOAD-BEARING HERE, not decoration: this crate's headers legitimately DISCUSS the kernel and its /// sibling planes — `lib.rs:35` names the older `busbar_kernel::plane::registry::PlaneDecl` -/// architecture it is not built on, `claims.rs:117` cites `busbar_plane_llm`'s ladder, and -/// `governed.rs:10/18` explain the dependency inversion by naming `busbar_voice::runtime`. Every +/// architecture it is not built on and `claims.rs:117` cites `busbar_plane_llm`'s ladder. Every /// one is prose about what this plane does NOT do. A gate its own explanation fails is a gate /// somebody deletes. fn is_comment(line: &str) -> bool { @@ -140,3 +139,28 @@ fn the_plane_names_no_kernel_side_crate() { "the streaming plane reaches a kernel-side crate or a sibling plane: {offenders:#?}" ); } + +/// LAW 11 (QUESTIONS Q98): busbar does not act on the data it carries, so the plane holds no port +/// that runs a tool. A tool call is relayed to the caller as-is (`session_pump`'s +/// `a_tool_call_is_relayed_to_the_caller_as_is_and_the_gateway_answers_nothing`); this arm keeps the +/// executor seam itself from coming back. RED: `tools.rs` declared `ToolExecutor` with an +/// `async fn execute(&self, name, arguments) -> Vec` the session pump called on a call's close. +#[test] +fn the_plane_holds_no_tool_executor_port() { + let mut offenders = Vec::new(); + walk(&src_dir(), &mut |path, text| { + for (n, line) in text.lines().enumerate() { + if is_comment(line) { + continue; + } + if line.contains("ToolExecutor") || line.contains("fn execute(") { + offenders.push(format!("{}:{}: {}", path.display(), n + 1, line.trim())); + } + } + }); + assert!( + offenders.is_empty(), + "the streaming plane runs no tool; a tool call is the caller's to run:\n{}", + offenders.join("\n") + ); +} diff --git a/crates/busbar-voice/Cargo.toml b/crates/busbar-voice/Cargo.toml deleted file mode 100644 index 4b3f957e2c..0000000000 --- a/crates/busbar-voice/Cargo.toml +++ /dev/null @@ -1,154 +0,0 @@ -# busbar-voice — THE DUPLEX / LIVE-VOICE PLANE CRATE (Plane 4). -# -# The fourth plane KIND — long-lived, bidirectional, per-frame-governed — added as a NEW crate that -# owns 100% of its protocol vocabulary (see docs/design/BUSBAR-1.6.0.md #18/#45). Core / substrate -# / api never learn the word "audio": this crate declares its own `PlaneDecl` and a `ProtocolDecl` -# with `codec: None` exactly as `busbar-mcp` does (the two dialects meet in the plane's own shared IR -# types, not a `DialectCodec` facade), and the neutral registry unions whatever it declares without -# naming it. -# -# STATUS: the four-layer duplex/session IR (`BUSBAR-1.6.0.md` #18/#45), both dialect codecs (OpenAI -# Realtime + Gemini Live), the T2 session pump, and both topologies are implemented behind the "runtime" -# feature. Boot-mounting into the composition root (PLANE_HOOKS' build / admission / routes hooks) is -# separate, tracked work. It compiles, registers, and is strong-form deletable. -# -# DEPENDENCY POSTURE: `busbar-substrate` is the neutral shared surface the plugin names instead of -# `busbar-core` — in production AND in its tests. Nothing in this crate's closure, under any feature, -# names or links `busbar-core`: the host-driving tests run over the substrate's in-memory fixture host -# (`busbar_substrate::testkit`), reached through the same plane-host seam the production path uses. -[package] -name = "busbar-voice" -version = "1.6.0" -publish = false # internal workspace crate; never goes to crates.io -edition = "2021" -# `rust-version` rationale: see busbar-kernel/Cargo.toml — one pinned number, set to what CI verifies. -rust-version = "1.97" -description = "The duplex / live-voice plane for busbar (Plane 4): the OpenAI Realtime and Gemini Live dialects and the four-layer session IR, as one removable plugin." -license = "Apache-2.0" - -[lib] -name = "busbar_voice" -path = "src/lib.rs" - -# THE VOICE CONFORMANCE HARNESS — the real driver the `testing/voice-conformance/` battery legs shell -# out to. It reuses THIS crate's codecs (`OpenAiRealtimeCodec` / `GeminiLiveCodec`) and the T2 runtime -# (`SessionCore` / `LocalLease` hard-close) to decode/encode the captured fixtures and diff — the legs -# never reimplement a codec in shell. Gated on `runtime` (the D2 governance probe needs the async -# session engine + tokio) AND `test-support` (the admit/route/audit/exit composition legs drive the -# substrate's `FixtureHost` — a full `EngineHost` double — over the SAME production `EngineHost` seam -# `build_runtime_hosted` takes, so the admin-audit row and the breaker/lease behavior are read back -# through the host, not reimplemented in the harness). Neither feature is default-on, so the default / -# prod build never compiles this bin. -[[bin]] -name = "voice-conform" -path = "src/bin/voice-conform.rs" -required-features = ["runtime", "test-support"] - -[dependencies] -# THE SEAL: busbar-core is NOT a dependency of this plugin at all — not in the shipped build and not in -# its test build. Every build compiles with NO busbar-core in its closure; the neutral surface the -# plane names is `busbar-substrate` (+ `busbar-api`), and its tests drive the substrate's fixture host. -# The substrate crate: the neutral shared surface the plugin names instead of busbar-core — the plane -# and protocol registry descriptors (`PlaneDecl`, `ProtocolDecl`), the diagnostics namespace, the -# proxy/egress constants the `ProtocolDecl` fields carry. -busbar-kernel = { path = "../busbar-kernel", default-features = false } -# The contract face. The plane's REGISTRATION item, `PLANE_DECLARATION`, is a -# `busbar_contract::plane::PlaneDeclaration` — plain data naming no kernel type — so the declaration -# a plane states about itself survives the fold into `busbar-plane-streaming` without a kernel edge. -busbar-contract = { path = "../busbar-contract" } -# `axum::http::StatusCode` appears in the `ProtocolDecl` field set (quota / auth-failure statuses). -axum = { workspace = true } -# The plane's OWN media-frame carrier: `IrAudioFrame.media` is opaque `bytes::Bytes` (`BUSBAR-1.6.0.md` #18/#45 identity IR). -bytes = { workspace = true } -# THE DIALECT CODEC's (de)serialization surface. The plane's IR is busbar-owned, but the OpenAI -# Realtime wire is JSON: the `DuplexReader`/`DuplexWriter` pair parses/produces JSON event frames, so -# the codec derives serde on its OWN wire structs and hand-maps them to/from the neutral IR. Opaque -# JSON payloads (tool arguments, tool definitions, tool_choice) ride as `serde_json::Value` — the -# plane never reshapes them, it carries them verbatim (the `BUSBAR-1.6.0.md` #18/#45 tool moat normalizes correlation, not -# the argument bytes). -serde = { workspace = true } -serde_json = { workspace = true } -# THE `streams:` CONFIG-SECTION reader (`config.rs`): `PLANE_HOOKS.parse_section` deserializes the -# owned section from a `serde_yaml::Value` through the plane's own typed `StreamsCfg`, exactly as the -# MCP/A2A `parse_section` hooks do. UNCONDITIONAL (config parse/validate is needed in the default -# feature-off build too), so it is a plain dep rather than a `runtime`-gated one. -serde_yaml = { workspace = true } - -# ── THE T2 RUNTIME's async surface — OPTIONAL, pulled ONLY by the `runtime` feature (OFF by default). -# The default / prod `cargo build -p busbar-voice` compiles with NONE of these in its closure: the -# IR + declarations need no async runtime. The live duplex session pump (`session.rs`), the -# metering lease, and the two topologies name `async_trait`, the `futures` Stream/Sink vocabulary the -# neutral `serve_messages` pump is generic over, and `tokio` sync/task primitives — so each is an -# OPTIONAL dep the `runtime` feature turns on (HARD RULE 4: the async surface stays feature-gated, not compiled into the default build). -async-trait = { workspace = true, optional = true } -futures = { workspace = true, optional = true } -# `net` + `rt-multi-thread`: the voice CONFORMANCE HARNESS's `provider-dial` / `gemini-live-route` legs -# stand up a tiny in-process loopback WS provider (a stand-in for a real realtime provider) and drive -# `topology::dial_provider` at it end to end — the harness needs a real listener + a runtime that can -# accept a connection while the dial races it, not just the single-threaded task/timer primitives the -# rest of the plane's async surface needs. The PRODUCTION runtime never binds a listener of its own -# (see `crate::mount::voice_start`), so this is pure harness/test weight, still `runtime`-gated. -tokio = { workspace = true, features = ["rt", "sync", "macros", "time", "net", "rt-multi-thread"], optional = true } -# THE LOOPBACK PROVIDER's WS ACCEPT SIDE — `topology::dial_provider` (and the substrate egress dialer -# it rides) already pulls `tokio-tungstenite` transitively through `busbar-kernel/runtime`; this is -# the harness's OWN direct dep for the ACCEPT half (`accept_async`), which the substrate crate has no -# reason to expose. Same feature set as the substrate's own use (handshake only, no TLS/connector). -tokio-tungstenite = { workspace = true, optional = true } -# `url` — ONE reading of the deployment's `public_url` into the plane's RFC 8707 audience + metadata URL -# (`crate::mount::absolute`, the A2A `serve::absolute` discipline). OPTIONAL, pulled only by `runtime`. -url = { workspace = true, optional = true } -# `tracing` — the boot `hydrate` / `start` hooks log the durable-session restore outcome (counts, and a -# per-row unreadable warning) exactly as the A2A / MCP boot hooks do. OPTIONAL, pulled only by `runtime` -# (the boot hooks arm with the live session runtime); the default feature-off build wires no boot hook. -tracing = { workspace = true, optional = true } -# The egress request/response vocabulary the concrete HTTPS token minter names to drive the substrate -# egress engine: `http::Request`/`Response`, the `Full` request body, and the `Incoming` body -# reader. OPTIONAL, pulled only by `runtime` (the minter compiles with the rest of the async topology). -http = { workspace = true, optional = true } -http-body-util = { workspace = true, optional = true } -# The neutral telemetry recorder the testkit's in-memory capture installs (`testkit::metrics_capture`). -# The front door itself reports through the host's `request_finished` seam (count + duration, item -# 566) and emits nothing by hand. OPTIONAL, pulled only by `runtime`. -metrics = { workspace = true, optional = true } -# THE PURE HALF OF THIS PLANE, AND THE CODECS TOO NOW. Named for three things. -# (1) The conformance harness and the governed-binding cells pin the tool-reply deadline -# (`busbar_plane_streaming::plane::TOOL_REPLY_DEADLINE_SECS`) against the one declaration the composition -# root's own leg is built from, rather than restating thirty seconds in a second place where the two -# could drift apart in silence. (2) The GOVERNED-CALL PORT itself -# (`busbar_plane_streaming::governed::{GovernedCalls, GovernedSession, ReplyRefusal}`) now lives there, -# beside the correlation key a wait is entered under, and `crate::runtime` re-exports it under its -# old name — so the port is unconditional and the dep cannot be feature-gated behind `test-support`. -# (3) THE CODECS: the duplex IR and the dialect grammars are the plane crate's own `codec` module, -# and this crate re-exports each of those modules under the path its callers already use (`lib.rs`, -# `topology/mod.rs`). The closure this adds to the shipped build is `busbar-contract` + `bytes`, both -# of which this crate already links. No cycle: the plane crate names `busbar-contract` and never -# this crate. -busbar-plane-streaming = { path = "../busbar-plane-streaming" } - -[features] -# THE VOICE SESSION RUNTIME — the T2 live duplex engine (session pump + SessionScope + D2 metering -# lease + both topologies). OFF by default (HARD RULE 4): the default workspace build is UNAFFECTED and -# the crate still compiles with the flag off (IR + declarations only, no async runtime pulled in). Turning it on pulls the -# async closure (`async-trait` / `futures` / `tokio`) and compiles `session`/`topology`. -# `busbar-kernel/runtime` ARMS the neutral full-duplex WS transport (the ingress acceptor + the -# net-guard-pinned egress dialer): the plane SELECTS `Transport::WebSocket` and lets the substrate open -# the socket, so the provider sideband WSS dials THROUGH `net_guard` and the plane holds no socket -# plumbing of its own. Turned on in lock-step with the plane's own async runtime. -runtime = ["dep:async-trait", "dep:futures", "dep:tokio", "dep:tokio-tungstenite", "dep:url", "dep:tracing", "dep:http", "dep:http-body-util", "dep:metrics", "busbar-kernel/runtime"] -# THE OPENAPI-SCHEMA EDGE, mirroring busbar-mcp/busbar-a2a: `PlaneDecl::openapi_schemas` is a field the -# substrate struct gates on its own `openapi-schema` feature, so this forwards to substrate to keep the -# `#[cfg(feature = "openapi-schema")]` field site and the struct definition in lock-step. The plane -# contributes no schemas (the field is `None`), so no `schemars` dep is pulled. -openapi-schema = ["busbar-kernel/openapi-schema"] -# THE HOST-DRIVING TEST SURFACE: reveals THIS CRATE'S OWN `testkit` (src/testkit) — the in-memory -# fixture host, the loopback HTTP provider, the metrics capture — to the cells that drive a plane-host -# seam (the breaker, hook-gate/tap, metrics, SDP-broker and billing-ledger batteries) and to the -# `voice-conform` binary. Those three doubles USED to live in `busbar_kernel::testkit`; that module is -# deleted (1.6.0 Locked Decision #33 — "the kernel never tests a plugin"), so a plane carries the -# doubles it drives. They still name only the neutral plane-host seam production uses, so this pulls -# no core test fixture: the `busbar-kernel/test-support` forward below is for the OTHER cells in this -# crate that build a core `TestApp`, not for the doubles. -test-support = ["busbar-kernel/test-support"] - -[dev-dependencies] -# None beyond the workspace crates above: the test surface is the `test-support` feature. diff --git a/crates/busbar-voice/src/bin/voice-conform.rs b/crates/busbar-voice/src/bin/voice-conform.rs deleted file mode 100644 index d33f279a09..0000000000 --- a/crates/busbar-voice/src/bin/voice-conform.rs +++ /dev/null @@ -1,2407 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE VOICE CONFORMANCE HARNESS — the real driver behind the `testing/voice-conformance/` battery. -//! -//! Each leg (`spec-per-dialect`, `replay`, `cross-parity`, the three composition legs, and -//! `governance`) shells out to ONE subcommand of this bin, which reuses THIS crate's production -//! codecs ([`OpenAiRealtimeCodec`] / [`GeminiLiveCodec`]), the T2 runtime ([`SessionCore`] / -//! kernel session-meter hard-close) and the plane's own composition seams to decode / encode the -//! captured fixtures and diff. The legs never reimplement a codec — or a gate — in shell: every -//! conformance claim below is proven against the plane's own code. -//! -//! Output contract (the leg runner greps `^RESULT `): each asserted item prints exactly one line -//! RESULT -//! Non-`RESULT` lines (`NOTE:` / `SUBITEM`) are ignored by the runner and used to record documented -//! sub-item gaps that must stay HONESTLY PENDING rather than be dressed as a green. - -use busbar_kernel::plane_host::{BreakerHost, DispatchScope, EngineHost}; -use busbar_kernel::store::PlaneBreakers; -use busbar_plane_streaming::session::TurnCounters; -use busbar_voice::ir::usage::IrDuplexUsage; -use busbar_voice::ir::{ - DecodeState, DuplexReader, DuplexWriter, GeminiLiveCodec, IrClientEvent, IrDuplexControl, - IrDuplexTool, IrServerEvent, OpenAiRealtimeCodec, WireEvent, -}; -use busbar_voice::runtime::{ - build_runtime_hosted, Carrier, EchoToolExecutor, SessionCore, TurnMeter, TurnVerdict, - VoiceRuntime, -}; -use busbar_voice::testkit::fixture_host::FixtureHost; -use busbar_voice::topology::{begin_session, dial_provider, DialProviderError, StartError}; -use bytes::Bytes; -use serde_json::Value; -use std::collections::BTreeMap; -use std::fs; -use std::path::Path; -use std::sync::Arc; - -// ── wire helpers ────────────────────────────────────────────────────────────────────────────────── - -fn wire_of(v: &Value) -> WireEvent { - WireEvent(Bytes::from( - serde_json::to_vec(v).expect("serialize wire value"), - )) -} - -fn val_of(w: &WireEvent) -> Value { - serde_json::from_slice(&w.0).unwrap_or(Value::Null) -} - -/// A decoded fixture: the direction the codec recognized it in, plus the IR it produced. Direction is -/// inferred by trying uplink then downlink — the two dispatch tables never share a wire tag, so at most -/// one is non-empty. `Empty` means the codec mapped the frame to nothing (a documented drop+warn). -enum Decoded { - Up(Vec), - Down(Vec), - Empty, -} - -fn decode(codec: &C, v: &Value) -> Decoded { - let mut su = DecodeState::default(); - let up = codec.read_up(wire_of(v), &mut su); - if !up.is_empty() { - return Decoded::Up(up); - } - let mut sd = DecodeState::default(); - let down = codec.read_down(wire_of(v), &mut sd); - if !down.is_empty() { - return Decoded::Down(down); - } - Decoded::Empty -} - -// ── the shared normal form ────────────────────────────────────────────────────────────────────────── -// -// Both direction-split IR enums collapse onto one `Norm` list so `spec`, `replay` and `cross-parity` -// can reason about concepts uniformly (and so a bridged, re-decoded stream compares to its source). - -#[derive(Clone, Debug, PartialEq)] -enum Norm { - Config(String), // essentials only: instructions|voice|tools_count|max (the survivable fields) - ConfigModalities(Vec), - Connect, - AudioUp(Vec), - AudioDown(Vec), - AudioDone, - Item(Value), - SpeechStart, - SpeechStop, - Truncate(u64), // audio_played_ms — precision that does NOT survive toward Gemini - Commit, - Clear, - ResponseCreate, - ResponseCancel, - ItemDelete, - Usage(u64, u64, u64, u64), // audio_in, audio_out, text_in, text_out (cached rarely survives) - RateLimits, - Error(String, String), - ToolOpen(String, String), // id, name - ToolArgs(String, Value), // id, parsed args - ToolClose(String), // id - ToolResult(String, Value), // id, parsed output -} - -fn cfg_essentials(config: &busbar_voice::ir::SessionConfig) -> Norm { - Norm::Config(format!( - "{:?}|{:?}|{}|{:?}", - config.instructions, - config.voice, - config.tools.len(), - config.max_output_tokens - )) -} - -fn norm_tool(t: &IrDuplexTool) -> Norm { - match t { - IrDuplexTool::CallOpen { call_id, name, .. } => { - Norm::ToolOpen(call_id.clone(), name.clone()) - } - IrDuplexTool::CallArgs { - call_id, - json_delta, - .. - } => Norm::ToolArgs( - call_id.clone(), - serde_json::from_slice(json_delta).unwrap_or(Value::Null), - ), - IrDuplexTool::CallClose { call_id, .. } => Norm::ToolClose(call_id.clone()), - IrDuplexTool::CallResult { - call_id, output, .. - } => Norm::ToolResult( - call_id.clone(), - serde_json::from_slice(output).unwrap_or(Value::Null), - ), - } -} - -fn norm_up(evs: &[IrClientEvent]) -> Vec { - let mut out = Vec::new(); - for e in evs { - match e { - IrClientEvent::AudioFrame(f) => out.push(Norm::AudioUp(f.media.to_vec())), - IrClientEvent::Tool(t) => out.push(norm_tool(t)), - IrClientEvent::Control(c) => match c { - IrDuplexControl::SessionConfigure { config } => { - out.push(cfg_essentials(config)); - out.push(Norm::ConfigModalities(config.modalities.clone())); - } - IrDuplexControl::ItemCreate { item } => out.push(Norm::Item(item.clone())), - IrDuplexControl::ItemTruncate { - audio_played_ms, .. - } => out.push(Norm::Truncate(*audio_played_ms)), - IrDuplexControl::InputAudioCommit => out.push(Norm::Commit), - IrDuplexControl::InputAudioClear => out.push(Norm::Clear), - IrDuplexControl::ResponseCreate { .. } => out.push(Norm::ResponseCreate), - IrDuplexControl::ResponseCancel => out.push(Norm::ResponseCancel), - IrDuplexControl::ItemDelete { .. } => out.push(Norm::ItemDelete), - }, - } - } - out -} - -fn norm_down(evs: &[IrServerEvent]) -> Vec { - let mut out = Vec::new(); - for e in evs { - match e { - IrServerEvent::SessionCreated { .. } => out.push(Norm::Connect), - IrServerEvent::Tool(t) => out.push(norm_tool(t)), - IrServerEvent::SpeechStarted { .. } => out.push(Norm::SpeechStart), - IrServerEvent::SpeechStopped { .. } => out.push(Norm::SpeechStop), - IrServerEvent::AudioFrame(f) => out.push(Norm::AudioDown(f.media.to_vec())), - IrServerEvent::AudioDone { .. } => out.push(Norm::AudioDone), - IrServerEvent::Usage(u) => { - out.push(Norm::Usage(u.audio_in, u.audio_out, u.text_in, u.text_out)) - } - IrServerEvent::RateLimits => out.push(Norm::RateLimits), - IrServerEvent::Error { code, message } => { - out.push(Norm::Error(code.clone(), message.clone())) - } - } - } - out -} - -// ── round-trip (re-encode then re-decode) ────────────────────────────────────────────────────────── - -fn reencode_up( - codec: &C, - ir1: &[IrClientEvent], -) -> Vec { - let mut st = DecodeState::default(); - let mut ir2 = Vec::new(); - // ONE session state for the whole re-encode: the writers accumulate a streamed tool call's - // arguments on it, exactly as a live session's would. - let mut wst = DecodeState::default(); - for e in ir1 { - // A concept the dialect has no verb for frames NOTHING; it re-decodes to nothing too. - if let Some(w) = codec.write_up(e.clone(), &mut wst) { - ir2.extend(codec.read_up(w, &mut st)); - } - } - ir2 -} - -fn reencode_down( - codec: &C, - ir1: &[IrServerEvent], -) -> Vec { - let mut st = DecodeState::default(); - let mut ir2 = Vec::new(); - let mut wst = DecodeState::default(); - for e in ir1 { - // A downlink event that is not a frame on its own (a held tool-argument fragment) writes - // nothing; the call it belongs to frames whole at its close. - if let Some(w) = codec.write_down(e.clone(), &mut wst) { - ir2.extend(codec.read_down(w, &mut st)); - } - } - ir2 -} - -/// A call-collapsed fingerprint: tool events are merged BY call_id (an atomic Gemini `toolCall` decodes -/// to a streamed open/args/close triple, and the stateless writer re-frames per event — so frame counts -/// legitimately differ across a re-encode; the correlation join is what must survive, not the arity). -/// Everything else is compared verbatim (audio payloads, config essentials, usage, items). -/// One correlated tool call, keyed by call_id: `(name, args, result)`, each present only once its -/// event has been seen (open→name, args, result). -type CallEntry = (Option, Option, Option); - -#[derive(Debug, Default, PartialEq)] -struct Fingerprint { - calls: BTreeMap, // id -> (name, args, result) - other: Vec, -} - -fn fingerprint(norms: &[Norm]) -> Fingerprint { - let mut fp = Fingerprint::default(); - for n in norms { - match n { - Norm::ToolOpen(id, name) => { - let e = fp.calls.entry(id.clone()).or_default(); - let nm = if name.is_empty() { - None - } else { - Some(name.clone()) - }; - if e.0.is_none() { - e.0 = nm; - } - } - Norm::ToolArgs(id, args) => { - let e = fp.calls.entry(id.clone()).or_default(); - if e.1.is_none() && !args.is_null() { - e.1 = Some(args.clone()); - } - } - Norm::ToolClose(id) => { - fp.calls.entry(id.clone()).or_default(); - } - Norm::ToolResult(id, out) => { - let e = fp.calls.entry(id.clone()).or_default(); - if e.2.is_none() { - e.2 = Some(out.clone()); - } - } - other => fp.other.push(other.clone()), - } - } - fp -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEG 1 — spec-per-dialect: every dialect fixture round-trips stably through the shared IR. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -/// Fixtures that legitimately decode to NOTHING, with the reason. Every arity-0 fixture MUST be listed -/// here (a documented drop+warn) or it is a genuine unexercised gap and fails RED. -fn drop_reason(dialect: &str, fixture: &str) -> Option<&'static str> { - match (dialect, fixture) { - // Gemini concepts with no shared-IR home — the codec's documented drop+warn set. - ("gemini", "goAway.json") => { - Some("gemini_go_away: no OpenAI advance-disconnect twin (drop+warn)") - } - ("gemini", "toolCallCancellation.json") => { - Some("gemini_tool_call_cancellation: no OpenAI server-driven cancel (drop+warn)") - } - ("gemini", "serverContent.inputTranscription.json") => { - Some("input transcription side-channel: no shared IR home (drop+warn)") - } - ("gemini", "serverContent.outputTranscription.json") => { - Some("output transcription side-channel: no shared IR home (drop+warn)") - } - // NB: `realtimeInput.audioStreamEnd` is NOT here — it is no longer a drop. It maps to the - // shared `InputAudioCommit` (OpenAI's `input_audio_buffer.commit` twin), so it decodes to a - // real IR event and is exercised as an IR-fixpoint-stable fixture, not a documented drop. - _ => None, - } -} - -fn spec(dialect: &str, dir: &Path) -> i32 { - let mut fixtures: Vec = fs::read_dir(dir) - .unwrap_or_else(|e| panic!("read fixtures dir {}: {e}", dir.display())) - .filter_map(|e| e.ok()) - .map(|e| e.file_name().to_string_lossy().to_string()) - .filter(|n| n.ends_with(".json")) - .collect(); - fixtures.sort(); - assert!( - !fixtures.is_empty(), - "no .json fixtures under {}", - dir.display() - ); - - let mut fails = 0; - for f in &fixtures { - let v: Value = serde_json::from_str(&fs::read_to_string(dir.join(f)).unwrap()) - .unwrap_or_else(|e| panic!("parse fixture {f}: {e}")); - let (verdict, detail) = match dialect { - "openai" => spec_one(&OpenAiRealtimeCodec, dialect, f, &v), - "gemini" => spec_one(&GeminiLiveCodec, dialect, f, &v), - other => panic!("unknown dialect {other}"), - }; - if verdict == "FAIL" { - fails += 1; - } - println!("RESULT {dialect} {verdict} {f} — {detail}"); - } - if fails > 0 { - 1 - } else { - 0 - } -} - -fn spec_one( - codec: &C, - dialect: &str, - fixture: &str, - v: &Value, -) -> (&'static str, String) { - match decode(codec, v) { - Decoded::Up(ir1) => { - let ir2 = reencode_up(codec, &ir1); - spec_verdict(&norm_up(&ir1), &norm_up(&ir2), ir1.len()) - } - Decoded::Down(ir1) => { - let ir2 = reencode_down(codec, &ir1); - spec_verdict(&norm_down(&ir1), &norm_down(&ir2), ir1.len()) - } - Decoded::Empty => match drop_reason(dialect, fixture) { - Some(reason) => ("PASS", format!("documented drop — {reason}")), - None => ( - "FAIL", - "decoded to NO IR events and is not a documented drop — unexercised".to_string(), - ), - }, - } -} - -fn spec_verdict(n1: &[Norm], n2: &[Norm], arity: usize) -> (&'static str, String) { - if n1 == n2 { - return ("PASS", format!("IR-fixpoint stable ({arity} IR event(s))")); - } - // Multi-event atomic frames (Gemini toolCall) legitimately differ in arity across a per-event - // re-encode; the correlation-collapsed fingerprint is the guarantee the codec actually makes. - if fingerprint(n1) == fingerprint(n2) { - return ( - "PASS", - format!("IR-fixpoint stable by correlation fingerprint (atomic expansion, {arity} event(s))"), - ); - } - ("FAIL", format!("round-trip diverged: {n1:?} != {n2:?}")) -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEG 2 — replay: a captured transcript re-derives the expected ordered IR concept skeleton. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -/// Decode a whole `transcript.jsonl` through ONE session `DecodeState`, honoring each line's `dir` -/// (client → uplink, server → downlink, meta → skipped). Returns the ordered concept tags plus a count -/// of frames that re-encoded to valid JSON (the end-to-end write proof). -fn replay_decode( - codec: &C, - lines: &[Value], -) -> (Vec<&'static str>, usize, usize) { - let mut st = DecodeState::default(); - // The write seam's own session state (a streamed tool call accumulates on it). - let mut wst = DecodeState::default(); - let mut tags: Vec<&'static str> = Vec::new(); - let mut decoded = 0usize; - let mut reencoded = 0usize; - for line in lines { - let dir = line.get("dir").and_then(Value::as_str).unwrap_or("meta"); - let ev = match line.get("event") { - Some(e) => e, - None => continue, // meta line - }; - match dir { - "client" => { - let irs = codec.read_up(wire_of(ev), &mut st); - for ir in &irs { - decoded += 1; - // A dropped concept frames nothing at all — that is not a re-encode. - if codec - .write_up(ir.clone(), &mut wst) - .is_some_and(|w| !val_of(&w).is_null()) - { - reencoded += 1; - } - } - tags.extend(norm_up(&irs).iter().map(tag)); - } - "server" => { - let irs = codec.read_down(wire_of(ev), &mut st); - for ir in &irs { - decoded += 1; - if codec - .write_down(ir.clone(), &mut wst) - .is_some_and(|w| !val_of(&w).is_null()) - { - reencoded += 1; - } - } - tags.extend(norm_down(&irs).iter().map(tag)); - } - _ => {} - } - } - (tags, decoded, reencoded) -} - -fn tag(n: &Norm) -> &'static str { - match n { - Norm::Config(_) => "config", - Norm::ConfigModalities(_) => "config-modalities", - Norm::Connect => "connect", - Norm::AudioUp(_) => "audio-in", - Norm::AudioDown(_) => "audio-out", - Norm::AudioDone => "audio-done", - Norm::Item(_) => "item", - Norm::SpeechStart => "speech-start", - Norm::SpeechStop => "speech-stop", - Norm::Truncate(_) => "truncate", - Norm::Commit => "commit", - Norm::Clear => "clear", - Norm::ResponseCreate => "response-create", - Norm::ResponseCancel => "cancel", - Norm::ItemDelete => "item-delete", - Norm::Usage(..) => "usage", - Norm::RateLimits => "rate-limits", - Norm::Error(..) => "error", - Norm::ToolOpen(..) => "tool-open", - Norm::ToolArgs(..) => "tool-args", - Norm::ToolClose(..) => "tool-close", - Norm::ToolResult(..) => "tool-result", - } -} - -/// Assert `expected` appears, in order, as a subsequence of `tags`. Returns the first missing tag. -fn ordered_subsequence(tags: &[&str], expected: &[&str]) -> Result<(), String> { - let mut i = 0; - for want in expected { - match tags[i..].iter().position(|t| t == want) { - Some(off) => i += off + 1, - None => { - return Err(format!( - "expected concept '{want}' not found after position {i} in {tags:?}" - )) - } - } - } - Ok(()) -} - -fn replay(dir: &Path) -> i32 { - let mut fails = 0; - // OpenAI transcript: the full client↔server session, richly decoded. - let oa = read_jsonl(&dir.join("openai").join("transcript.jsonl")); - let (otags, od, or_) = replay_decode(&OpenAiRealtimeCodec, &oa); - let oexp = [ - "config", - "connect", - "audio-in", - "speech-start", - "tool-args", - "tool-close", - "tool-result", - "audio-out", - "speech-start", - "cancel", - ]; - match ordered_subsequence(&otags, &oexp) { - Ok(()) => println!( - "RESULT default PASS openai-transcript — {od} IR events, {or_} re-encoded, skeleton [config→connect→audio-in→barge→tool→result→audio-out→barge-in→cancel] in order" - ), - Err(e) => { - fails += 1; - println!("RESULT default FAIL openai-transcript — {e}"); - } - } - - // Gemini transcript: same logical conversation in the Gemini idiom. - let ge = read_jsonl(&dir.join("gemini").join("transcript.jsonl")); - let (gtags, gd, gr) = replay_decode(&GeminiLiveCodec, &ge); - let gexp = [ - "config", - "connect", - "audio-in", - "tool-open", - "tool-args", - "tool-close", - "tool-result", - "audio-out", - "speech-start", - "audio-done", - "usage", - ]; - match ordered_subsequence(>ags, &gexp) { - Ok(()) => println!( - "RESULT default PASS gemini-transcript — {gd} IR events, {gr} re-encoded, skeleton [config→connect→audio-in→tool→result→audio-out→barge-in→turn-complete→usage] in order" - ), - Err(e) => { - fails += 1; - println!("RESULT default FAIL gemini-transcript — {e}"); - } - } - println!( - "NOTE: gemini uplink audio frames (realtimeInput.audio{{}}) now decode and are asserted in the \ - skeleton above — the codec reads BOTH the GA realtimeInput.audio{{}} blob and the legacy \ - realtimeInput.mediaChunks[] array." - ); - if fails > 0 { - 1 - } else { - 0 - } -} - -fn read_jsonl(path: &Path) -> Vec { - fs::read_to_string(path) - .unwrap_or_else(|e| panic!("read {}: {e}", path.display())) - .lines() - .filter(|l| !l.trim().is_empty()) - .map(|l| serde_json::from_str(l).unwrap_or_else(|e| panic!("parse jsonl line: {e}"))) - .collect() -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEG 3 — cross-parity: read(A) → IR → write(B) → IR must preserve shared-concept fields, and every -// asymmetry-table row must be exercised as a documented drop. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -/// Bridge one fixture FROM codec A to codec B: decode with A, re-frame each event onto B's wire, decode -/// B's wire back. Returns (source Norms, bridged-and-re-decoded Norms). Empty source ⇒ both empty. -fn bridge(from: &A, to: &B, v: &Value) -> (Vec, Vec) -where - A: DuplexReader, - B: DuplexReader + DuplexWriter, -{ - match decode(from, v) { - Decoded::Up(ir) => { - let n1 = norm_up(&ir); - let mut st = DecodeState::default(); - let mut wst = DecodeState::default(); - let mut n2 = Vec::new(); - for e in ir { - // The destination dialect may have no verb for the concept: it frames nothing, and - // nothing is what the bridged side then carries (the map's documented drop). - if let Some(w) = to.write_up(client_from_norm_passthrough(e), &mut wst) { - n2.extend(norm_up(&to.read_up(w, &mut st))); - } - } - (n1, n2) - } - Decoded::Down(ir) => { - let n1 = norm_down(&ir); - let mut st = DecodeState::default(); - let mut wst = DecodeState::default(); - let mut n2 = Vec::new(); - for e in ir { - // An event that is not a frame on its own (a held tool-argument fragment) bridges - // nothing by itself; its call arrives whole at the close. - if let Some(w) = to.write_down(e, &mut wst) { - n2.extend(norm_down(&to.read_down(w, &mut st))); - } - } - (n1, n2) - } - Decoded::Empty => (Vec::new(), Vec::new()), - } -} - -// `write_up`/`write_down` consume the IR by value; the closures above already own their events, so this -// is just an identity used to keep the generic bridge readable. -fn client_from_norm_passthrough(e: IrClientEvent) -> IrClientEvent { - e -} - -/// Bridge a concept's fixtures AS ONE EXCHANGE — one source session, one destination session — for the -/// concepts whose map transform is stated across EVENTS rather than within one. A streamed tool call is -/// the case in point: the map says the bridge toward Gemini must ACCUMULATE the argument deltas and -/// parse them into the atomic `args` object, so a lone `…arguments.delta` frame legitimately bridges to -/// nothing on its own — the call frames at its `…arguments.done`. Judging that fragment alone would -/// demand the very mistranslation the map forbids (a call dispatched with arguments nobody sent). -fn bridge_exchange(from: &A, to: &B, vals: &[Value]) -> (Vec, Vec) -where - A: DuplexReader, - B: DuplexReader + DuplexWriter, -{ - let (mut rst, mut wst, mut bst) = ( - DecodeState::default(), - DecodeState::default(), - DecodeState::default(), - ); - let (mut n1, mut n2) = (Vec::new(), Vec::new()); - for v in vals { - let up = from.read_up(wire_of(v), &mut rst); - if !up.is_empty() { - n1.extend(norm_up(&up)); - for e in up { - if let Some(w) = to.write_up(e, &mut wst) { - n2.extend(norm_up(&to.read_up(w, &mut bst))); - } - } - continue; - } - let down = from.read_down(wire_of(v), &mut rst); - n1.extend(norm_down(&down)); - for e in down { - if let Some(w) = to.write_down(e, &mut wst) { - n2.extend(norm_down(&to.read_down(w, &mut bst))); - } - } - } - (n1, n2) -} - -fn cross( - from: &A, - to: &B, - from_d: &str, - to_d: &str, - oa_dir: &Path, - ge_dir: &Path, - map: &Value, -) -> i32 -where - A: DuplexReader + DuplexWriter, - B: DuplexReader + DuplexWriter, -{ - let slice = pair_slice(from_d, to_d); - let mut fails = 0; - let dir_for = |d: &str| if d == "openai" { oa_dir } else { ge_dir }; - - // ── shared concepts: the load-bearing fields must survive the bridge (correlation fingerprint) ── - let concepts = map["concepts"].as_array().expect("map.concepts array"); - for c in concepts { - let concept = c["concept"].as_str().unwrap_or("?"); - let fixtures = c[from_d]["fixtures"] - .as_array() - .cloned() - .unwrap_or_default(); - // Every FROM-side fixture this concept names, in the map's order. - let present: Vec<(String, Value)> = fixtures - .iter() - .filter_map(|fx| { - let name = fx.as_str().unwrap_or(""); - if name.is_empty() || name.ends_with(".jsonl") { - return None; - } - let path = dir_for(from_d).join(name); - if !path.exists() { - return None; - } - Some(( - name.to_string(), - serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(), - )) - }) - .collect(); - // pick the first FROM-side fixture that actually decodes - let mut chosen: Option<(String, Vec, Vec)> = None; - for (name, v) in &present { - let (n1, n2) = bridge(from, to, v); - if !n1.is_empty() { - chosen = Some((name.clone(), n1, n2)); - break; - } - } - // A concept whose transform is stated ACROSS events (the streamed⟷atomic tool call) can bridge - // one of its frames to nothing on its own; judge those fixtures as the single exchange the map - // describes before calling anything lost. - if let Some((_, _, n2)) = &chosen { - if n2.is_empty() && present.len() > 1 { - let vals: Vec = present.iter().map(|(_, v)| v.clone()).collect(); - let (m1, m2) = bridge_exchange(from, to, &vals); - let names: Vec<&str> = present.iter().map(|(n, _)| n.as_str()).collect(); - chosen = Some((names.join("+"), m1, m2)); - } - } - match chosen { - None => { - // No FROM-side fixture decodes. Classify precisely so the one GENUINE codec gap is - // never buried among the legitimate documented drops. Never faked as a pass. - println!( - "SUBITEM {slice}:{concept} PENDING — {}", - none_reason(concept, from_d) - ); - } - Some((name, n1, n2)) => { - let (verdict, detail) = survives(concept, &n1, &n2, from_d, to_d); - if verdict == "FAIL" { - fails += 1; - } - println!("RESULT {slice} {verdict} shared:{name} — {detail}"); - } - } - } - - // ── asymmetry: every one-dialect-only row exercised as a documented drop/handling ── - let asym = map["asymmetry"].as_array().expect("map.asymmetry array"); - for row in asym { - let id = row["id"].as_str().unwrap_or("?"); - let dialect = row["dialect"].as_str().unwrap_or("?"); - let fixture = row["fixture"].as_str().unwrap_or(""); - // A row is EXERCISED as a drop only when we bridge OUT of its origin dialect to the other one. - // Diagonal pairs (oo/gg) and the mismatched-origin cross pair record it as covered-elsewhere. - if from_d != dialect || from_d == to_d { - println!("RESULT {slice} PASS asym:{id} — not this pair's drop direction (origin={dialect}); exercised in the {dialect}→other pair"); - continue; - } - let name = fixture - .strip_prefix(&format!("{dialect}/")) - .unwrap_or(fixture); - let path = dir_for(dialect).join(name); - if !path.exists() { - fails += 1; - println!("RESULT {slice} FAIL asym:{id} — fixture {fixture} missing"); - continue; - } - let v: Value = serde_json::from_str(&fs::read_to_string(&path).unwrap()).unwrap(); - let (n1, n2) = bridge(from, to, &v); - let (verdict, detail) = asym_drop(id, &n1, &n2); - if verdict == "FAIL" { - fails += 1; - } - println!("RESULT {slice} {verdict} asym:{id} — {detail}"); - } - - if fails > 0 { - 1 - } else { - 0 - } -} - -/// Precise reason a concept has no decoding FROM-side fixture, so the report separates the ONE genuine -/// codec gap from the several legitimate documented drops / dialect-only concepts. -fn none_reason(concept: &str, from_d: &str) -> &'static str { - match (concept, from_d) { - // NOTE: "input audio frame" for gemini is no longer a codec gap — realtimeInput.json (GA - // realtimeInput.audio{}) now decodes, so the go/gg pairs take the RESULT-PASS branch and this - // reason is never reached for it. - ("input turn commit / end-of-audio", "gemini") => { - "documented drop — gemini audioStreamEnd is dropped by the codec (map aspires to commit-mapping)" - } - ("server-side VAD speech boundary", "gemini") => { - "dialect-only concept — Gemini has no explicit server-VAD boundary fixture; exercised as the openai_speech_boundary asymmetry in og" - } - ("input transcription", _) | ("output transcription", _) => { - "documented side-channel drop+warn — no shared IR home; the same-dialect round-trip is exercised in spec-per-dialect" - } - _ => "no decoding fixture on the source side (documented drop / exercised in the reverse pair)", - } -} - -fn pair_slice(from: &str, to: &str) -> &'static str { - match (from, to) { - ("openai", "openai") => "oo", - ("openai", "gemini") => "og", - ("gemini", "openai") => "go", - ("gemini", "gemini") => "gg", - _ => "??", - } -} - -/// Do the shared-concept's load-bearing fields survive A→IR→B→IR? Compared on the correlation -/// fingerprint, minus fields the mapping documents as non-survivors (text modality, VAD specifics, -/// truncate ms, session formats) which are excluded from `Fingerprint`/`cfg_essentials`. -fn survives( - concept: &str, - n1: &[Norm], - n2: &[Norm], - from_d: &str, - to_d: &str, -) -> (&'static str, String) { - // Same dialect (oo/gg): the mapping must be identity on the survivable fingerprint. - // Cross dialect: the fingerprint must still match on the shared fields. - let f1 = fingerprint(n1); - let f2 = fingerprint(n2); - if f1 == f2 { - return ( - "PASS", - format!("shared fields survive {from_d}→{to_d} ({concept})"), - ); - } - // Config is a genuine cross-dialect map: only the survivable essentials (instructions/voice/ - // tools/max) are compared; if THOSE match we pass and note the documented drops. - if concept.contains("session") && cfg_from(n1) == cfg_from(n2) && !cfg_from(n1).is_empty() { - return ( - "PASS", - format!("session config essentials survive {from_d}→{to_d} (modalities-text/VAD/format dropped per map)"), - ); - } - // Some concepts are DIRECTIONAL DROPS per the mapping's transform column (e.g. an explicit - // commit is implicit under Gemini auto-VAD; a client truncate/cancel has no Gemini counterpart). - // When the bridge drops the concept entirely in the documented direction, that IS the mapping - // holding — accounted for, not a silent mistranslation. - let f2_empty = f2.calls.is_empty() && f2.other.is_empty(); - if f2_empty && directional_drop(concept, from_d, to_d) { - return ( - "PASS", - format!("{concept}: documented directional drop {from_d}→{to_d} (implicit / no counterpart per map)"), - ); - } - ( - "FAIL", - format!("{concept}: shared fields did not survive {from_d}→{to_d}: {f1:?} != {f2:?}"), - ) -} - -/// Concepts the mapping documents as dropping in a specific direction (the transform column says the -/// counterpart is implicit or absent), so an empty bridge result is the mapping holding, not a loss. -fn directional_drop(concept: &str, _from: &str, to: &str) -> bool { - match (concept, to) { - // Explicit input-audio commit is implicit under Gemini automatic activity detection. - ("input turn commit / end-of-audio", "gemini") => true, - // A client truncate/cancel (OpenAI barge-in) has no Gemini client verb — Gemini surfaces - // barge-in only as server-side interrupted (that survivable path is the "speech boundary" row). - ("barge-in / truncate / cancel", "gemini") => true, - _ => false, - } -} - -fn cfg_from(norms: &[Norm]) -> Vec { - norms - .iter() - .filter_map(|n| match n { - Norm::Config(s) => Some(s.clone()), - _ => None, - }) - .collect() -} - -/// A one-dialect-only concept, bridged toward the other dialect, must be ACCOUNTED FOR — either dropped -/// entirely, or reduced to a benign twin — never silently mistranslated into a wrong concept. The -/// per-id check states exactly what "accounted for" means for that row. -fn asym_drop(id: &str, n1: &[Norm], n2: &[Norm]) -> (&'static str, String) { - let has = |ns: &[Norm], pred: &dyn Fn(&Norm) -> bool| ns.iter().any(pred); - match id { - // Dropped-at-decode (source already empty) or dropped-on-bridge (bridged empty/benign). - "openai_buffer_clear" => drop_if( - n2.iter().all(|n| !matches!(n, Norm::Clear)), - "clear has no Gemini twin — dropped", - ), - "openai_response_overrides" => drop_if( - n2.iter().all(|n| !matches!(n, Norm::ResponseCreate)), - "per-response overrides dropped (Gemini is setup-time only)", - ), - "openai_truncate_precision" => drop_if( - n2.iter().all(|n| !matches!(n, Norm::Truncate(_))), - "sample-accurate truncate dropped (Gemini interrupted carries no ms)", - ), - "openai_structured_error" => drop_if( - n2.iter().all(|n| !matches!(n, Norm::Error(..))), - "structured error dropped toward Gemini (WS close codes instead)", - ), - "openai_event_id" | "openai_noise_reduction" => { - // Both live inside a session.update; the codec never lifts them into IR — so they are - // absent from the SOURCE IR already. Prove the bridged config carries no such concept. - drop_if( - true, - "field never enters the IR (dropped at decode); bridged config omits it", - ) - } - "openai_semantic_vad" | "openai_g711" => { - // The session bridges (config survives), but the semantic_vad / g711 SPECIFICS do not: - // the Gemini setup has no semantic eagerness and no g711 format. Config still present. - drop_if( - has(n2, &|n| matches!(n, Norm::Config(_))), - "config bridges; semantic_vad/g711 specifics dropped per map", - ) - } - "openai_speech_boundary" => { - // speech_started maps onto Gemini's interrupted (a barge-in), but the ms offset is lost. - drop_if( - has(n2, &|n| matches!(n, Norm::SpeechStart)) || n1.is_empty(), - "boundary maps to interrupted; ms offset dropped", - ) - } - "openai_uplink_rate" => { - // The uplink audio BYTES survive verbatim; what does not happen is the resample Gemini's - // 16 kHz input wants. The bridge states the rate the bytes are really in, so the gap is - // visible on the wire rather than hidden behind a mimeType that names a rate nobody sent. - drop_if( - n1.iter() - .zip(n2.iter()) - .all(|(a, b)| matches!((a, b), (Norm::AudioUp(x), Norm::AudioUp(y)) if x == y)) - && !n2.is_empty(), - "uplink audio bridges verbatim at its true rate; the resample toward Gemini's 16 kHz input is not performed", - ) - } - "gemini_go_away" | "gemini_tool_call_cancellation" => drop_if( - n1.is_empty() && n2.is_empty(), - "no OpenAI twin — dropped at decode (drop+warn)", - ), - "gemini_generation_complete" => { - // turnComplete → AudioDone survives; the generationComplete distinction is collapsed away. - drop_if( - has(n2, &|n| matches!(n, Norm::AudioDone)), - "turnComplete→AudioDone survives; generationComplete collapsed", - ) - } - "gemini_audio_stream_end" => drop_if( - has(n1, &|n| matches!(n, Norm::Commit)) && has(n2, &|n| matches!(n, Norm::Commit)), - "audioStreamEnd ↔ input_audio_buffer.commit: the end-of-uplink turn survives cross-dialect", - ), - "gemini_setup_complete" => { - // setupComplete → SessionCreated (the ack survives); the GATE semantics are runtime-only. - drop_if( - has(n2, &|n| matches!(n, Norm::Connect)), - "ack maps to session.created; gate semantics are runtime-only, not IR", - ) - } - other => ("FAIL", format!("no asymmetry handler for id '{other}'")), - } -} - -fn drop_if(cond: bool, msg: &str) -> (&'static str, String) { - if cond { - ("PASS", format!("accounted for — {msg}")) - } else { - ("FAIL", format!("NOT accounted for — {msg}")) - } -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEG 4 — governance: the 5 vision checkpoints, probed over the real runtime. NOT a conformance result. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -fn usage_frame(audio_out: u64) -> Value { - serde_json::json!({ - "type": "response.done", - "response": { "usage": { - "total_tokens": audio_out, - "output_token_details": { "audio_tokens": audio_out }, - }}, - }) -} - -fn governance(checkpoint: &str) -> i32 { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime"); - let (verdict, detail) = rt.block_on(async move { - match checkpoint { - "D2-hard-close-on-exhaustion" => gov_d2().await, - "V1-barge-in-preemption" => gov_v1().await, - "V2-turn-budget-enforcement" => gov_v2(), - "V3-metering-lease-settled" => gov_v3().await, - "V4-dialect-downscope" => gov_v4(), - other => ("FAIL", format!("unknown checkpoint '{other}'")), - } - }); - println!("RESULT {checkpoint} {verdict} {detail}"); - // Governance NEVER gates conformance — always exit 0; the observation is the RESULT line above. - 0 -} - -/// A downlink-facing core whose turns go to the kernel session account of the probe key over a -/// governed fixture host, whose budget view is ONE bucket of `limit` counts (`None`: uncapped) — so the -/// D2/V probes exercise the real report-turn → kernel verdict → hard-close path. -fn core_with_downlink( - limit: Option, -) -> ( - Arc>, - futures::channel::mpsc::UnboundedReceiver>, - Arc, -) { - let (dtx, drx) = futures::channel::mpsc::unbounded::>(); - let carrier = Carrier::with_downlink(dtx); - let host = counting_host(limit); - let metering = meter(&host, probe_key()) - .open("") - .expect("a chain with room opens") - .expect("a governed host opens an account"); - let core = Arc::new(SessionCore::new( - OpenAiRealtimeCodec, - Some(metering), - Arc::new(EchoToolExecutor), - carrier, - None, - )); - (core, drx, host) -} - -/// A governed fixture host whose budget view is capped at `limit` counts (`None`: uncapped). -fn counting_host(limit: Option) -> Arc { - let host = FixtureHost::new().governed(); - Arc::new(match limit { - Some(l) => host.with_count_cap(i64::try_from(l).unwrap_or(i64::MAX)), - None => host, - }) -} - -/// The presenting key every metered probe opens its session for. -fn probe_key() -> busbar_contract::records::VirtualKey { - key_with_scopes("vk", vec![]) -} - -/// `key`'s meter over `host` — the attribution the served door builds, before the account opens. -fn meter(host: &Arc, key: busbar_contract::records::VirtualKey) -> TurnMeter { - TurnMeter::new( - Arc::clone(host) as Arc, - key, - "streaming-server", - busbar_voice::OPENAI_REALTIME, - ) -} - -/// Every count `host` has ledgered for the probe key. -fn counts_ledgered(host: &FixtureHost) -> u64 { - host.ledger_usage(&probe_key().id).map_or(0, |u| u.tokens) -} - -/// One turn of `n` emitted audio tokens. -fn audio_out(n: u64) -> IrDuplexUsage { - IrDuplexUsage { - audio_out: n, - ..IrDuplexUsage::default() - } -} - -/// D2 — the marquee guarantee: settle past cap ⇒ carrier HARD-closes ⇒ no post-close audio reaches the -/// client. Driven through the real `SessionCore` → kernel session account path. -async fn gov_d2() -> (&'static str, String) { - use futures::StreamExt; - let (core, mut drx, _host) = core_with_downlink(Some(5)); - - let p1 = core.on_server_frame(wire_of(&usage_frame(3))).await; - if p1.close || core.carrier().is_closed() { - return ( - "FAIL", - "closed under the limit (reported 3 < limit 5)".into(), - ); - } - let p2 = core.on_server_frame(wire_of(&usage_frame(3))).await; - if !p2.close { - return ( - "FAIL", - "did NOT hard-close at exhaustion (reported 6 >= limit 5)".into(), - ); - } - if !core.carrier().is_closed() { - return ("FAIL", "plan.close set but carrier not closed".into()); - } - let cancelled = p2 - .upstream - .iter() - .any(|w| String::from_utf8_lossy(&w.0).contains("response.cancel")); - if !cancelled { - return ( - "FAIL", - "exhaustion did not cancel the in-flight response upstream".into(), - ); - } - // A post-close audio frame must produce NO downlink, and the carrier must refuse a direct send. - let p3 = core - .on_server_frame(wire_of( - &serde_json::json!({ "type": "response.output_audio.delta", "delta": "AAAA" }), - )) - .await; - if !p3.downlink.is_empty() || core.carrier().send_downlink(vec![1, 2, 3]) { - return ("FAIL", "post-close audio leaked to the client".into()); - } - drx.close(); - let mut leaked = false; - while (drx.next().await).is_some() { - leaked = true; - } - if leaked { - return ( - "FAIL", - "downlink audio leaked to the client after hard close".into(), - ); - } - ( - "PASS", - "counts past the limit → the kernel reads the chain dry → response.cancel upstream → carrier hard-closed → no post-close audio".into(), - ) -} - -/// V1 — a caller barge-in preempts the in-flight turn: `speech_started` after played audio yields an -/// upstream response.cancel + truncate at the heard position. -async fn gov_v1() -> (&'static str, String) { - let (core, _drx, _host) = core_with_downlink(None); - let payload = vec![0u8; 96]; // 2 ms of pcm16 - let b64 = busbar_contract::media::base64_encode(&Bytes::from(payload)); - let _ = core - .on_server_frame(wire_of( - &serde_json::json!({ "type": "response.output_audio.delta", "delta": b64 }), - )) - .await; - let plan = core - .on_server_frame(wire_of(&serde_json::json!({ - "type": "input_audio_buffer.speech_started", "audio_start_ms": 0, "item_id": "it1" - }))) - .await; - let joined: String = plan - .upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect::>() - .join("\n"); - if joined.contains("response.cancel") && joined.contains("conversation.item.truncate") { - ( - "PASS", - "barge-in preempts: response.cancel + truncate at the heard ms".into(), - ) - } else { - ("FAIL", format!("no preemption observed: {joined}")) - } -} - -/// V2 — a turn/session budget is BOUNDED: a limited session must close at the limit (never overruns). -fn gov_v2() -> (&'static str, String) { - let host = counting_host(Some(50)); - let Ok(Some(session)) = meter(&host, probe_key()).open("m") else { - return ("FAIL", "a limited session did not open".into()); - }; - let quiet = TurnCounters::default(); - let a = session.report_turn(Some(&audio_out(20)), quiet); - let b = session.report_turn(Some(&audio_out(20)), quiet); - let c = session.report_turn(Some(&audio_out(20)), quiet); // 60 >= 50 - if a == TurnVerdict::Live && b == TurnVerdict::Live && c == TurnVerdict::MustClose { - ( - "PASS", - format!( - "a limited session bounds use: 20,20 live then 20 → must close at {} counts", - counts_ledgered(&host) - ), - ) - } else { - ("FAIL", format!("budget not bounded: {a:?},{b:?},{c:?}")) - } -} - -/// V3 — a session's usage actually reaches the ledger (the plane's counts through the kernel session -/// account; nothing is left unledgered). -async fn gov_v3() -> (&'static str, String) { - let (core, _drx, host) = core_with_downlink(None); - let _ = core.on_server_frame(wire_of(&usage_frame(7))).await; - if counts_ledgered(&host) == 7 { - ( - "PASS", - "usage ledgered through the kernel session account (7 counts, the plane's own class)" - .into(), - ) - } else { - ( - "FAIL", - format!( - "the turn did not reach the ledger: {} counts", - counts_ledgered(&host) - ), - ) - } -} - -/// V4 — crossing the OpenAI→Gemini boundary DOWN-SCOPES: an OpenAI-only concept (semantic_vad + g711) -/// is not widened into Gemini; the far dialect never sees a concept it cannot honor. -fn gov_v4() -> (&'static str, String) { - let v = serde_json::json!({ - "type": "session.update", - "session": { "instructions": "x", "turn_detection": { "type": "semantic_vad", "eagerness": "high" }, "output_audio_format": "g711_ulaw" } - }); - let (_n1, n2) = bridge(&OpenAiRealtimeCodec, &GeminiLiveCodec, &v); - // The bridged Gemini setup carries a config but no semantic_vad eagerness and no g711 — down-scoped. - let s = format!("{n2:?}"); - if s.contains("Config") && !s.contains("semantic") && !s.contains("g711") { - ( - "PASS", - "OpenAI-only semantic_vad/g711 down-scoped, not widened, toward Gemini".into(), - ) - } else { - ( - "FAIL", - format!("boundary widened a far-dialect concept: {s}"), - ) - } -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEGS 5-7 — composition: the three seams that decide whether a MOUNTED voice door can actually -// serve, meter and authorize a session on a real deployment. Each is a conformance leg of its own, -// because each fails on its own: a door with no provider credential answers "nothing to dial", a door -// whose sessions skip the kernel's account ledgers nothing and closes on no budget, and a door with no -// grant check admits anyone holding a token for its audience. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -/// A stand-in for a deployment's secret resolver: answers ONE declared reference and refuses every -/// other, so the probe can tell "resolved through the seam" apart from "guessed". -struct OneSecretResolver { - expect: busbar_contract::secret_ref::SecretRef, - value: String, -} - -impl busbar_contract::secret::SecretResolve for OneSecretResolver { - fn resolve(&self, secret: &busbar_contract::secret_ref::SecretRef) -> Result, String> { - self.resolve_string(secret).map(String::into_bytes) - } - fn resolve_string( - &self, - secret: &busbar_contract::secret_ref::SecretRef, - ) -> Result { - if secret == &self.expect { - Ok(self.value.clone()) - } else { - Err("no such secret reference in this deployment".to_string()) - } - } -} - -/// One bucket of a caller's budget chain, with `remaining` micro-units (`None` = uncapped). -fn budget_bucket(id: &str, remaining: Option) -> busbar_contract::hooks::BudgetBucketState { - busbar_contract::hooks::BudgetBucketState { - bucket_id: id.to_string(), - budget_group: None, - pool: None, - spend_micros_at_current_rate: 0, - remaining_micros: remaining, - window_start: 0, - budget_period: "day".to_string(), - } -} - -/// A key carrying an EXPLICIT scope list (exhaustive across kinds — whatever is absent is not granted). -fn key_with_scopes( - id: &str, - scopes: Vec, -) -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: id.to_string(), - name: id.to_string(), - allowed_scopes: Some(scopes), - ..Default::default() - } -} - -fn composition(slice: &str) -> i32 { - let (verdict, detail) = match slice { - "provider-credential" => probe_provider_credential(), - "metering-lease" => probe_metering_lease(), - "session-scope" => probe_session_scope(), - "gemini-live-route" => probe_gemini_live_route(), - "provider-dial" => probe_provider_dial(), - "admit-refusal" => probe_admit_refusal(), - "route-failover" => probe_route_failover(), - "audit-record" => probe_audit_record(), - "exit-terminal" => probe_exit_terminal(), - "tool-reply" => probe_tool_reply(), - other => ("FAIL", format!("unknown composition slice '{other}'")), - }; - println!("RESULT {slice} {verdict} {detail}"); - i32::from(verdict == "FAIL") -} - -/// K-gap 1 — the realtime provider credential reaches the plane from the deployment's own catalog: -/// the composition root hands over an origin plus the secret REFERENCE the provider entry declares, -/// and the plane resolves it through the deployment's secret resolver. Without this, the mint and SDP -/// passes are governed but have nothing to dial. -fn probe_provider_credential() -> (&'static str, String) { - if busbar_voice::mount::provider_composed() { - return ( - "FAIL", - "a provider was already composed before the probe ran".into(), - ); - } - let reference = busbar_contract::secret_ref::SecretRef::env("REALTIME_PROVIDER_KEY"); - let resolver = OneSecretResolver { - expect: reference.clone(), - value: "sk-realtime-key-held-server-side".to_string(), - }; - // Fail closed: a reference this deployment does not declare composes nothing. - let undeclared = busbar_contract::secret_ref::SecretRef::env("NOT_DECLARED_HERE"); - if busbar_voice::mount::compose_provider("https://api.example.com", &undeclared, &resolver) - .is_ok() - || busbar_voice::mount::provider_composed() - { - return ( - "FAIL", - "an unresolvable credential reference still composed a provider".into(), - ); - } - // The declared reference resolves and composes the endpoint the mint / SDP passes read. - match busbar_voice::mount::compose_provider("https://api.example.com", &reference, &resolver) { - Ok(true) => {} - Ok(false) => { - return ( - "FAIL", - "the first compose reported an existing endpoint".into(), - ) - } - Err(e) => { - return ( - "FAIL", - format!("the declared credential did not resolve: {e}"), - ) - } - } - if !busbar_voice::mount::provider_composed() { - return ("FAIL", "composing left the plane with no provider".into()); - } - if busbar_voice::mount::composed_provider_base_url() != Some("https://api.example.com") { - return ("FAIL", "the composed origin is not the declared one".into()); - } - // Set-once: a later caller cannot silently swap the deployment's credential out. - if busbar_voice::mount::compose_provider("https://other.example.com", &reference, &resolver) - != Ok(false) - || busbar_voice::mount::composed_provider_base_url() != Some("https://api.example.com") - { - return ("FAIL", "a second compose swapped the endpoint".into()); - } - ( - "PASS", - "the declared provider reference resolves through the deployment's secret resolver and \ - composes the endpoint the mint / SDP passes serve under (set-once; an unresolvable \ - reference composes nothing)" - .into(), - ) -} - -// ── the governed tool-call wait, driven through a real session ─────────────────────────────────── - -/// The node's open-call table, as a session reaches it — the same port the composition root's own -/// implementor answers, over a table this probe can watch. -#[derive(Debug, Default)] -struct ProbeCalls { - open: std::sync::Mutex>, - woken: std::sync::Mutex>, - refused: std::sync::Mutex>, - /// The client-served legs the session itself planned, by call identifier. - planned: std::sync::Mutex>, - /// The calls the tick ended because nobody answered them, and the wall each was ended at. - ended: std::sync::Mutex>, -} - -/// One call this session has open, and the wall it stops waiting at. -#[derive(Debug)] -struct ProbeCall { - session: u64, - call_id: String, - deadline: u64, -} - -/// How long a tool call's reply leg waits, in milliseconds — the PLANE's own declaration, read from -/// the crate the composition root builds its leg out of rather than restated here. A second spelling -/// of thirty seconds would let this leg keep passing after the figure it is meant to be pinning had -/// moved. -fn tool_reply_deadline_ms() -> u64 { - u64::from(busbar_plane_streaming::plane::TOOL_REPLY_DEADLINE_SECS) * 1_000 -} - -impl ProbeCalls { - /// Enter a call as waiting, at `now_ms`, under the deadline the plane's leg declares — the same - /// arithmetic the kernel's own wait table does when the root plans the leg. - fn enter(&self, session: u64, call_id: &str, now_ms: u64) { - self.open.lock().unwrap().push(ProbeCall { - session, - call_id: call_id.to_string(), - deadline: now_ms + tool_reply_deadline_ms(), - }); - } -} - -impl busbar_voice::runtime::GovernedCalls for ProbeCalls { - fn planned(&self, _session: u64, call_id: &str, _now_ms: u64) -> bool { - // Recorded, not entered: the leg below enters its waits at a fixed wall so the deadline it - // pins is arithmetic rather than a reading of the clock. - self.planned.lock().unwrap().push(call_id.to_string()); - true - } - - fn replied( - &self, - session: u64, - call_id: &str, - ) -> Result<(), busbar_voice::runtime::ReplyRefusal> { - let mut open = self.open.lock().unwrap(); - if !open.iter().any(|c| c.session == session) { - self.refused.lock().unwrap().push(call_id.to_string()); - return Err(busbar_voice::runtime::ReplyRefusal::NoSuchSession); - } - match open - .iter() - .position(|c| c.session == session && c.call_id == call_id) - { - Some(i) => { - let call = open.remove(i); - self.woken.lock().unwrap().push(call.call_id); - Ok(()) - } - None => { - self.refused.lock().unwrap().push(call_id.to_string()); - Err(busbar_voice::runtime::ReplyRefusal::UnknownCall) - } - } - } - - fn expired(&self, now_ms: u64) -> usize { - let mut open = self.open.lock().unwrap(); - let (done, still): (Vec, Vec) = std::mem::take(&mut *open) - .into_iter() - .partition(|c| c.deadline <= now_ms); - *open = still; - let mut ended = self.ended.lock().unwrap(); - for c in &done { - ended.push((c.call_id.clone(), now_ms)); - } - done.len() - } -} - -/// A tool executor that serves nothing at all — every call this session sees is the client's to -/// answer, which is the shape the governed wait exists for. -#[derive(Debug)] -struct ServesNothing; - -#[async_trait::async_trait] -impl busbar_voice::runtime::ToolExecutor for ServesNothing { - fn executes_here(&self, _name: &str) -> bool { - false - } - async fn execute(&self, _name: &str, _arguments: &[u8]) -> Vec { - b"{}".to_vec() - } -} - -/// One dialect's three wire shapes for this leg: the frames that announce a call, the client's own -/// reply to it, and a reply naming a call nobody opened. -struct ToolWire { - dialect: &'static str, - announce: Vec, - reply: Value, - wrong: Value, -} - -fn openai_tool_wire() -> ToolWire { - ToolWire { - dialect: "openai-realtime", - announce: vec![ - serde_json::json!({"type":"response.output_item.added", - "item":{"type":"function_call","call_id":"call_x","name":"lookup"}}), - serde_json::json!({"type":"response.function_call_arguments.delta", - "call_id":"call_x","delta":"{\"q\":1}"}), - serde_json::json!({"type":"response.function_call_arguments.done","call_id":"call_x"}), - ], - reply: serde_json::json!({"type":"conversation.item.create", - "item":{"type":"function_call_output","call_id":"call_x","output":"ok"}}), - wrong: serde_json::json!({"type":"conversation.item.create", - "item":{"type":"function_call_output","call_id":"call_forged","output":"ok"}}), - } -} - -fn gemini_tool_wire() -> ToolWire { - ToolWire { - dialect: "gemini-live", - // Gemini delivers a call ATOMICALLY; the codec expands it into the same open/args/close - // triple, which is exactly why one runtime serves both dialects. - announce: vec![serde_json::json!({"toolCall":{"functionCalls":[ - {"id":"call_x","name":"lookup","args":{"q":1}}]}})], - reply: serde_json::json!({"toolResponse":{"functionResponses":[ - {"id":"call_x","name":"lookup","response":{"ok":true}}]}}), - wrong: serde_json::json!({"toolResponse":{"functionResponses":[ - {"id":"call_forged","name":"lookup","response":{"ok":true}}]}}), - } -} - -/// Drive one dialect's whole leg over a real [`SessionCore`]: announce a client-served call, answer -/// it, forge an answer, and let the tick sweep an unanswered one. `Ok(())` or the first failure. -fn drive_tool_reply(codec: C, w: &ToolWire) -> Result<(), String> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime"); - let table = Arc::new(ProbeCalls::default()); - let core = SessionCore::new( - codec, - None, - Arc::new(ServesNothing), - Carrier::sideband(), - None, - ) - .with_governed(busbar_voice::runtime::GovernedSession { - session: 7, - calls: Arc::clone(&table) as Arc, - }); - - // The call is announced. Nothing goes upstream: the node serves no tool, so it authors no - // answer — the wait the root planned is the only thing that can end this call. - for f in &w.announce { - let plan = rt.block_on(core.on_server_frame(wire_of(f))); - if !plan.upstream.is_empty() { - return Err(format!( - "{}: the node answered a call it does not serve", - w.dialect - )); - } - } - - // THE PLAN. The session itself tells the node's table the leg is the client's to answer. - if table.planned.lock().unwrap().as_slice() != ["call_x"] { - return Err(format!( - "{}: the session never planned the client-served leg", - w.dialect - )); - } - - // THE WAKE. The root entered the wait where it planned the leg; the client's reply names it. - table.enter(7, "call_x", 0); - let plan = core.on_client_frame(wire_of(&w.reply)); - if plan.refused_reply { - return Err(format!( - "{}: an open wait refused its own answer", - w.dialect - )); - } - if table.woken.lock().unwrap().as_slice() != ["call_x"] { - return Err(format!( - "{}: the reply never reached the node's table", - w.dialect - )); - } - let up: String = plan - .upstream - .iter() - .map(|e| String::from_utf8_lossy(&e.0).to_string()) - .collect::>() - .join("\n"); - if !up.contains("call_x") { - return Err(format!( - "{}: the woken reply never reached the model: {up}", - w.dialect - )); - } - - // THE REFUSAL. A reply naming a call nobody is waiting on reaches the model on no wire at all. - table.enter(7, "call_y", 0); - let plan = core.on_client_frame(wire_of(&w.wrong)); - if !plan.refused_reply || !plan.upstream.is_empty() { - return Err(format!( - "{}: a reply naming no open call was carried upstream anyway", - w.dialect - )); - } - if table.refused.lock().unwrap().as_slice() != ["call_forged"] { - return Err(format!( - "{}: the forged reply was not refused by identifier", - w.dialect - )); - } - - // THE SWEEP, AT THE DEADLINE THE PLANE DECLARES. `call_y` is still open and nobody answered it; - // the tick beside the pump is what ends it, and WHEN it ends it is the whole of what this half of - // the leg pins. The wall is the plane's own `TOOL_REPLY_DEADLINE_SECS`, read from the plane crate - // the composition root builds its reply leg from, so a change to that figure moves this leg with - // it instead of leaving it asserting the old one. Not one millisecond early: a tick that ended a - // call before its declared deadline would be settling a hold the client still had time to close. - let deadline = tool_reply_deadline_ms(); - if core.sweep_expired(deadline - 1) != 0 { - return Err(format!( - "{}: a call was ended one millisecond before the deadline its leg declared", - w.dialect - )); - } - if core.sweep_expired(deadline) != 1 { - return Err(format!( - "{}: the tick did not end the unanswered call at the declared deadline ({deadline} ms)", - w.dialect - )); - } - // And what it left behind is an ENDING, not a settlement: this call was ended by the deadline, - // and nothing was ever woken for it. On the composition root's own table that ending is what the - // unit's exit path reads to end as `Failed(Route, DeadlineExceeded)`; here it is read as the fact - // the served path put there — the call, and the wall it was ended at. - if table.ended.lock().unwrap().as_slice() != [("call_y".to_string(), deadline)] { - return Err(format!( - "{}: the unanswered call was not ended at its own deadline: {:?}", - w.dialect, - table.ended.lock().unwrap() - )); - } - if table.woken.lock().unwrap().as_slice() != ["call_x"] { - return Err(format!( - "{}: an unanswered call was settled as though its answer had arrived", - w.dialect - )); - } - Ok(()) -} - -/// A client-served tool call's reply reaches the node's own table through a REAL session, on every -/// dialect the plane serves a duplex session on. Without this the root entered a wait that the served -/// path could never wake and no tick ever swept — the call's hold was held open by a client that -/// simply never replied, and a forged reply rode upstream under whatever call happened to be open. -fn probe_tool_reply() -> (&'static str, String) { - if let Err(e) = drive_tool_reply(OpenAiRealtimeCodec, &openai_tool_wire()) { - return ("FAIL", e); - } - if let Err(e) = drive_tool_reply(GeminiLiveCodec, &gemini_tool_wire()) { - return ("FAIL", e); - } - ( - "PASS", - format!( - "on both duplex dialects a call for a tool the node does not serve is answered by \ - nobody but the client: the reply wakes the wait the root planned and only then reaches \ - the model, a reply naming no open call is refused and carried on no wire, and the tick \ - beside the pump ends a call nobody answered at exactly the {} s its leg declares — not \ - one millisecond early — so its unit exits under that deadline rather than settling as \ - though the answer had arrived", - busbar_plane_streaming::plane::TOOL_REPLY_DEADLINE_SECS - ), - ) -} - -/// K-gap 2 (the `metering-lease` slice, OWNER RULING Q21b) — a session is metered on the KERNEL's -/// session account for the presenting key: its turns' counts are ledgered through the one metering -/// path, and the kernel's budget view of the caller's chain refuses a spent caller at the open and -/// closes a live session at the turn that dries it. The D2 lease this slice once probed is deleted. -fn probe_metering_lease() -> (&'static str, String) { - let quiet = TurnCounters::default(); - // A spent caller never opens a session: the kernel's view reads its chain dry. - let spent = Arc::new( - FixtureHost::new() - .governed() - .with_budget_chain(vec![budget_bucket("vk", Some(0))]), - ); - if meter(&spent, probe_key()).open("m").is_ok() { - return ( - "FAIL", - "a session opened for a caller whose budget is spent".into(), - ); - } - // An unbudgeted caller has no ceiling: no turn ever closes the session. - let open = Arc::new( - FixtureHost::new() - .governed() - .with_budget_chain(vec![budget_bucket("vk", None)]), - ); - let Ok(Some(session)) = meter(&open, probe_key()).open("m") else { - return ( - "FAIL", - "an unbudgeted caller could not open a session".into(), - ); - }; - if session.report_turn(Some(&audio_out(u64::MAX / 2)), quiet) != TurnVerdict::Live { - return ("FAIL", "an unbudgeted caller was given a ceiling".into()); - } - // A chain of 4 000 counts: a turn one short of it is live, and the turn that reaches it closes. - let capped = counting_host(Some(4_000)); - let Ok(Some(session)) = meter(&capped, probe_key()).open("m") else { - return ("FAIL", "a budgeted caller could not open a session".into()); - }; - let live = session.report_turn(Some(&audio_out(3_999)), quiet); - let dry = session.report_turn(Some(&audio_out(1)), quiet); - if live != TurnVerdict::Live || dry != TurnVerdict::MustClose { - return ( - "FAIL", - format!("turns did not close at the caller's ceiling: {live:?} then {dry:?}"), - ); - } - if counts_ledgered(&capped) != 4_000 { - return ( - "FAIL", - format!( - "the ledger did not hold the exact counts: {}", - counts_ledgered(&capped) - ), - ); - } - ( - "PASS", - "the session is metered on the kernel's session account: its counts are ledgered exactly, \ - a spent caller is refused at the open, and a live one must close at the turn that dries \ - its chain" - .into(), - ) -} - -/// K-gap 3 — the plane's declared `session` scope kind is enforced at session open. Without this, any -/// key valid for the voice audience opens a session, and the declared vocabulary is inert. -fn probe_session_scope() -> (&'static str, String) { - let pool_scope = busbar_contract::records::ScopeRef::pool("fast"); - let session_here = busbar_contract::records::ScopeRef { - kind: "session".to_string(), - value: "streaming-server".to_string(), - }; - let session_elsewhere = busbar_contract::records::ScopeRef { - kind: "session".to_string(), - value: "some-other-pool".to_string(), - }; - - // A wildcard principal (no list at all) is granted every kind, as on every other plane. - let wildcard = busbar_contract::records::VirtualKey { - id: "vk-wildcard".to_string(), - ..Default::default() - }; - if !busbar_voice::mount::session_scope_allowed(&wildcard) { - return ("FAIL", "a wildcard principal was refused a session".into()); - } - // An explicit grant on the voice pool admits. - if !busbar_voice::mount::session_scope_allowed(&key_with_scopes( - "vk-granted", - vec![session_here.clone()], - )) { - return ("FAIL", "an explicit session grant was refused".into()); - } - // Everything else with an explicit list is refused: a model-plane key, a session grant aimed at - // another pool, and an empty list. - for (name, scopes) in [ - ("a model-plane key", vec![pool_scope]), - ("a session grant for another pool", vec![session_elsewhere]), - ("an empty grant list", Vec::new()), - ] { - if busbar_voice::mount::session_scope_allowed(&key_with_scopes("vk-ungranted", scopes)) { - return ("FAIL", format!("{name} was admitted a voice session")); - } - } - ( - "PASS", - "the declared session scope is enforced against the presenting key's own grant: granted \ - and wildcard keys open, a key without it (or with it aimed elsewhere) is refused" - .into(), - ) -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEG 8 (the second-dialect route) — gemini-live-route: the Gemini Live dialect has a MOUNTED route, not just a codec the -// spec/cross-parity legs exercise off to the side. Proves, on the plane's own PUBLIC functions (the -// same ones the composition root calls, and the same `WsArrivalSpec` a real deployment mounts): -// -// * the dispatch slot CLAIMS a Gemini-labelled base distinct from the OpenAI one (`voice_claims`) -// * the plane still ADMITS exactly one audience for both dialects (`voice_admission`) -// * a Gemini WS-accept arrival is actually declared, keyed to this plane's own slot (`voice_ws_arrivals`) -// * the wire handshake itself: a `setupComplete` frame from the far side (what a dialed provider -// sends) is relayed to the client verbatim through a `SessionCore` — the EXACT -// codec type the mounted route's `WsArrivalSpec` closure closes over, not a stand-in. -// -// WAS RED: `PLANE_HOOKS.wire_format_names` named `gemini_live`, the codec existed and passed the -// spec/cross-parity battery, but no ingress route spoke it — `voice_claims`/`voice_ws_arrivals` named -// only the OpenAI base, so a caller had no path to reach the Gemini dialect at all. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -fn probe_gemini_live_route() -> (&'static str, String) { - let unit = (); - let ctx = busbar_kernel::plane::registry::BuildCtx { - endpoint_slot: None, - agent_defs: &unit, - tool_defs: &unit, - public_url: Some("https://gw.conform.example.com"), - prior: None, - providers: None, - }; - let Some(slot) = busbar_voice::mount::voice_build(&ctx) else { - return ("FAIL", "voice_build produced no dispatch slot".into()); - }; - - let claims = busbar_voice::mount::voice_claims(slot.as_ref()); - if !claims.contains(&("/v1/realtime/gemini".to_string(), busbar_voice::GEMINI_LIVE)) { - return ( - "FAIL", - format!("the Gemini base is not claimed under its own dialect: {claims:?}"), - ); - } - if !claims.contains(&("/v1/realtime".to_string(), busbar_voice::OPENAI_REALTIME)) { - return ( - "FAIL", - format!("the OpenAI base is no longer claimed alongside Gemini: {claims:?}"), - ); - } - - let Some(admission) = busbar_voice::mount::voice_admission(slot.as_ref()) else { - return ( - "FAIL", - "a plane that claims paths must admit (the claim-admits ratchet): admission is None" - .into(), - ); - }; - if !admission.audience.ends_with("/v1/realtime") { - return ( - "FAIL", - format!("unexpected audience: {}", admission.audience), - ); - } - - let arrivals = busbar_voice::mount::voice_ws_arrivals(); - let Some(gemini) = arrivals - .iter() - .find(|a| a.path == "/v1/realtime/gemini/{call_id}") - else { - return ( - "FAIL", - format!( - "no Gemini WS-accept arrival mounted; declared paths: {:?}", - arrivals.iter().map(|a| &a.path).collect::>() - ), - ); - }; - if gemini.slot_key != busbar_voice::PLANE_DECLARATION.key { - return ( - "FAIL", - format!( - "the Gemini arrival is keyed to '{}', not the plane's own slot '{}'", - gemini.slot_key, - busbar_voice::PLANE_DECLARATION.key - ), - ); - } - - // THE HANDSHAKE ITSELF, over the exact runtime type the mounted route is generic over: a provider's - // `setupComplete` answers the client's `setup` by relaying verbatim (`IrServerEvent::SessionCreated` - // is a pass-through in `SessionCore::on_server_frame`) — the same relay the Gemini WS accept's - // provider leg drives once a session is dialed (see the `provider-dial` leg for the live-socket - // proof; this leg proves the PLANE'S side of that relay against the mounted codec). - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime"); - let core = SessionCore::new( - GeminiLiveCodec, - None, - Arc::new(EchoToolExecutor), - Carrier::sideband(), - None, - ); - let setup_complete = serde_json::json!({ "setupComplete": {} }); - let outbound = rt.block_on(core.on_server_frame(wire_of(&setup_complete))); - if outbound.downlink.len() != 1 { - return ( - "FAIL", - format!( - "expected exactly one relayed downlink frame from setupComplete, got {}", - outbound.downlink.len() - ), - ); - } - let got = val_of(&outbound.downlink[0]); - if got.get("setupComplete").is_none() { - return ( - "FAIL", - format!("the relayed downlink frame was not setupComplete: {got}"), - ); - } - - ( - "PASS", - "the Gemini Live route is mounted under its own claim, admits the same audience, declares a \ - WS-accept arrival keyed to the plane's slot, and the mounted codec relays a provider's \ - setupComplete handshake to the client verbatim" - .into(), - ) -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEG 9 (the provider-dial leg) — provider-dial: `topology::dial_provider` is a library function nothing calls in -// production without a composed provider; this leg proves a session actually dials one end to end. -// A loopback WS "provider" stands in for a real realtime upstream (no network, no vendor credential -// needed): the harness binds it on an ephemeral port, `dial_provider` dials it through the SAME -// net-guarded path the mounted WS-accept legs now call, the loopback sends one usage frame, and the -// session's own kernel account ledgers it — proving the wiring from a live socket to the ledger, not -// just the codec math the other legs already cover. -// -// WAS RED: `topology::dial_provider` existed, breaker/net-guarded, but nothing in the mounted routes -// called it — a session never dialed a live socket, so no leg drove one. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -/// A minimal [`busbar_kernel::plane_host::BreakerHost`] — `dial_provider` reads only the breaker -/// slice of the host seam (never the rest of `EngineHost`), so the loopback leg needs only this much: -/// a fresh kernel breaker store (every cell Closed, so the one dial below is admitted) that the -/// kernel's breaker functions drive. Not a plane-private breaker implementation — it lives only in -/// this dev-only conformance binary. -struct FreshBreakerHost(std::sync::Arc); - -impl Default for FreshBreakerHost { - fn default() -> Self { - FreshBreakerHost(std::sync::Arc::new(PlaneBreakers::provisioned())) - } -} - -impl BreakerHost for FreshBreakerHost { - fn breaker_store(&self) -> &std::sync::Arc { - &self.0 - } - fn breaker_settle( - &self, - scope: &DispatchScope, - admission: busbar_contract::abi::hot::AdmissionId, - signal: &busbar_contract::abi::hot::Signal, - ) -> busbar_contract::abi::hot::StatusClass { - scope - .settle_admission(admission, signal) - .unwrap_or(busbar_contract::abi::hot::StatusClass::Refused) - } -} - -fn probe_provider_dial() -> (&'static str, String) { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime"); - rt.block_on(async move { - // THE LOOPBACK PROVIDER: bind an ephemeral port, accept ONE connection, upgrade it to a bare WS - // server (no TLS — the dial below opts into plaintext for this loopback target only), read - // whatever the client sends (ignored — this leg proves the DOWNLINK leg settles, not the uplink - // shape), then send one `response.done` usage frame and close. - let listener = match tokio::net::TcpListener::bind("127.0.0.1:0").await { - Ok(l) => l, - Err(e) => return ("FAIL", format!("loopback provider could not bind: {e}")), - }; - let addr = listener.local_addr().expect("a bound listener has a local addr"); - let server = tokio::spawn(async move { - let (tcp, _peer) = listener.accept().await.expect("one loopback connection"); - let mut ws = tokio_tungstenite::accept_async(tcp) - .await - .expect("the loopback WS handshake completes"); - let usage = usage_frame(9).to_string(); - let _ = futures::SinkExt::send( - &mut ws, - tokio_tungstenite::tungstenite::Message::text(usage), - ) - .await; - let _ = futures::SinkExt::close(&mut ws).await; - }); - - let host = FreshBreakerHost::default(); - let url = format!("ws://{addr}"); - let policy = busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy { - allow_private: true, - allow_plaintext: true, - ..busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy::default() - }; - let (mut provider_in, _provider_out) = match busbar_voice::topology::dial_provider( - &host, - "stream:conform-loopback", - 0, - &url, - policy, - None, - ) - .await - { - Ok(pair) => pair, - Err(e) => return ("FAIL", format!("dial_provider could not reach the loopback: {e}")), - }; - - // ONE SESSION END TO END: the dialed frame drives the SAME `SessionCore` the mounted route - // opens, and its kernel account must ledger the usage the loopback sent — over a governed - // host that records every count, so the leg cannot pass unless the frame arrived. - let counting = counting_host(None); - let Ok(Some(metering)) = meter(&counting, probe_key()).open("") else { - return ("FAIL", "the probe key's account did not open".into()); - }; - let core = SessionCore::new( - OpenAiRealtimeCodec, - Some(metering), - Arc::new(EchoToolExecutor), - Carrier::sideband(), - None, - ); - let Some(frame) = futures::StreamExt::next(&mut provider_in).await else { - return ( - "FAIL", - "the loopback provider closed before sending its usage frame".into(), - ); - }; - let _ = core - .on_server_frame(WireEvent(Bytes::from(frame))) - .await; - let _ = server.await; - - if counts_ledgered(&counting) == 9 { - ( - "PASS", - "one session dialed the loopback provider through `dial_provider`'s net-guarded path \ - end to end, and its kernel account ledgered the usage the loopback sent (9 counts)" - .into(), - ) - } else { - ( - "FAIL", - format!( - "the dialed usage did not reach the ledger: {} counts", - counts_ledgered(&counting) - ), - ) - } - }) -} - -// ══════════════════════════════════════════════════════════════════════════════════════════════════ -// LEGS 10-13 — the last four gaps: admit / route / audit / exit, each judged against a REAL -// `EngineHost` (the substrate's `FixtureHost`, a full double over the same seam `build_runtime_hosted` -// takes in production) rather than a narrow `MeteringHost`/`BreakerHost` stand-in. -// ══════════════════════════════════════════════════════════════════════════════════════════════════ - -/// Assemble a per-generation [`VoiceRuntime`] bound onto `host` — the harness's own -/// [`build_runtime_hosted`] call, shared by every probe below that needs a governed session. -fn hosted_runtime(host: Arc) -> VoiceRuntime { - let base = VoiceRuntime::new( - Arc::new(busbar_kernel::plane::handle_engine::DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ); - build_runtime_hosted(&base, host) -} - -/// LEG 10 — admit-refusal: a key whose budget is already spent is refused AT THE DOOR -/// (`StartError::BudgetRefused`), before any session exists and before any ledger posting — so no -/// provider dial has anything left to reach. A negative control (the same destination, a funded key) -/// proves the leg can also see a clean open. -fn probe_admit_refusal() -> (&'static str, String) { - // The spent caller's budget is the KERNEL's reading of its budget chain (a spent bucket). - let fixture = Arc::new( - FixtureHost::new() - .governed() - .with_budget_chain(vec![budget_bucket("spent-caller", Some(0))]), - ); - let rt = hosted_runtime(Arc::clone(&fixture) as Arc); - match begin_session( - &rt, - OpenAiRealtimeCodec, - "spent-caller", - "call-admit-refusal-1", - None, - Carrier::sideband(), - Some(meter(&fixture, key_with_scopes("spent-caller", vec![]))), - 0, - ) { - Err(StartError::BudgetRefused) => {} - Ok(_) => return ("FAIL", "a spent budget was admitted a session".into()), - Err(e) => { - return ( - "FAIL", - format!("wrong refusal reason for a spent budget: {e}"), - ) - } - } - if fixture.ledger_usage("spent-caller").is_some() || !fixture.audit_log().is_empty() { - return ( - "FAIL", - "a refused admission still posted a ledger entry or opened a session -- voice posts no \ - separate floor line at Admit (nothing was ever dialed, so nothing is owed)" - .into(), - ); - } - - // NEGATIVE CONTROL: the SAME destination for a funded key must open cleanly, so this leg could not - // pass by always refusing. - let funded = Arc::new(FixtureHost::new().governed()); - let rt = hosted_runtime(Arc::clone(&funded) as Arc); - if let Err(e) = begin_session( - &rt, - OpenAiRealtimeCodec, - "funded-caller", - "call-admit-refusal-2", - None, - Carrier::sideband(), - Some(meter(&funded, key_with_scopes("funded-caller", vec![]))), - 0, - ) { - return ( - "FAIL", - format!("the negative control (a funded key) was refused too: {e}"), - ); - } - if funded.audit_log().len() != 1 { - return ( - "FAIL", - "the negative control did not open exactly one session".into(), - ); - } - - ( - "PASS", - "a spent budget is refused at the door (StartError::BudgetRefused) with no session opened \ - and zero ledger postings -- no provider dial has anything left to reach; the same \ - destination for a funded key opens cleanly" - .into(), - ) -} - -/// LEG 11 — route-failover: a hard-down provider dial trips the breaker cell on its first strike, and -/// the tripped cell refuses every FURTHER dial before any socket/URL work — the documented terminal -/// outcome, with no repeated egress once the cell is open. -fn probe_route_failover() -> (&'static str, String) { - let tokio_rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("tokio runtime"); - tokio_rt.block_on(async move { - let host = FixtureHost::new(); - let pool = "stream:conform-route-failover"; - let policy = busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy::default(); - - // ATTEMPT 1 — breaker CLOSED: a real dial to a target the default fail-closed guard refuses (a - // plaintext `ws://` loopback address, `allow_plaintext: false` by default) genuinely fails, - // and the guard refusal's canonical signal (Auth-class => HardDown) trips the cell. - match dial_provider(&host, pool, 0, "ws://127.0.0.1:1/", policy, None).await { - Err(DialProviderError::Dial(_)) => {} - Ok(_) => { - return ( - "FAIL", - "the guard-refused target dialed successfully".into(), - ) - } - Err(e) => return ("FAIL", format!("expected a real dial failure, got: {e}")), - } - if !matches!( - host.breaker_state(pool, 0), - busbar_kernel::store::BreakerState::Open { .. } - ) { - return ( - "FAIL", - "the hard-down dial failure did not trip the breaker cell".into(), - ); - } - - // ATTEMPT 2 — the breaker is now OPEN: a syntactically GARBAGE target (one a real dial would - // fail differently on, `DialProviderError::Dial(Url(_))`) must instead come back - // `BreakerOpen` -- proving the breaker check runs STRICTLY BEFORE any further dial, not merely - // that a retried dial fails again for a different reason. - match dial_provider(&host, pool, 0, "not a url at all", policy, None).await { - Err(DialProviderError::BreakerOpen { retry_after_secs }) if retry_after_secs > 0 => {} - Ok(_) => return ("FAIL", "a tripped breaker still dialed".into()), - Err(e) => { - return ( - "FAIL", - format!("a tripped breaker let a further dial attempt happen: {e}"), - ) - } - } - - ( - "PASS", - "a hard-down dial trips the breaker cell on its first strike, and the tripped cell \ - refuses every further dial before any socket/URL work is attempted -- the terminal \ - outcome holds with no repeated egress" - .into(), - ) - }) -} - -/// LEG 12 — audit-record: one governed session lands EXACTLY ONE new admin-audit entry, carrying the -/// plane's own action literal and outcome — and a second, independent session adds exactly one more. -fn probe_audit_record() -> (&'static str, String) { - let fixture = Arc::new(FixtureHost::new()); - let host: Arc = Arc::clone(&fixture) as Arc; - let rt = hosted_runtime(host); - - let Ok(_first) = begin_session( - &rt, - OpenAiRealtimeCodec, - "alice", - "call-audit-1", - None, - Carrier::sideband(), - None, - 0, - ) else { - return ("FAIL", "a clean session open was refused".into()); - }; - let log = fixture.audit_log(); - if log.len() != 1 { - return ( - "FAIL", - format!( - "expected exactly one admin-audit row after one session, got {}", - log.len() - ), - ); - } - let row = &log[0]; - if row.action != "streaming.session.open" - || row.outcome != "applied" - || row.principal != "alice" - { - return ("FAIL", format!("wrong audit row shape: {row:?}")); - } - if row.resource != "streaming:call-audit-1" { - return ( - "FAIL", - format!("the audit row does not name this session: {row:?}"), - ); - } - - // A second, INDEPENDENT session must add exactly one MORE row. - let Ok(_second) = begin_session( - &rt, - OpenAiRealtimeCodec, - "bob", - "call-audit-2", - None, - Carrier::sideband(), - None, - 0, - ) else { - return ("FAIL", "the second session's clean open was refused".into()); - }; - let log2 = fixture.audit_log(); - if log2.len() != 2 { - return ( - "FAIL", - format!( - "expected exactly two admin-audit rows after two sessions, got {}", - log2.len() - ), - ); - } - - ( - "PASS", - "each governed session open lands exactly one admin-audit row, carrying the \ - `streaming.session.open` action literal and the `applied` outcome -- two sessions land exactly \ - two rows, never doubled, never dropped" - .into(), - ) -} - -/// LEG 13 — exit-terminal: one session ends ONCE. Two independent proofs over the same host: a -/// session's open turn is ledgered exactly once under a double teardown (the "interrupted" shape a -/// parked handler plus the node's own sweep produce), and a session's one admin-audit row survives -/// being torn down before it ever runs a frame. -fn probe_exit_terminal() -> (&'static str, String) { - let fixture = counting_host(None); - let rt = hosted_runtime(Arc::clone(&fixture) as Arc); - - // PART 1 — the teardown settle is idempotent: a second settle of the same (already-settled) turn - // is a harmless no-op, never a second ledger posting. - let Ok((core, _handle)) = begin_session( - &rt, - OpenAiRealtimeCodec, - "dave", - "call-exit-0", - None, - Carrier::sideband(), - Some(meter(&fixture, probe_key())), - 0, - ) else { - return ("FAIL", "a funded session open was refused".into()); - }; - let pcm = Bytes::from(vec![0u8; 48_000]); // one second of PCM16 uplink - let _ = core.on_client_frame(wire_of(&serde_json::json!({ - "type": "input_audio_buffer.append", - "audio": busbar_contract::media::base64_encode(&pcm), - }))); - core.settle_open_turn(); - if counts_ledgered(&fixture) != 1 { - return ("FAIL", "the teardown did not ledger the open turn".into()); - } - core.settle_open_turn(); - if counts_ledgered(&fixture) != 1 { - return ( - "FAIL", - "a second teardown after interruption ledgered the turn again instead of being a \ - harmless no-op" - .into(), - ); - } - drop(core); - - // PART 2 — the session's one admin-audit row survives an interruption: torn down before it ever - // runs a single frame. - let audited = fixture.audit_log().len(); - let Ok((core, _handle)) = begin_session( - &rt, - OpenAiRealtimeCodec, - "carol", - "call-exit-1", - None, - Carrier::sideband(), - None, - 0, - ) else { - return ("FAIL", "a clean session open was refused".into()); - }; - if fixture.audit_log().len() != audited + 1 { - return ( - "FAIL", - "the session's audit row did not land before the interruption".into(), - ); - } - // INTERRUPTED: no frame is ever run; the core is simply dropped. - drop(core); - if fixture.audit_log().len() != audited + 1 { - return ( - "FAIL", - format!( - "the interruption changed the audit row count: {}", - fixture.audit_log().len() - ), - ); - } - - ( - "PASS", - "a session's open turn is ledgered exactly once under a double teardown (a redundant \ - settle is a harmless no-op, never a second posting), and a session's one admin-audit row \ - survives being torn down before it ever runs a frame" - .into(), - ) -} - -// ── entry point ───────────────────────────────────────────────────────────────────────────────────── - -fn main() { - let args: Vec = std::env::args().collect(); - let usage = || -> ! { - eprintln!( - "usage:\n voice-conform spec \n voice-conform replay \n voice-conform cross \n voice-conform governance \n voice-conform composition " - ); - std::process::exit(2); - }; - let code = match args.get(1).map(String::as_str) { - Some("spec") => { - let dialect = args.get(2).unwrap_or_else(|| usage()); - let dir = args.get(3).unwrap_or_else(|| usage()); - spec(dialect, Path::new(dir)) - } - Some("replay") => { - let root = args.get(2).unwrap_or_else(|| usage()); - replay(Path::new(root)) - } - Some("cross") => { - let pair = args.get(2).unwrap_or_else(|| usage()); - let oa = Path::new(args.get(3).unwrap_or_else(|| usage())); - let ge = Path::new(args.get(4).unwrap_or_else(|| usage())); - let map: Value = serde_json::from_str( - &fs::read_to_string(args.get(5).unwrap_or_else(|| usage())).unwrap(), - ) - .expect("parse cross-dialect map json"); - match pair.as_str() { - "oo" => cross( - &OpenAiRealtimeCodec, - &OpenAiRealtimeCodec, - "openai", - "openai", - oa, - ge, - &map, - ), - "og" => cross( - &OpenAiRealtimeCodec, - &GeminiLiveCodec, - "openai", - "gemini", - oa, - ge, - &map, - ), - "go" => cross( - &GeminiLiveCodec, - &OpenAiRealtimeCodec, - "gemini", - "openai", - oa, - ge, - &map, - ), - "gg" => cross( - &GeminiLiveCodec, - &GeminiLiveCodec, - "gemini", - "gemini", - oa, - ge, - &map, - ), - _ => usage(), - } - } - Some("governance") => { - let cp = args.get(2).unwrap_or_else(|| usage()); - governance(cp) - } - Some("composition") => { - let slice = args.get(2).unwrap_or_else(|| usage()); - composition(slice) - } - _ => usage(), - }; - std::process::exit(code); -} diff --git a/crates/busbar-voice/src/config.rs b/crates/busbar-voice/src/config.rs deleted file mode 100644 index 4c86cac822..0000000000 --- a/crates/busbar-voice/src/config.rs +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE `streams:` SECTION'S KERNEL SEAM — the plane's grammar ([`StreamsCfg`], owned by the -//! streaming plane) behind the kernel's config-section trait, and the parse/default hooks. -//! -//! The grammar itself, its defaults and the session ceilings live with the plane. What stays here is -//! what the kernel's registry reaches: [`StreamsSection`] carries a parsed `StreamsCfg` as the -//! kernel's section object, and its `as_any` answers the inner `StreamsCfg`, so a reader that -//! downcasts the section finds the plane's own type. - -pub use crate::plane_config::StreamsCfg; - -/// A parsed `streams:` section as the kernel's config-section object. -#[derive(Debug, Clone, PartialEq)] -pub struct StreamsSection(pub StreamsCfg); - -impl busbar_kernel::plane::config::PlaneCfg for StreamsSection { - /// The voice plane's `streams:` section carries NO secret reference — the exhaustive destructure - /// (no `..`) is kept anyway so a future secret-bearing field fails to compile until someone - /// decides, HERE, whether it is a secret, exactly as `AgentsCfg`/`ToolsCfg` do. - fn secret_refs(&self) -> Vec<(String, &busbar_contract::secret_ref::SecretRef)> { - let StreamsCfg { - session: _, - session_max_secs: _, - context_window_tokens: _, - max_output_tokens: _, - } = &self.0; - Vec::new() - } - - /// `streams:` is a SINGULAR section, not a named-definition registry — there are no definitions to - /// contain, name, project, or insert. The registry methods are therefore trivial. - fn contains_def(&self, _name: &str) -> bool { - false - } - - fn def_names(&self) -> Vec<&str> { - Vec::new() - } - - fn entry_document(&self, _name: &str) -> Option { - None - } - - fn insert_def(&mut self, _name: &str, _def: &serde_json::Value) -> Result<(), String> { - Err("`streams:` has no named definitions".into()) - } - - fn container_gates(&self) -> busbar_kernel::plane::config::ContainerGateInputs { - busbar_kernel::plane::config::ContainerGateInputs { - section_hooks: Vec::new(), - containers: Vec::new(), - } - } - - fn validate_registry(&self) -> Result<(), String> { - Ok(()) - } - - /// Present only when the operator wrote CONTENT — i.e. anything other than the plane's own - /// defaults. Read by the config deletion-gate leg to refuse a present `streams:` naming a - /// compiled-out voice plane. - fn is_present(&self) -> bool { - self.0 != StreamsCfg::default() - } - - fn as_any(&self) -> &dyn std::any::Any { - &self.0 - } - - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } - - fn clone_arc_any(&self) -> std::sync::Arc { - std::sync::Arc::new(self.0.clone()) - } -} - -/// THE LAST `streams:` SECTION THIS PROCESS PARSED — the plane's own copy of its operator posture. -/// -/// The engine's resolved config carries the named-map plane registries forward, but a SINGULAR plane -/// section like `streams:` is read at deserialize time and not re-handed to the plane afterwards: the -/// voice dispatch slot is built from `public_url` alone, and nothing calls the plane's runtime-build -/// hook. So the plane keeps what it parsed, here, and the mount reads it back when it assembles a -/// generation's runtime. A config reload re-parses and replaces it, so this always holds the posture -/// of the most recently loaded config rather than a boot-frozen one. Absent (no `streams:` block in -/// the file, so nothing was parsed) reads back as the plane's own defaults — byte-identical to what a -/// deployment that writes nothing already got. -static PARSED_SECTION: std::sync::RwLock> = std::sync::RwLock::new(None); - -/// The operator's `streams:` posture, or the plane's defaults when no block was written. Read by the -/// mount when it builds a generation's session runtime, and by the composition root when it resolves -/// the realtime provider credential for the session's configured model. -#[must_use] -pub fn configured() -> StreamsCfg { - PARSED_SECTION - .read() - .ok() - .and_then(|held| held.clone()) - .unwrap_or_default() -} - -/// The upstream model the configured session posture targets (`streams.session.model`), or `None` -/// when the operator pinned none. This is the ONE name the composition root looks up in the -/// deployment's existing model/provider catalog to find the realtime provider credential to compose — -/// the voice grammar declares no credential field of its own and gains none here. -#[must_use] -pub fn configured_session_model() -> Option { - configured().session.model -} - -/// PLANE_HOOKS.parse_section — deserialize `streams:` through the plane's own typed shape, boxed as the -/// neutral [`busbar_kernel::plane::config::PlaneCfg`]. Mirror of `mcp_parse_section` / -/// `a2a_parse_section`. UNCONDITIONAL (outside the `runtime` gate): config parse/validate is needed -/// even in the skeleton/no-`runtime` build. -pub fn streams_parse_section( - v: &serde_yaml::Value, -) -> Result, String> { - let parsed = serde_yaml::from_value::(v.clone()).map_err(|e| e.to_string())?; - // Keep what we just parsed (see `PARSED_SECTION`). Only a SUCCESSFUL parse is kept, so a refused - // config never replaces the posture a good one installed. A poisoned lock is ignored rather than - // panicking here — the read side then falls back to the plane defaults. - if let Ok(mut held) = PARSED_SECTION.write() { - *held = Some(parsed.clone()); - } - Ok(Box::new(StreamsSection(parsed)) as Box) -} - -/// PLANE_HOOKS.default_section — the empty `streams:` (mirror of `mcp_default_section` / -/// `a2a_default_section`), so an ABSENT `streams:` decodes byte-identically to the plane's own -/// [`StreamsCfg::default`]. -pub fn streams_default_section() -> Box { - Box::new(StreamsSection(StreamsCfg::default())) -} - -#[cfg(test)] -#[path = "tests/config_tests.rs"] -mod config_tests; diff --git a/crates/busbar-voice/src/lib.rs b/crates/busbar-voice/src/lib.rs deleted file mode 100644 index 3883cec2e8..0000000000 --- a/crates/busbar-voice/src/lib.rs +++ /dev/null @@ -1,492 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! busbar-voice — the DUPLEX / LIVE-VOICE plane (Plane 4), as ONE plugin crate. -//! -//! WHAT THIS CRATE HOLDS. The plane's DECLARATIONS — [`PLANE_DECLARATION`] (a contract -//! [`busbar_contract::plane::PlaneDeclaration`], joined kernel-side to [`PLANE_HOOKS`]) and [`DECLS`] (a -//! [`busbar_kernel::proto::ProtocolDecl`] with `codec: None`) — plus the plane's OWN four-layer -//! duplex/session IR ([`ir`]) and BOTH dialect codecs (OpenAI Realtime + Gemini Live). The live pump, -//! reader/writer bodies, and session store are implemented in [`runtime`] behind the `runtime` feature -//! (see `docs/design/BUSBAR-1.6.0.md` #18/#45). -//! -//! BOOT-MOUNTING HAS LANDED — this paragraph used to say it was separate, tracked work, and that is -//! no longer true. `plane-streaming` is IN the binary's `default` feature set, and the composition root -//! installs this crate at four sites: `register_planes` pushes [`PLANE_DECLARATION`] into the plane axis, -//! `register_diagnostics` extends with [`DIAGNOSTICS`], `register_ws_arrivals` installs -//! `mount::voice_ws_arrivals()`, and `mount_root_voice` calls `mount::install_governed_calls`. The -//! shipped binary therefore serves this plane; see [`PLANE_DECLARATION`]. -//! -//! ONE PLUGIN PER PROTOCOL, the same rule `busbar-mcp` / `busbar-a2a` state: nothing in `busbar-core` -//! names this crate. Everything the plane consumes from the engine comes through the neutral -//! `busbar-substrate` surface (+ `busbar-api`); the `busbar` BINARY — the composition root — is what -//! links it and hands [`PLANE_DECLARATION`] / [`DECLS`] to the registry installers at boot. The neutral crates -//! do NOT change to accept it, and the crate is strong-form DELETABLE (`git rm -r crates/busbar-voice` -//! leaves the neutral crates compiling) — proven by `scripts/plane-delete-test.sh voice`. -//! -//! THE SUPERSET IR, EARNED (`BUSBAR-1.6.0.md` #18/#45). With TWO dialects — OpenAI Realtime and -//! Gemini Live — the plane earns a cross-dialect superset IR: a neutral vocabulary both codecs read and -//! write, so `Plane::has_superset_ir("voice")` is true — DERIVED from the length-2 `VOICE_WIRE_FORMATS` -//! (the A2A rule: a plane earns a superset at its SECOND wire format and not before). The plane realizes -//! that superset as its OWN shared IR types ([`ir`]), not the LLM-style `DialectCodec` facade, so -//! [`DECLS`] stays `codec: None` exactly as MCP / A2A do. - -/// THE DUPLEX IR AND THE DIALECT CODECS, RE-EXPORTED FROM THE PLANE CRATE'S OWN `codec` MODULE. -/// -/// The plane-4 session intermediate representation (media, control, events, tools, session config, -/// usage), the shared duplex reader/writer and the Gemini Live dialect are that module's. They are -/// re-exported HERE as `ir`, the path every caller of this crate already spells, and resolve to the -/// same items. -pub use busbar_plane_streaming::codec::ir; - -/// THE `streams:` CONFIG SECTION — the voice plane's owned config grammar ([`config::StreamsCfg`]) and -/// its `parse_section` / `default_section` seam hooks. UNCONDITIONAL (outside the `runtime` gate): -/// the plane DECLARES and PARSES `streams:` even in the default feature-off build, so config validation -/// reaches the owned grammar whether or not the live session pump is compiled in. -pub mod config; - -// The streaming plane's `streams:` grammar, which `config` carries into the kernel's registry. -use busbar_plane_streaming::config as plane_config; - -pub use busbar_plane_streaming::diagnostics; - -/// THE VOICE PLANE'S PLANE-CONTRIBUTED DIAGNOSTICS — the `&'static [&'static Diagnostic]` the -/// composition root installs via `busbar_kernel::diagnostics::install_diagnostics`, re-exported at -/// the crate root so the `busbar` binary names one stable path (`busbar_voice::DIAGNOSTICS`), exactly -/// as `busbar_mcp::DIAGNOSTICS` / `busbar_a2a::DIAGNOSTICS`. BOOTED BY THE BINARY: `main.rs`'s -/// `register_diagnostics` carries `installed.extend_from_slice(busbar_voice::DIAGNOSTICS)` under -/// `plane-streaming`, which is in `default`. (This line used to read "not yet booted … at M5"; M5 -/// landed.) See [`diagnostics`]. -pub use diagnostics::DIAGNOSTICS; - -/// THE ONE ENTRY THIS PLUGIN IS REGISTERED THROUGH — everything a composition root that linked it -/// wires, one item per registration axis, read off the crate rather than spelled at the root. The -/// root's manifest names this crate and the axes it registers on -/// (`[package.metadata.busbar.linked-axes]`: the plane — its protocol declaration is not installed -/// on the protocol axis — its owned diagnostics, its inbound WS-accept arrivals, and its realtime -/// provider composed off the deployment's catalog; the last two are wired behind `runtime`, exactly -/// as the mount hooks are); its build script turns that into one table per axis over these items, -/// and the root's source names no item of this crate. -pub mod linked { - /// The WS-accept axis and the provider-composition axis. - #[cfg(feature = "runtime")] - pub use crate::mount::{compose_plane as compose, install_arrivals as install_ws_arrivals}; - /// The diagnostics axis. - pub use crate::DIAGNOSTICS; - /// The plane axis: the contract declaration, joined kernel-side to the behaviour table. - pub use crate::{PLANE_DECLARATION, PLANE_HOOKS}; - /// The claims axis: the pure plane the composition root's boot seal registers, and the claims it - /// declares. - pub const PLANE: busbar_plane_streaming::StreamingPlane = - busbar_plane_streaming::StreamingPlane::EMPTY; - /// The bytes that plane claims. - pub const CLAIMS: &[busbar_contract::grammar::Claim] = - ::CLAIMS; -} - -// THE T2 LIVE-SESSION RUNTIME + both topologies — behind the `runtime` cargo feature (OFF by default, -// HARD RULE 4). The default / prod build compiles the IR + declarations only (no async runtime pulled -// in); turning the feature on compiles the duplex session pump, the D2 metering lease, the durable -// `SessionScope` binding, and the browser-sideband / telephony topologies — all feature-gated OFF by -// default (HARD RULE 4), not because the code is incomplete. -#[cfg(feature = "runtime")] -pub mod runtime; - -/// THE NODE'S OPEN-CALL TABLE — the client-served tool calls this node's sessions are waiting on, -/// and the port the session runtime reaches it through; composed by this plane's own `compose` step. -#[cfg(feature = "runtime")] -pub use busbar_plane_streaming::open_calls as governed; -#[cfg(feature = "runtime")] -pub mod topology; - -/// THE DATA-ROUTE MOUNT — the voice plane's `PLANE_HOOKS` `routes` / `claims` / `admission` / `build` -/// hooks and the neutral handlers behind them (see [`mount`]). Behind the `runtime` feature because the -/// route handlers open governed sessions through the T2 topologies (`crate::topology`, itself -/// runtime-gated); the default feature-off build mounts nothing and keeps the byte-unchanged -/// `PLANE_HOOKS` (the hooks stay empty/`None`, wired through the [`VOICE_CLAIMS`] / [`VOICE_ADMISSION`] / -/// [`VOICE_BUILD`] / [`VOICE_ROUTES`] cfg-split consts, exactly as [`VOICE_BUILD_RUNTIME`] is). -#[cfg(feature = "runtime")] -pub mod mount; - -/// THIS PLANE'S OWN TEST-KIT — the in-memory engine-host double, the loopback HTTP provider and the -/// metrics capture this crate's batteries drive. A plane tests ITSELF (1.6.0 Locked Decision #33: -/// "the kernel never tests a plugin"), so these doubles live HERE rather than in the deleted -/// `busbar_kernel::testkit`. Behind `runtime` for the same reason `mount`/`topology` are — every cell -/// that drives them is runtime-gated, and the doubles name the async/metrics deps that feature pulls. -#[cfg(all(feature = "runtime", any(test, feature = "test-support")))] -pub mod testkit; - -/// THE `PLANE_HOOKS.build_runtime` VALUE — wired to the real runtime constructor -/// ([`runtime::build_runtime`]) behind the `runtime` feature, `None` when the feature is off so the -/// default `PLANE_HOOKS` is byte-unchanged. Split by `cfg` because the -/// `runtime` module (and its constructor) only exist behind the feature. -// `type_complexity`: this fn-pointer is the mirror of the frozen `PlaneDecl::build_runtime` field type -// (`busbar_kernel::plane::registry`), which carries the SAME `#[allow(clippy::type_complexity)]` — the -// shape is the ABI, not a factorable local type. -#[cfg(feature = "runtime")] -#[allow(clippy::type_complexity)] -const VOICE_BUILD_RUNTIME: Option< - fn( - &dyn std::any::Any, - Option<&dyn busbar_kernel::plane_host::PlaneSlots>, - ) -> std::sync::Arc, -> = Some(runtime::build_runtime); -#[cfg(not(feature = "runtime"))] -#[allow(clippy::type_complexity)] -const VOICE_BUILD_RUNTIME: Option< - fn( - &dyn std::any::Any, - Option<&dyn busbar_kernel::plane_host::PlaneSlots>, - ) -> std::sync::Arc, -> = None; - -// ── THE DATA-ROUTE MOUNT HOOKS — cfg-split exactly as `VOICE_BUILD_RUNTIME`, so the default -// (feature-off) `PLANE_HOOKS` is BYTE-UNCHANGED (claims empty, admits no one, builds no slot, mounts no -// route) and the `runtime` build wires the real neutral hooks in `crate::mount`. Route-mounting the -// pump needs the T2 topologies (runtime-gated) to open governed sessions, so these arm in lock-step -// with the runtime — a plane installed at boot (only under `plane-streaming`, which turns on -// `busbar-voice/runtime`) both mounts and admits, keeping the ratchet's "mounted ⇒ admitted" true. - -/// `PLANE_HOOKS.build` — construct the per-generation dispatch slot from `public_url` (the audience), -/// `crate::mount::voice_build` with the feature on; `|_| None` (no slot) with it off. -#[cfg(feature = "runtime")] -const VOICE_BUILD: fn( - &busbar_kernel::plane::registry::BuildCtx, -) -> Option> = mount::voice_build; -#[cfg(not(feature = "runtime"))] -const VOICE_BUILD: fn( - &busbar_kernel::plane::registry::BuildCtx, -) -> Option> = |_ctx| None; - -/// `PLANE_HOOKS.claims` — the one audience-checked base the plane answers on, or nothing off-feature. -#[cfg(feature = "runtime")] -const VOICE_CLAIMS: fn(&dyn std::any::Any) -> Vec<(String, &'static str)> = mount::voice_claims; -#[cfg(not(feature = "runtime"))] -const VOICE_CLAIMS: fn(&dyn std::any::Any) -> Vec<(String, &'static str)> = |_slot| Vec::new(); - -/// `PLANE_HOOKS.admission` — the RFC 8707 audience bound from `public_url`, or `None` off-feature. -#[cfg(feature = "runtime")] -const VOICE_ADMISSION: fn(&dyn std::any::Any) -> Option = - mount::voice_admission; -#[cfg(not(feature = "runtime"))] -const VOICE_ADMISSION: fn(&dyn std::any::Any) -> Option = - |_slot| None; - -/// `PLANE_HOOKS.routes` — the four neutral ingress routes, or `None` (no data path) off-feature. -#[cfg(feature = "runtime")] -#[allow(clippy::type_complexity)] -const VOICE_ROUTES: Option< - fn(&dyn std::any::Any) -> Vec, -> = Some(mount::voice_routes); -#[cfg(not(feature = "runtime"))] -#[allow(clippy::type_complexity)] -const VOICE_ROUTES: Option< - fn(&dyn std::any::Any) -> Vec, -> = None; - -/// `PLANE_HOOKS.hydrate` — boot-rehydrate the durable voice-session working-set before any listener -/// binds ([`mount::voice_hydrate`]); `None` off-feature so the default decl is byte-unchanged. -#[cfg(feature = "runtime")] -const VOICE_HYDRATE: Option = Some(mount::voice_hydrate); -#[cfg(not(feature = "runtime"))] -const VOICE_HYDRATE: Option = None; - -/// `PLANE_HOOKS.start` — the post-listener boot step ([`mount::voice_start`]); `None` off-feature so the -/// default decl is byte-unchanged. -#[cfg(feature = "runtime")] -const VOICE_START: Option = Some(mount::voice_start); -#[cfg(not(feature = "runtime"))] -const VOICE_START: Option = None; - -/// THE VOICE (STREAMING) PLANE'S CAPABILITY KEY (`"voice"`) — the string the composition root flips -/// onto the unified kernel loop's SESSION admit and the same string the voice session gauntlet reports -/// from its `GauntletPlane::capability_key`. Re-exported at the crate root so the `busbar` binary names -/// ONE stable path (`busbar_voice::PLANE_KEY`) and the plane and the flip cannot drift onto two. -pub use busbar_plane_streaming::codec::PLANE_KEY; - -/// The two dialect names this plane speaks (the streaming plane's own declarations): the [`DECLS`] -/// registry key is the first, and both are the plane's [`PLANE_HOOKS`] wire formats. -pub use busbar_plane_streaming::provider::{GEMINI_LIVE, OPENAI_REALTIME}; - -// A dialect's provider address rules (the streaming plane's), read by the mount's provider dial -// (runtime-only, as the mount is). -#[cfg(feature = "runtime")] -use busbar_plane_streaming::provider as plane_provider; - -// A live session's durable row (the streaming plane's shape), bound by the runtime's session scope. -#[cfg(feature = "runtime")] -use busbar_plane_streaming::session_row as plane_session_row; - -/// THE TWO WIRE FORMATS this plane translates: OpenAI Realtime and Gemini Live. Its length (== 2) is -/// what EARNS this plane a superset IR (`Plane::has_superset_ir` is DERIVED from this list's length), -/// the A2A discipline — a plane earns a superset at its SECOND wire format and not before. -const VOICE_WIRE_FORMATS: &[&str] = &[OPENAI_REALTIME, GEMINI_LIVE]; - -/// THE VOICE PLANE'S PROVIDER-FACING BEARER — the ONE construction that plans the provider-facing -/// `Authorization: Bearer ` from the RESOLVED provider-credential string, and NEVER a -/// caller/governance token passed through. Both [`voice_egress_auth_headers`] (the -/// `egress_auth_headers` decl) and the LIVE one-shot serving passes ([`mount::serve_sdp`]) authenticate -/// the provider hop through THIS function, so the lane-constant builder the `egress_tests` battery -/// proves is the SAME code the live SDP broker runs — the tested builder IS the live credential path. -/// It authenticates busbar's OWN authority to the provider (the real provider key on the broker hop), -/// never the caller's inbound bearer (which the Auth plugin already consumed at the door). -/// -/// NOTE — the SDP broker authenticates with the PROVIDER key (busbar brokers the call server-side). -/// An `ek_`-RELAY model (forward the browser's ephemeral secret as the upstream credential) is a -/// distinct protocol shape: it needs a resolved `ek_` source — the inbound `RouteAuth::Key` -/// `Authorization` slot carries the caller's GOVERNANCE key (forwarding it upstream would leak -/// busbar's own authority), so an `ek_` relay would have to arrive on a non-`Authorization` inbound -/// slot or be minted+persisted server-side. That is a flagged protocol follow-on, not this hop. -/// -/// A key that is not a legal header value (a stray CR/LF/NUL a config system injected) yields NO -/// header — the upstream then 401s — and one coded diagnostic naming the protocol; the key itself -/// is never logged. -pub(crate) fn voice_provider_bearer( - key: &str, -) -> Vec<(axum::http::HeaderName, axum::http::HeaderValue)> { - use busbar_contract::header::{is_legal_header_value, token_value}; - #[cfg(feature = "runtime")] - use busbar_kernel::diagnostics::{diag_debug, PROTO_AUTH_INVALID_HEADER_BYTES as BAD_BYTES}; - let value = is_legal_header_value(key) - .then(|| axum::http::HeaderValue::from_str(&token_value(key)).ok()) - .flatten(); - let Some(value) = value else { - // The coded line rides the runtime's logging (the serving build's). - #[cfg(feature = "runtime")] - diag_debug!( - BAD_BYTES, - protocol = OPENAI_REALTIME, - "authorization credential contains invalid header bytes (ASCII control character); \ - omitting auth header — upstream will reject with 401" - ); - return Vec::new(); - }; - vec![(axum::http::header::AUTHORIZATION, value)] -} - -/// `ProtocolDecl::egress_auth_headers` — the plain-Bearer arm of `busbar-llm`'s OpenAI dialect (the -/// Bedrock module hands in its SigV4 signer the same way). A pure function of the resolved credential -/// string (it reads nothing off the [`SigningContext`]), so [`DECLS`] declares it LANE-CONSTANT. -fn voice_egress_auth_headers( - key: &str, - _ctx: &busbar_kernel::proto::SigningContext, -) -> Vec<(axum::http::HeaderName, axum::http::HeaderValue)> { - voice_provider_bearer(key) -} - -use busbar_contract::{ - plane::{BillableClass, PER_SESSION, TOKEN_FAMILY}, - plugin::Kind, -}; -use busbar_kernel::plane::registry::PlaneHooks; - -/// THE VOICE PLANE'S DECLARATION — contract data the composition root registers at boot, joined -/// kernel-side to [`PLANE_HOOKS`], so the `busbar` binary names one stable path -/// (`busbar_voice::PLANE_DECLARATION`). Together the two declare the plane's identity -/// (key, config section, audit kind, wire formats), is INSTALLED at boot behind the `plane-streaming` -/// feature with `build_runtime` wired to the real runtime constructor, and — behind the `runtime` -/// feature (which `plane-streaming` turns on) — MOUNTS its data plane: `build` erases the dispatch slot from -/// `public_url`, `claims`/`admission` bind the plane's RFC 8707 audience, and `routes` mounts the five -/// ingress doors across both dialects (see [`mount`]), and the BOOT hooks `hydrate` / `start` rehydrate the durable session -/// working-set before the listener and confirm readiness after. What stays `None` is the `ProtocolDecl` -/// `handler`: a long-lived duplex SESSION is not a one-shot `RequestHandler` (whose `OperationHandler` -/// cells are request→response codecs), so the session driver lives in the plane's own runtime -/// (`crate::topology`) over the neutral pump, not on that field. The neutral registry unions this -/// without naming it (the MCP/A2A precedent). -pub const PLANE_DECLARATION: busbar_contract::plane::PlaneDeclaration = - busbar_contract::plane::PlaneDeclaration { - key: "streaming", - // A MOUNTED plane, not the fallback catch-all. - fallback: false, - // THE DUPLEX / LIVE-VOICE PLANE'S DECLARING SECTION is `streams:` (1.6.0 config-seam KEYSTONE - // stage C) — the top-level noun whose mere existence declares this plane, the fourth plane - // noun beside `pools:`/`tools:`/`agents:`. The registry `key` stays the crate identity - // (`"voice"`), so key ≠ config_section here exactly as the MCP plane's key is `"mcp"` while its - // declaring section is `"tools:"`. The plane parses `streams:` through the seam - // (`parse_section`) once its config grammar lands; the plane declares the noun now so - // core names no `streams`/`voice` parse target and voice can register `streams:` and boot. - config_section: "streams", - // One session is granted at the whole-session granularity. - scope_kinds: &["session"], - subject_noun: "streaming session", - admin_noun: "streaming-session", - audit_kind: "streaming_session", - card_signing_domain: None, - card_kid_prefix: None, - // config-seam: voice OWNS the `streams:` grammar. `"streams"` is NOT in core's - // `CORE_OWNED_CONCRETE_SECTIONS` (providers/models/pools/rate_card/limits), so the dup-claim - // guard admits this claim; a second claimant of `streams` is refused by construction. - owned_config_sections: &["streams"], - // The classes the plane crate declares (`busbar_plane_streaming::meta`), by its own symbols. - // - // `cached_tokens` is deliberately NOT here (architect ruling #71, money-model integrity): the - // dialects' own `cached_tokens` figure is a SUBSET of the input token classes above (OpenAI - // Realtime reports it INSIDE `input_token_details`; Gemini Live the same, inside - // `promptTokensDetails`), so a rate card that priced it too would bill the same cached input - // twice. A billable class must be disjoint from every other one the plane declares; a subset - // measure is attribution, not a class. The figure is still carried, unbilled, as the - // `cached_tokens` content fact (`busbar_plane_streaming::meta::FACT_CACHED_TOKENS`). - billable_classes: &[ - BillableClass { - class: busbar_plane_streaming::meta::CLASS_AUDIO_TOKENS_IN.as_str(), - family: TOKEN_FAMILY, - }, - BillableClass { - class: busbar_plane_streaming::meta::CLASS_AUDIO_TOKENS_OUT.as_str(), - family: TOKEN_FAMILY, - }, - BillableClass { - class: busbar_plane_streaming::meta::CLASS_TEXT_TOKENS_IN.as_str(), - family: TOKEN_FAMILY, - }, - BillableClass { - class: busbar_plane_streaming::meta::CLASS_TEXT_TOKENS_OUT.as_str(), - family: TOKEN_FAMILY, - }, - BillableClass { - class: busbar_plane_streaming::meta::CLASS_AUDIO_SECONDS_IN.as_str(), - family: "duration", - }, - BillableClass { - class: busbar_plane_streaming::meta::CLASS_TOOL_CALLS.as_str(), - family: "count", - }, - ], - // The fee unit this plane counts: one per opened session (`SessionAccount::open`). A turn never - // passes per-request admission, so `streams.fees.per_request` would charge nothing — refused. - fee_units: &[PER_SESSION], - metric_families: &[], - record_kinds: &[], - served_op_classes: &[], - // The realtime provider is resolved through the deployment's `providers:` catalog (the - // model this plane's section names, then that model's provider), so this plane consumes - // `providers`. `models` belongs to the plane that owns it. - required_config_sections: &[Kind::Transport.root()], - trust_keys: &[], - caller_credential_refusal: None, - }; - -/// THE PLANE'S BEHAVIOUR — every hook the kernel runs for it, handed over BESIDE -/// [`PLANE_DECLARATION`] and joined to it kernel-side (`PlaneDecl::assemble`). The registration item -/// is the declaration, which names no kernel type; this table is typed by kernel seams and stays on -/// the kernel side of every fold. -pub const PLANE_HOOKS: PlaneHooks = PlaneHooks { - // TWO dialects ⇒ superset IR, DERIVED from this list's length (see VOICE_WIRE_FORMATS). - wire_format_names: || VOICE_WIRE_FORMATS, - // MOUNTED (behind `runtime`): the plane builds its dispatch slot from `public_url`, claims TWO - // audience-checked bases — `/v1/realtime` (OpenAI) and `/v1/realtime/gemini` (Gemini Live) — - // under the SAME bound audience, and mounts the five ingress routes across both dialects, whose - // handlers open governed sessions through `run_gauntlet_session` (see `crate::mount`). - // Off-feature these stay empty/`None` (the byte-unchanged default decl). A plane installed at - // boot is installed under `plane-streaming` (⇒ `busbar-voice/runtime`), so it always both mounts and - // admits — the ratchet's "mounted ⇒ admitted" holds by construction. - claims: VOICE_CLAIMS, - admission: VOICE_ADMISSION, - build: VOICE_BUILD, - routes: VOICE_ROUTES, - admin_routes: None, - openapi: None, - // BOOT hooks — rehydrate the durable session working-set before the listener, then the - // post-listener readiness step. Wired behind `runtime` (see [`VOICE_HYDRATE`] / [`VOICE_START`]); - // `None` off-feature so the default decl is byte-unchanged. - hydrate: VOICE_HYDRATE, - start: VOICE_START, - config_validate: None, - named_def_list: None, - named_def_get: None, - registry_contains: None, - reresolve_gates: None, - openapi_schemas: None, - on_swap: None, - // config-seam: voice PARSES its owned `streams:` section through its own typed `StreamsCfg`, - // so `DeployCfg` names no `busbar_voice` type (the MCP `mcp_parse_section` / A2A - // `a2a_parse_section` pattern). UNCONDITIONAL — the hook lives outside the `runtime` gate so - // the default feature-off build validates `streams:` too. - parse_section: Some(config::streams_parse_section), - parse_endpoint: None, - lower_endpoint: None, - // RUNTIME HOOK — wired to the real per-generation runtime constructor behind the `runtime` - // feature (see [`VOICE_BUILD_RUNTIME`]); `None` when the feature is off so the default build is - // byte-unchanged. The DATA-plane hooks (`build` / `claims` / `admission` / `routes`) and the - // BOOT hooks (`hydrate` / `start`) are wired too (see [`mount`]) — all behind `runtime`. - build_runtime: VOICE_BUILD_RUNTIME, - viewer: None, - retain_verify_gates: None, - // config-seam: the empty `streams:` default — the plane's own statement that an ABSENT section - // is byte-identically `StreamsCfg::default()` (mirror of `a2a_default_section` / - // `mcp_default_section`). The kernel's declared-section carrier holds no entry for an absent - // section; this hook is what that absence means to the plane. - default_section: Some(config::streams_default_section), - resolve_provider: None, -}; - -/// THE VOICE PLANE'S PROTOCOL DECLARATION — a `ProtocolDecl` with `codec: None`, re-exported at the -/// crate root so the `busbar` binary names one stable path (`busbar_voice::DECLS`). Like MCP / A2A, it -/// declares NO codec even though the plane now speaks TWO dialects: the plane realizes its cross-dialect -/// superset as its OWN shared IR types (the [`ir`] module's `DuplexReader` / `DuplexWriter` pair, one -/// per dialect), not the LLM-style `DialectCodec` facade the `codec` field carries. -/// -/// NOT YET MOUNTED: `handler: None` and `verbs: &[]` — route-mounting the duplex handler / -/// gauntlet-session entry is follow-on work. Every other field carries the neutral default a codec-less -/// protocol declares (the MCP `DECL` shape). -pub static DECLS: busbar_kernel::proto::ProtocolDecl = busbar_kernel::proto::ProtocolDecl { - name: OPENAI_REALTIME, - // THE SUPERSET IS ITS OWN IR (`BUSBAR-1.6.0.md` #18/#45): the two dialects meet in the plane's - // shared IR types, not a `DialectCodec` facade — so this field stays `None`, the MCP/A2A precedent. - codec: None, - // NOT YET MOUNTED: no request handler wired here yet — the duplex pump exists in `crate::runtime`; - // route-mounting its entry point is follow-on work. - handler: None, - // NOT YET MOUNTED: no verbs declared yet (the long-lived Subscribe/Control shapes arrive with the - // boot-mounting pass). - verbs: &[], - head_keys: &[], - streaming_content_type: None, - array_stream_shim_key: None, - native_tool_id_prefix: None, - ingress_auth: busbar_kernel::proto::IngressAuth::Bearer, - // THE ONE EGRESS CREDENTIAL MECHANISM: the provider bearer / WebRTC `ek_` / telephony carrier - // credential is planned onto the dial's headers HERE, never a caller token passed through (see - // [`voice_egress_auth_headers`]). LANE-CONSTANT: the builder is a pure function of the resolved - // credential string and reads nothing off the `SigningContext`, so the boot path may prebuild the - // header set once per lane (the plain-Bearer discipline the LLM plane's OpenAI dialect declares). - egress_auth_headers: Some(voice_egress_auth_headers), - egress_auth_lane_constant: true, - egress_scheme: None, - stream_usage_requires_opt_in: false, - // Promoted writer facts: this plane declares no cross-dialect codec and has no writer, so every - // promoted fact is the `ProtocolWriter` trait DEFAULT — the same values the codec-less MCP `DECL` - // states. Inert for a `codec: None` protocol, but the declaration must state them. - requires_max_tokens: false, - stop_sequence_cap: None, - cache_markers_model_gated: false, - fills_thought_signature: false, - frame_after_message_start: None, - reshapes_body_at_path_base: false, - max_cache_control_breakpoints: None, - quota_exceeded_status: axum::http::StatusCode::TOO_MANY_REQUESTS, - ingress_is_eventstream: false, - emits_sse_done_terminator: false, - max_citations_per_delta: None, - egress_user_agent: busbar_kernel::proxy::EGRESS_UA_DEFAULT, - has_model_in_url: false, - auth_failure_status_and_kind: ( - axum::http::StatusCode::UNAUTHORIZED, - busbar_kernel::proto::ERR_TYPE_AUTHENTICATION, - ), - ingress_relays_amzn_headers: false, - ingress_relayed_response_header_names: &[], - auth_failure_message: "authentication failed", - uses_array_stream_shim: false, - has_native_path_not_found: false, - egress_stream_accept: busbar_kernel::proxy::TEXT_EVENT_STREAM, - models_list_envelope: None, - // Identified by its EXPLICIT mount, never by a wire fingerprint — so it claims no router or - // residual rung, contributes no vendor response metadata, and is not the residual default. - claims: None, - residual_claims: None, - residual_default: false, - vendor_response_metadata: None, - list_models_fingerprint_headers: &[], - static_headers: &[], -}; diff --git a/crates/busbar-voice/src/mount.rs b/crates/busbar-voice/src/mount.rs deleted file mode 100644 index fefead2989..0000000000 --- a/crates/busbar-voice/src/mount.rs +++ /dev/null @@ -1,1708 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE VOICE PLANE'S DATA-ROUTE MOUNT (behind the `runtime` feature) — the structural surface that -//! turns `PLANE_HOOKS`' `routes` / `claims` / `admission` / `build` hooks from `None`/empty into the -//! plane's real ingress door. -//! -//! ## The two slots, and which one this is -//! -//! Voice contributes a DISPATCH slot ([`VoiceMount`]) through [`PlaneDecl::build`], carried in -//! `plane_slots` under the plane's decl key (`"voice"`). It is the object [`voice_claims`] / -//! [`voice_admission`] / [`voice_routes`] all read, and the SAME object the core route adapter hands a -//! handler as `PlaneReqCtx::slot`. It carries the plane's RFC 8707 audience (derived from the -//! deployment's `public_url`, the A2A precedent — one reading so the audience a caller is told to ask -//! for and the one busbar demands cannot drift apart) plus a live [`VoiceRuntime`] the route handlers -//! open governed sessions from. `None` when there is no `public_url` — a deployment with no receiving -//! origin fronts nothing, claims nothing and admits no one (the A2A delegation-only asymmetry). -//! -//! ## Five routes, two dialects, and what is live -//! -//! Every route MOUNTS, is AUDIENCE-CHECKED (`RouteAuth::Key`, under the plane's one audience) and, on -//! arrival, runs the governed session-open through [`crate::topology::begin_session`] / -//! [`crate::topology::telephony::begin_telephony`] — which go through `run_gauntlet_session` -//! (verify-strictly-before-charge). The two one-shot HTTP passes (`ek_` mint, SDP broker) reach the -//! provider once one is composed ([`install_provider`]); the browser-sideband WS accept is -//! control-only by design (media is peer-to-peer, see `crate::topology::webrtc`); the telephony and -//! Gemini Live WS accepts DIAL the composed provider through [`crate::topology::dial_provider`] and -//! proxy both sockets (see [`ws_accept`]) once one is composed. With no provider composed, every leg -//! still governs and mounts — it serves the client socket only, exactly as documented in -//! `docs/voice.md`. - -use crate::ir::codec::gemini::GeminiLiveCodec; -use crate::ir::codec::{DuplexReader, DuplexWriter, OpenAiRealtimeCodec}; -use crate::ir::config::SessionConfig; -use crate::plane_provider::{credential_placement, provider_ws_url, CredentialPlacement}; -use crate::runtime::carrier::Carrier; -use crate::runtime::scope::SessionHandle; -use crate::runtime::session::{serve_to_teardown, serve_with_sweep, UplinkForwarder, VoiceSession}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::topology::minter_https::HttpsTokenMinter; -use crate::topology::telephony::{begin_telephony, g711_config, open_admitted_telephony}; -use crate::topology::webrtc::TokenMinter; -use crate::topology::{ - begin_session, dial_provider, open_admitted_session, stream_breaker_key, SessionGauntlet, - StartError, -}; -use busbar_kernel::config::RootCfg; -use busbar_kernel::door::UnitKeyMint; -use busbar_kernel::egress::{ - duplex_ws::{CredentialPlacement as WsPlacement, DialCredential}, - engine::{send_bounded, EngineClient}, -}; -use busbar_kernel::ingress::byte_duplex::serve_messages; -use busbar_kernel::ingress::duplex_ws::{ - accept_gauntlet, install_ws_arrivals, WsAcceptFuture, WsArrival, WsArrivalSpec, -}; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use busbar_kernel::plane::observe::Counted; -use busbar_kernel::plane::registry::{BuildCtx, PlaneBootCtx}; -use busbar_kernel::plane::PlaneAdmission; -use busbar_kernel::plane_host::{ - admission_gates_attached, admission_gates_decide, admission_tap_attached, admission_transform, - EngineHost, GateOutcome, TransformVerdict, -}; -use busbar_kernel::plane_host::{GauntletPlane, GauntletRequest}; -use busbar_kernel::plane_routes::PlaneRouteSpec; -use busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy; -use bytes::Bytes; -use http_body_util::{BodyExt, Full}; -use std::any::Any; -use std::sync::Arc; - -/// The bounded `pool` label the voice FRONT DOOR (the voice-server cell) emits under — a constant, not -/// a caller value, so the series count stays bounded. The outbound provider dial (the voice-client -/// cell) counts on its own `busbar_upstream_attempts_total` family in `topology::dial_provider`. -const FRONT_DOOR_POOL: &str = "streaming-server"; - -/// The session-open method label the hook gate/tap projection carries — the single operation a voice -/// front-door request performs. Bounded (one value), so it is safe as the hook `tool` slot the -/// operator's gate reads. -const SESSION_OPEN_METHOD: &str = "session.open"; - -/// THE ADMIN-AUDIT ACTION every voice session-open mutation lands under. Named ONCE here because two -/// sites write it: the APPLIED row a governed open seals ([`crate::topology::begin_session`]) and the -/// REJECTED row a committed-but-unreadable rewrite refuses under ([`hook_tap`]). An operator reading -/// the audit stream must see ONE action with two outcomes, not two spellings that drift apart. -pub(crate) const SESSION_AUDIT_ACTION: &str = "streaming.session.open"; - -/// THE HOOK CONTAINER the voice plane's session-open gate/tap fire under — the plane's SINGULAR config -/// section noun (`streams`), since voice declares no per-registration container (its config is one -/// object, not a named-definition map). The operator attaches a session-open gate to this ONE -/// container, and `admission_gates_attached(host, plane_key, container)` reads it here. -const GATE_CONTAINER: &str = "streams"; - -/// THE SCOPE KIND a key must hold to open a live session — the plane's one declared scope kind (see -/// `PLANE_DECLARATION.scope_kinds`). Named once here, so the vocabulary an operator writes in -/// `allowed_scopes: [{ kind: session, value: … }]` and the vocabulary the door demands cannot drift. -const SESSION_SCOPE_KIND: &str = "session"; - -/// MAY THIS KEY OPEN A LIVE SESSION? The plane's authorization question, asked of the presenting key's -/// own grant and nothing else — the same shape MCP asks about a tool and A2A asks about an agent. -/// -/// The value asked about is the voice front door's pool ([`FRONT_DOOR_POOL`]), which is the one pool a -/// voice session is ever served on, so an operator narrows a key to voice by granting -/// `{ kind: session, value: voice-server }` and nothing wider. A key with NO scope list at all is the -/// store's wildcard and is granted every kind, exactly as it is on every other plane; a key that -/// carries a list must have this entry in it. -#[must_use] -pub fn session_scope_allowed(key: &busbar_contract::records::VirtualKey) -> bool { - key.scope_allowed(SESSION_SCOPE_KIND, FRONT_DOOR_POOL) -} - -/// The refusal a key without session scope gets: the plane's own fail-closed answer, in the same -/// plain-text shape every other voice refusal takes, and BEFORE any hook, account, durable row or dial. -fn session_scope_refusal() -> axum::response::Response { - refusal( - axum::http::StatusCode::FORBIDDEN, - "streaming session-open refused: the presenting key holds no session scope for this streaming pool \ - (fail closed)", - ) -} - -/// A CONFIGURED PROVIDER ENDPOINT the one-shot mint / SDP-broker passes reach the realtime provider -/// through — the base origin plus the real server-side key. Composed by the composition root through -/// [`install_provider`]; a loopback test injects one directly into [`open_governed`] instead. The real -/// key stays server-side: it authenticates only the busbar↔provider hop and never reaches a browser -/// payload, and it is never rendered into any public accessor here. -pub(crate) struct ProviderEndpoint { - /// The provider origin (scheme + authority, e.g. `https://api.openai.com`). - pub base_url: String, - /// The REAL provider key, held server-side and `Redacted`: exposed only at the two sites that - /// put it on the provider hop (the mint minter, the SDP bearer). The WebSocket dial hands it, - /// still `Redacted`, to the substrate dialer, which writes it into the upgrade request only. - pub api_key: busbar_contract::Redacted, -} - -impl ProviderEndpoint { - /// The KEYLESS provider WebSocket URL for `dialect`. - pub(crate) fn ws_url(&self, dialect: &str) -> String { - provider_ws_url(&self.base_url, dialect) - } - - /// The provider key and where `dialect` presents it on the WebSocket dial. - pub(crate) fn ws_credential(&self, dialect: &str) -> DialCredential<'_> { - let placement = match credential_placement(dialect) { - CredentialPlacement::Query(name) => WsPlacement::Query(name), - CredentialPlacement::Header { name, prefix } => WsPlacement::Header { name, prefix }, - }; - DialCredential { - placement, - secret: &self.api_key, - } - } -} - -/// THE COMPOSED REALTIME PROVIDER — the one endpoint the mint / SDP-broker passes dial, written once -/// by the composition root and read by every route thereafter. -/// -/// SET-ONCE, first writer wins, exactly like the composition root's other process-wide installs (the -/// WS-accept arrivals, the hostless-egress driver). It is written after the deployment's config -/// resolves — which is later than the per-generation dispatch slot is built — so the routes read it -/// HERE rather than off the slot, and a config apply that rebuilds the slot cannot drop it. -static COMPOSED_PROVIDER: std::sync::OnceLock = std::sync::OnceLock::new(); - -/// COMPOSE the realtime provider endpoint the mint / SDP-broker passes authenticate with — the -/// composition root's one write of the voice plane's egress credential. -/// -/// `base_url` is the provider origin and `api_key` the credential the deployment already resolved -/// through its ordinary provider catalog and secret resolver — the plane's `streams:` grammar carries -/// no credential field and gains none. Returns `false` when an endpoint was already composed (the -/// first write stands), so a second caller is a no-op rather than a silent credential swap. -pub fn install_provider(base_url: impl Into, api_key: impl Into) -> bool { - COMPOSED_PROVIDER - .set(ProviderEndpoint { - base_url: base_url.into(), - api_key: busbar_contract::Redacted::new(api_key.into()), - }) - .is_ok() -} - -/// COMPOSE the realtime provider endpoint from a provider-catalog entry — the form the composition -/// root calls, so the plane resolves its own credential through the deployment's ordinary secret -/// resolver rather than the root handing plaintext across. -/// -/// `base_url` and `api_key` are the origin and the secret REFERENCE the deployment already declared -/// for this provider in its provider catalog; `resolver` is the neutral secret-resolver seam (the -/// same one every other credential in the deployment is resolved through, built-in `env`/`file` plus -/// any `kind: secret` module). Fails closed with the resolver's own message: an unresolvable -/// reference composes nothing, so the mint / SDP routes keep answering "no provider composed" rather -/// than dialing with an empty credential. `Ok(false)` means an endpoint was already composed. -pub fn compose_provider( - base_url: impl Into, - api_key: &busbar_contract::secret_ref::SecretRef, - resolver: &dyn busbar_contract::secret::SecretResolve, -) -> Result { - let resolved = resolver.resolve_string(api_key)?; - Ok(install_provider(base_url, resolved)) -} - -/// The composed provider endpoint, or `None` when the composition root composed none — in which case -/// the mint / SDP routes still answer "governed, but no provider credential composed". -pub(crate) fn composed_provider() -> Option<&'static ProviderEndpoint> { - COMPOSED_PROVIDER.get() -} - -/// Whether a realtime provider endpoint has been composed — i.e. whether the mint / SDP passes serve -/// rather than answering "no provider composed". The credential itself is never exposed. -#[must_use] -pub fn provider_composed() -> bool { - COMPOSED_PROVIDER.get().is_some() -} - -/// The composed provider's ORIGIN (never its key) — the non-secret half, for a boot log or a -/// conformance probe that needs to confirm which endpoint this deployment composed. -#[must_use] -pub fn composed_provider_base_url() -> Option<&'static str> { - COMPOSED_PROVIDER.get().map(|p| p.base_url.as_str()) -} - -// ── THE SECOND DIALECT'S PROVIDER ENDPOINT (the second-dialect route) ───────────────────────────────────────────────────── -// -// The K1 provider seam above is single-endpoint and OpenAI-shaped: one `OnceLock`, one credential, one -// `Authorization: Bearer` scheme. Gemini Live's provider hop authenticates with a DIFFERENT native -// scheme (`x-goog-api-key`, never `Authorization`), so it cannot share the OpenAI endpoint without -// silently reusing the wrong header shape. A second, independently-composed endpoint is the fix: same -// set-once discipline, same fail-closed resolve, keyed to its own dialect rather than folded into the -// first. - -/// A COMPOSED REALTIME PROVIDER — the ONE endpoint the Gemini Live route dials, written once by the -/// composition root and read by [`ws_accept`] (via [`composed_gemini_provider`]) thereafter. SET-ONCE, first writer -/// wins, exactly like [`COMPOSED_PROVIDER`] — a second compose is a no-op, never a silent swap. -static COMPOSED_PROVIDER_GEMINI: std::sync::OnceLock = std::sync::OnceLock::new(); - -/// COMPOSE the Gemini Live provider endpoint the plane's Gemini route dials — the Gemini twin of -/// [`install_provider`]. `api_key` is the RAW resolved credential (Gemini's native `x-goog-api-key` -/// value, never wrapped as a bearer token); the plane never renders it into a public accessor. -pub fn install_gemini_provider(base_url: impl Into, api_key: impl Into) -> bool { - COMPOSED_PROVIDER_GEMINI - .set(ProviderEndpoint { - base_url: base_url.into(), - api_key: busbar_contract::Redacted::new(api_key.into()), - }) - .is_ok() -} - -/// COMPOSE the Gemini Live provider endpoint from a provider-catalog entry — the Gemini twin of -/// [`compose_provider`]: resolves `api_key` through the deployment's neutral secret resolver and fails -/// closed (composes nothing) on an unresolvable reference. -pub fn compose_gemini_provider( - base_url: impl Into, - api_key: &busbar_contract::secret_ref::SecretRef, - resolver: &dyn busbar_contract::secret::SecretResolve, -) -> Result { - let resolved = resolver.resolve_string(api_key)?; - Ok(install_gemini_provider(base_url, resolved)) -} - -/// A provider composition step: captured off the resolved configuration, run once the deployment's -/// secret resolver exists (the root's `compose` axis shape). -pub type Compose = Box; - -/// THE PLANE'S PROVIDER, COMPOSED FROM THE DEPLOYMENT'S ORDINARY CATALOG — this plane's `compose` entry -/// hook ([`crate::linked`]). The `streams:` grammar carries no credential field, so the realtime -/// provider is the one already serving the model that section targets: `streams.session.model` names -/// a model, the model names its provider, and that provider entry carries the origin and the secret -/// reference every other lane's key is declared as. Captured off the resolved configuration BEFORE -/// the app is built; the returned step runs once the resolver that turns a reference into a -/// credential exists. A deployment with no `streams:` block pins no model and captures nothing. -/// -/// Silent on a deployment that captured nothing; the only lines this emits are the fail-closed -/// warnings when a reference the operator DID declare will not resolve. They are the composition -/// root's lines (its log target), because it is the root's boot that is reporting them. -pub fn compose_from_config(cfg: &RootCfg) -> Option { - let (base_url, api_key) = crate::config::configured_session_model() - .and_then(|model| cfg.models.get(&model).map(|m| m.provider.clone())) - .and_then(|provider| cfg.providers.get(&provider)) - .map(|p| (p.base_url.clone(), p.api_key.clone()))?; - Some(Box::new(move |resolver| { - if let Err(e) = compose_provider(base_url.clone(), &api_key, resolver) { - tracing::warn!( - target: "busbar", - "streaming: the realtime provider credential did not resolve, so the streaming mint and SDP \ - routes stay uncomposed: {e}" - ); - } - // K4: THE GEMINI LIVE ROUTE'S PROVIDER, composed under its OWN endpoint (a separate set-once - // slot, `x-goog-api-key` scheme) rather than reusing the OpenAI one's — a deployment cannot - // silently point one dialect's traffic at the other's credential. `streams:` still names ONE - // model, so both endpoints are composed from the SAME resolved (origin, reference) pair; a - // deployment that fronts Gemini Live through a distinct provider entry needs a second - // `streams:` knob to name it, which is not this cycle's grammar change (see docs/voice.md). - if let Err(e) = compose_gemini_provider(base_url, &api_key, resolver) { - tracing::warn!( - target: "busbar", - "streaming: the Gemini Live provider credential did not resolve, so the Gemini route \ - stays uncomposed: {e}" - ); - } - })) -} - -/// THIS PLANE'S `compose` ENTRY ([`crate::linked`]): compose the node's open-call table onto the -/// served door, then capture the provider off the deployment's catalog ([`compose_from_config`]). -/// -/// The table is the plane's own ([`crate::governed`]): nothing configuration decides is in it — it is -/// empty at boot and its whole contents are what the sessions this node serves have opened since — -/// so it is composed here, on the boot slot every linked plane's compose step runs in, before any -/// listener binds. From then on every session the served door opens is a governed one. -pub fn compose_plane(cfg: &RootCfg) -> Option { - compose_node_calls(); - compose_from_config(cfg) -} - -/// COMPOSE the node's open-call table onto the served door: one [`crate::governed::OpenToolCalls`] -/// behind the port the session runtime reaches it through. Set once — a second compose is a no-op -/// rather than a silent swap of the table live sessions are already keyed into. -pub fn compose_node_calls() -> bool { - let keys = UnitKeyMint::default(); - install_governed_calls(Arc::new(crate::governed::NodeCalls::new( - Arc::new(crate::governed::OpenToolCalls::new()), - Box::new(move || keys.mint()), - ))) -} - -/// The composed Gemini Live provider endpoint, or `None` when the composition root composed none. -pub(crate) fn composed_gemini_provider() -> Option<&'static ProviderEndpoint> { - COMPOSED_PROVIDER_GEMINI.get() -} - -// ── THE NODE'S OPEN-CALL TABLE, AS A SERVED SESSION REACHES IT ─────────────────────────────────── -// -// The runtime declares the port (`crate::runtime::GovernedCalls`) and the node's table implements it -// (`crate::governed`). Between the two, this is what hands a served session the table: composed -// exactly the way this plane's provider credentials are — set-once, process-wide, written at boot by -// this plane's own `compose` step ([`compose_plane`]) and read by every session opened thereafter. - -/// THE COMPOSED OPEN-CALL TABLE — the node's own, as the served path is allowed to see it. -/// -/// SET-ONCE, first writer wins, exactly like [`COMPOSED_PROVIDER`]: a second compose is a no-op -/// rather than a silent swap of the table one half of this node's live sessions are already keyed -/// into. `None` is a node whose compose step never ran (a harness that mounts the plane's routes -/// alone): every tool call is served in-process there, exactly as before the governed wait existed. -static COMPOSED_GOVERNED_CALLS: std::sync::OnceLock> = - std::sync::OnceLock::new(); - -/// The identifier the next served session is known to the node's table by. -/// -/// Minted here rather than derived from anything on the wire. The root keys its open calls by this -/// number and two conversations may legitimately carry identical call identifiers — a provider mints -/// them per conversation — so the one thing this number must be is unique per session on this node, -/// which a counter is and a hash of a client-supplied `call_id` is not. -static NEXT_SERVED_SESSION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); - -/// COMPOSE a table behind the served door — the one write of the governed-call port. -/// -/// Returns `false` when a table was already composed (the first write stands). Nothing here learns -/// what is behind the port: what crosses is what [`crate::runtime::GovernedCalls`] declares. -pub fn install_governed_calls(calls: Arc) -> bool { - COMPOSED_GOVERNED_CALLS.set(calls).is_ok() -} - -/// Whether a composition root has bound its open-call table — i.e. whether the sessions this door -/// serves are the governed kind. -#[must_use] -pub fn governed_calls_composed() -> bool { - COMPOSED_GOVERNED_CALLS.get().is_some() -} - -/// THE BINDING ONE SERVED SESSION OPENS UNDER: a fresh identifier for this node's table, and the -/// table itself. -/// -/// Every served session-open on this door calls this and hands what it returns to the open, which is -/// what makes "served" and "governed" the same set on a composed node. `None` on a node with no -/// table composed — an ungoverned deployment serves exactly what it served before. -#[must_use] -pub fn served_governed_session() -> Option { - let calls = COMPOSED_GOVERNED_CALLS.get()?; - Some(crate::runtime::GovernedSession { - session: NEXT_SERVED_SESSION.fetch_add(1, std::sync::atomic::Ordering::Relaxed), - calls: Arc::clone(calls), - }) -} - -/// Gemini Live's native provider auth HEADER NAME — `x-goog-api-key`, never `Authorization`. Named -/// once, publicly, so a conformance probe names the literal. The Live WebSocket dial presents the key -/// as the `key` query parameter instead (see [`credential_placement`]), the bytes it has always sent. -pub const GEMINI_API_KEY_HEADER: &str = "x-goog-api-key"; - -/// THE VOICE PLANE'S RFC 8707 RESOURCE PATH — the segment the plane's canonical audience carries and -/// the base every voice ingress route sits under. A token presented on any `/v1/realtime/*` path must -/// carry the audience `/v1/realtime`; the claim of this ONE base covers all four routes by -/// segment-boundary match (the MCP `mount_path` / A2A `/a2a` precedent — one resource, one audience). -pub const MOUNT_PATH: &str = "/v1/realtime"; - -/// The browser-WebRTC ephemeral (`ek_`) client-secret MINT endpoint — a one-shot HTTP pass. The -/// concrete `TokenMinter` (the live `POST /v1/realtime/client_secrets` to the provider) is the port a -/// deployment binds; this route mounts + governs the session-open the mint is scoped to. -pub const MINT_PATH: &str = "/v1/realtime/client_secrets"; - -/// The SDP BROKER endpoint — a one-shot HTTP pass (`application/sdp` in, the provider's SDP answer -/// out). The upstream `POST /v1/realtime/calls` is the credential-gated tail behind the egress port. -pub const SDP_PATH: &str = "/v1/realtime/calls"; - -/// The BROWSER-WEBRTC SIDEBAND accept — the persistent control channel keyed by `{call_id}`. The -/// socket upgrade + provider dial is the credential-gated tail; the governed open runs here on arrival. -pub const SIDEBAND_PATH: &str = "/v1/realtime/sideband/{call_id}"; - -/// The TELEPHONY WS accept — the carrier media leg keyed by `{call_id}`, proxied `g711_ulaw` -/// end-to-end. The socket upgrade + provider WSS dial is the credential-gated tail. -pub const TELEPHONY_PATH: &str = "/v1/realtime/telephony/{call_id}"; - -/// THE GEMINI LIVE WS ACCEPT — the plane's SECOND dialect route (the second-dialect route): a THIN DUPLEX PROXY between the -/// caller's WS and the Gemini `BidiGenerateContent` upstream, the same shape as the telephony leg -/// (client WS <-> busbar <-> provider WS) rather than the OpenAI browser-sideband's mint+SDP dance — -/// Gemini Live has no ephemeral-token-mint or SDP-broker concept; it is a native full-duplex socket on -/// both legs. Keyed by `{call_id}` exactly as the other WS legs are. -pub const GEMINI_PATH: &str = "/v1/realtime/gemini/{call_id}"; - -/// THE GEMINI DIALECT'S CLAIMED BASE — a path distinct from [`MOUNT_PATH`] so [`voice_claims`] can name -/// the Gemini route under its OWN wire format ([`crate::GEMINI_LIVE`]) rather than folding it into the -/// OpenAI claim, the A2A precedent of returning more than one `(path, wire)` pair per plane. -const GEMINI_MOUNT_PATH: &str = "/v1/realtime/gemini"; - -/// The RFC 9728 protected-resource metadata path for the voice resource (the `resource_metadata` a -/// refused caller is pointed at). Derived from `public_url` beside the audience; a deployment serves -/// the document itself (not one of the five mounted data routes). -const METADATA_PATH: &str = "/.well-known/oauth-protected-resource/v1/realtime"; - -/// THE VOICE PLANE'S DISPATCH SLOT — the per-generation object `build` erases into `plane_slots` under -/// the decl key. Carries the plane's audience facts (derived from `public_url`) and a live runtime the -/// route handlers open governed sessions from. -pub struct VoiceMount { - /// The RFC 8707 audience every inbound voice token must carry — `/v1/realtime`. - audience: String, - /// The absolute RFC 9728 metadata URL quoted into a refused caller's `WWW-Authenticate` challenge. - resource_metadata: String, - /// The per-generation session runtime every route opens governed sessions from, carrying the - /// operator's own `streams:` posture and ceilings. Each route binds the live host onto it - /// (`build_runtime_hosted`) once a request hands it an engine host. - runtime: Arc, - /// A per-slot provider endpoint override — `None` in every built slot, because the composition - /// root composes the provider process-wide ([`install_provider`]) AFTER the slot is built. Kept as - /// a field so a loopback test can inject one directly into [`open_governed`]. - /// - /// The Gemini Live provider has NO per-slot override twin: unlike the OpenAI mint/SDP one-shot - /// passes (which take a request-scoped `Option<&ProviderEndpoint>` a test can construct locally, - /// see [`GovernedOpen::provider`]), the Gemini leg is a WS accept whose `on_socket` closure must be - /// `'static` — so it reads the process-wide [`composed_gemini_provider`] directly rather than - /// through the (non-`'static`) mount, and there is no request-scoped path to override on. - provider: Option, -} - -impl VoiceMount { - /// The realtime provider endpoint this mount's routes dial: the slot's own override when one was - /// injected, else the one the composition root composed. `None` on a deployment that composed no - /// provider, in which case the mint / SDP passes answer "governed, but no provider composed". - fn provider(&self) -> Option<&ProviderEndpoint> { - match self.provider.as_ref() { - Some(p) => Some(p), - None => composed_provider(), - } - } -} - -impl VoiceMount { - /// This plane's admission facts — the audience a token at the voice door must carry and where a - /// refused caller is sent for one. Both strings are ONE reading of `public_url`, so the audience a - /// caller is told to ask for and the one busbar demands cannot drift apart (the A2A precedent). - fn admission(&self) -> PlaneAdmission { - PlaneAdmission { - audience: self.audience.clone(), - resource_metadata: self.resource_metadata.clone(), - } - } -} - -/// ONE READING of `public_url`: parse it, REPLACE the path wholesale, drop query and fragment — so a -/// `public_url` carrying a path of its own cannot produce a second spelling of the voice resource. The -/// A2A `serve::absolute` discipline, kept local so the plane names no core serve helper. -fn absolute(public_url: &str, path: &str) -> Option { - let mut u = url::Url::parse(public_url).ok()?; - u.set_path(path); - u.set_query(None); - u.set_fragment(None); - Some(u.to_string()) -} - -/// [`PlaneDecl::hydrate`] — BOOT-REHYDRATE the durable voice-session working-set BEFORE any listener is -/// bound, mirroring the A2A task-set restore. Under `store: memory` (the ephemeral posture this -/// in-process mount runs) there is no durable working-set, so it skips exactly as the A2A / MCP hydrate -/// hooks do — the plane's live carriers are per-connection and never survive a restart. When a durable -/// store IS configured, it restores the persisted `voice_session` rows through the neutral engine seam -/// ([`crate::runtime::scope::rehydrate_sessions`]): an active row is re-installed, a terminal one is -/// counted and left, and a row whose durable body cannot be decoded is counted and skipped — one bad -/// row never aborts the restore. Only a store-level list failure refuses boot (propagated as `Err`). -/// -/// # Errors -/// Returns the store error's text when the durable list itself fails — a boot-refusing condition. -pub fn voice_hydrate(ctx: &dyn PlaneBootCtx) -> Result<(), String> { - // Ephemeral by design (no configured store): no durable working-set to restore, so skip — the same - // first move the A2A task-set restore makes. - let Some(store) = ctx.plane_store() else { - return Ok(()); - }; - // A configured store: restore the durable `voice_session` working-set into a fresh engine and - // surface the outcome. A durable row a resumed session reattaches to via `SessionHandle::bind`. - let engine = DurableHandleEngine::new(); - let counts = crate::runtime::scope::rehydrate_sessions(&engine, store.as_ref()) - .map_err(|e| format!("streaming session rehydrate failed: {e}"))?; - if counts.unreadable > 0 { - tracing::warn!( - unreadable = counts.unreadable, - "streaming: some durable session rows could not be decoded on boot; counted and skipped" - ); - } - // DEBUG, not INFO: a 1.5.5-shaped config with no `streams:` section still runs this hook (it is - // gated on `has_store()`, not on the plane being configured) and must not gain a new boot line at - // INFO — the neutrality binding (qa/parity-bindings.md). An operator who DID - // configure `streams:` can still see this at DEBUG. - tracing::debug!( - active = counts.active, - terminal = counts.terminal, - unreadable = counts.unreadable, - "streaming: durable session working-set rehydrated" - ); - Ok(()) -} - -/// [`PlaneDecl::start`] — the post-listener boot step. Voice opens no background boot task: a live -/// session is admitted and served per WS-accept ARRIVAL (through `run_gauntlet_session`, one governed -/// pass per connection), each running its own supervised pump that parks on the carrier's hard-close — -/// there is no process-wide sweep loop to spawn here (unlike the A2A start, which resolves outbound -/// client identities once). The one thing that DOES run on a timer, the governed tool-call sweep -/// ([`crate::runtime::serve_with_sweep`]), rides each session's own pump and ends with it, which is -/// what keeps that true: a sweep outliving the socket it sweeps for would be exactly the background -/// loop this hook says the plane does not have. The live accept listener + provider dial are composed by the composition -/// root behind the plane's ports (the credential-gated tail). So this confirms readiness and returns -/// `Ok`, participating in the boot fold so the plane is an explicit member of it rather than silent. -/// -/// # Errors -/// Never fails today; the signature is the boot-hook shape so future post-listener work refuses boot -/// through it rather than gaining a new seam. -pub fn voice_start(_ctx: &dyn PlaneBootCtx) -> Result<(), String> { - tracing::debug!( - "streaming: started — sessions are admitted and served per WS-accept arrival (one \ - run_gauntlet_session pass each); no process-wide background task is spawned" - ); - Ok(()) -} - -/// [`PlaneDecl::build`] — construct the voice DISPATCH slot for one config generation. Reads the -/// deployment's `public_url` (the receiving origin the plane fronts) and derives the plane's audience -/// from it. `None` when there is no `public_url`: a deployment with no receiving origin fronts nothing, -/// claims nothing and admits no one — the A2A delegation-only asymmetry, and what makes the plane bind -/// no audience rather than mount a door that could only ever refuse. -#[must_use] -pub fn voice_build(ctx: &BuildCtx) -> Option> { - let public = ctx.public_url?; - let audience = absolute(public, MOUNT_PATH)?; - let resource_metadata = absolute(public, METADATA_PATH)?; - let mount = VoiceMount { - audience, - resource_metadata, - runtime: Arc::new(dispatch_runtime()), - // No per-slot override: the composed provider is read process-wide at request time, because the - // composition root composes it only after the deployment's config resolves (see the field doc). - provider: None, - }; - Some(Arc::new(mount)) -} - -/// The per-generation session runtime the dispatch slot carries. Seeded with the operator's own -/// `streams:` posture (the section the plane parsed — see `crate::config::configured`), so the locked -/// session config a mint carries and the ceilings the pump enforces are the ones the deployment wrote -/// rather than the plane's dev defaults. Every route binds the live host onto it (see -/// [`VoiceMount::runtime`]). -fn dispatch_runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ) - .with_streams(&crate::config::configured()) -} - -/// [`PlaneDecl::claims`] — the ONE audience-checked region the voice plane answers on, spoken in its -/// first dialect. The base [`MOUNT_PATH`] covers all four routes by segment-boundary match, so every -/// `/v1/realtime/*` path a token is presented on is a path this plane claims (the R1 ratchet's -/// invariant: a path the plane answers on is a path it claims). Empty when the plane has no receiving -/// side (no dispatch slot), so it claims nothing exactly as it admits no one. -#[must_use] -pub fn voice_claims(slot: &dyn Any) -> Vec<(String, &'static str)> { - match slot.downcast_ref::() { - // TWO claims, one PER ROUTE DIALECT (the second-dialect route): the OpenAI base (mint/SDP/sideband/telephony) and the - // Gemini Live base (the thin duplex proxy) — the A2A precedent of naming more than one - // `(path, wire)` pair so each dialect's own `ingress_protocol` is the one its own path answers - // in, not a plane-wide constant that would mislabel the second dialect's traffic as the first. - Some(_) => vec![ - (MOUNT_PATH.to_string(), crate::OPENAI_REALTIME), - (GEMINI_MOUNT_PATH.to_string(), crate::GEMINI_LIVE), - ], - None => Vec::new(), - } -} - -/// [`PlaneDecl::admission`] — the audience a voice token must carry and where a refused caller is sent -/// for one, from the same dispatch slot. `Some` whenever the plane has a receiving side (a slot), -/// so `build_dispatch`'s R2 ratchet — a plane that CLAIMS a path must ADMIT (or boot refuses) — holds -/// by construction here: a slot ⇒ a claim AND an admission, never one without the other. -#[must_use] -pub fn voice_admission(slot: &dyn Any) -> Option { - slot.downcast_ref::().map(VoiceMount::admission) -} - -/// [`PlaneDecl::routes`] — the voice plane's TWO one-shot HTTP ingress routes, described NEUTRALLY -/// (S4a Option A): the `ek_` mint + SDP broker passes, each a thin neutral handler over `PlaneReqCtx`. -/// Both are `RouteAuth::Key` — behind the plane's one audience — so a token minted for another -/// resource is refused at the door. The browser-sideband + telephony WS-accept legs are NOT here: an -/// inbound WS upgrade cannot ride the buffered-body `PlaneReqCtx` adapter, so they are declared through -/// the neutral inbound WS-accept seam instead (see [`voice_ws_arrivals`]). Empty when the plane has no -/// receiving side (no dispatch slot), so a deployment that fronts nothing mounts nothing. -#[must_use] -pub fn voice_routes(slot: &dyn Any) -> Vec { - use busbar_contract::abi::mechanism::route::{RouteAuth, RouteMethod}; - use busbar_kernel::plane_routes::{PlaneReqCtx, PlaneRouteFuture}; - - if slot.downcast_ref::().is_none() { - return Vec::new(); - } - vec![ - PlaneRouteSpec { - path: MINT_PATH.to_string(), - method: RouteMethod::Post, - auth: RouteAuth::Key, - handler: Arc::new(|ctx: PlaneReqCtx| -> PlaneRouteFuture { Box::pin(mint_route(ctx)) }), - }, - PlaneRouteSpec { - path: SDP_PATH.to_string(), - method: RouteMethod::Post, - auth: RouteAuth::Key, - handler: Arc::new(|ctx: PlaneReqCtx| -> PlaneRouteFuture { Box::pin(sdp_route(ctx)) }), - }, - ] -} - -/// THE PLANE'S WS-ACCEPT ENTRY HOOK ([`crate::linked`]): installs [`voice_ws_arrivals`] into the -/// neutral WS-accept registry, before the router that drains it is built. -pub fn install_arrivals() { - install_ws_arrivals(voice_ws_arrivals()); -} - -/// THE VOICE PLANE'S INBOUND WS-ACCEPT ARRIVALS — the browser-sideband + telephony media legs, -/// declared through the neutral substrate WS-accept seam (`WsArrivalSpec`) rather than `routes`, -/// because an inbound WS upgrade cannot ride the buffered-body `PlaneReqCtx` adapter (its body is -/// already consumed). Both are `RouteAuth::Key` under the plane's one audience — the SAME admission bar -/// the mint/SDP routes carry — so the auth middleware refuses a foreign-resource token BEFORE the -/// accept fn runs. The composition root installs these (`install_ws_arrivals`) and the core router -/// mounts one gauntlet-gated WS-accept route per spec, resolving the live runtime slot under the -/// plane's decl `key`. Empty when the plane has no receiving side, exactly as [`voice_routes`]. -#[must_use] -pub fn voice_ws_arrivals() -> Vec { - use busbar_contract::abi::mechanism::route::RouteAuth; - let key = crate::PLANE_DECLARATION.key; - vec![ - WsArrivalSpec { - path: SIDEBAND_PATH.to_string(), - auth: RouteAuth::Key, - slot_key: key, - accept: Arc::new(|a: WsArrival| -> WsAcceptFuture { - Box::pin(ws_accept(a, Ingress::Sideband, OpenAiRealtimeCodec)) - }), - }, - WsArrivalSpec { - path: TELEPHONY_PATH.to_string(), - auth: RouteAuth::Key, - slot_key: key, - accept: Arc::new(|a: WsArrival| -> WsAcceptFuture { - Box::pin(ws_accept(a, Ingress::Telephony, OpenAiRealtimeCodec)) - }), - }, - // THE GEMINI LIVE THIN-DUPLEX ACCEPT (the second-dialect route) — same admission bar (RouteAuth::Key under the - // plane's one audience), the SAME `ws_accept` choke point, generic over the Gemini codec - // instead of the OpenAI one. - WsArrivalSpec { - path: GEMINI_PATH.to_string(), - auth: RouteAuth::Key, - slot_key: key, - accept: Arc::new(|a: WsArrival| -> WsAcceptFuture { - Box::pin(ws_accept(a, Ingress::Gemini, GeminiLiveCodec)) - }), - }, - ] -} - -/// WHICH ingress a route drives — the topology-shaping fact a handler carries into the shared governed -/// open. Every arm funnels through [`crate::topology::begin_session`] (so through -/// `run_gauntlet_session`); they differ only in the locked config and carrier the topology binds. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum Ingress { - /// The `ek_` mint (browser-WebRTC sideband): a sideband control session, no downlink media relay. - Mint, - /// The SDP broker: same sideband governed open; the SDP handshake is the credential-gated tail. - Sdp, - /// The browser-WebRTC sideband accept. - Sideband, - /// The telephony media leg — `g711_ulaw` end-to-end through the thin proxy. - Telephony, - /// THE GEMINI LIVE THIN-DUPLEX LEG (the second-dialect route) — a native full-duplex socket both sides, proxied through - /// the SAME [`crate::topology::telephony::TelephonyProxy`] shape the telephony leg uses (client WS - /// <-> busbar <-> provider WS), just without the g711 lock and under the Gemini codec + the - /// Gemini-keyed composed provider. - Gemini, -} - -/// The neutral inputs one governed session-open reads — bundled so the choke point takes ONE argument -/// and a test constructs the SAME shape the route handler does. `host` is the owned host seam (cloned -/// per blocking hook hop), `provider` the configured realtime endpoint (`None` ⇒ the mint / SDP passes -/// answer `501`), `key` the caller `(id, name)` the hook gate reads, `body`/`headers` the one-shot -/// pass's request payload (the SDP offer + its `Authorization: Bearer ek_`). -pub(crate) struct GovernedOpen<'a> { - pub rt: &'a VoiceRuntime, - pub host: Arc, - pub provider: Option<&'a ProviderEndpoint>, - pub ingress: Ingress, - pub owner: String, - pub call_id: String, - /// The resolved presenting virtual key (audience-checked key chain), or `None` ungoverned. The - /// hook gate reads its `(id, name)`; the Meter step lands each turn's usage on this key's ledger. - pub vkey: Option, - pub body: Bytes, - pub headers: axum::http::HeaderMap, - pub now: u64, -} - -/// THE SHARED GOVERNED OPEN every voice route funnels through — the ONE choke point. In order: -/// -/// 1. **hooks-gate** (`plane_host::admission_gates_decide`) — the operator's request-admission gate -/// over the session-open params. A `Reject` refuses BEFORE any lease/mint/dial. BYTE-IDENTICAL -/// (nothing serialized, no blocking hop) when no gate is attached — the -/// `admission_gates_attached` presence pre-filter. -/// 2. **hooks-tap** (`plane_host::admission_transform`) — a `prompt: rw` rewrite over the same params, AFTER the -/// gate and BEFORE the credential is leased. A committed rewrite REPLACES the locked session params -/// the mint/dial then carries; an abstaining chain (or no attached rewrite) leaves them -/// byte-identical. -/// 3. **the governed open** — [`crate::topology::begin_session`] / -/// [`crate::topology::telephony::begin_telephony`] runs `run_gauntlet_session` -/// (verify-strictly-before-charge): a gate refusal is `403` before any lease/durable open. -/// 4. **the serving leg** — for `Mint`, the `ek_` is minted through [`HttpsTokenMinter`] over the -/// configured provider and returned as JSON; for `Sdp`, the offer is brokered upstream, the -/// `rtc_` correlation key is stamped onto the durable row, and the answer + `Location` -/// header are returned. The `Sideband` / `Telephony` WS-accept legs answer `501` — the inbound -/// WS-accept seam lands separately. -/// -/// The front-door request is COUNTED on `busbar_plane_requests_total` (plane = voice), and the answer -/// carries a [`Counted`] marker so the core `plane::observe` middleware stands down (no double count). -pub(crate) async fn open_governed(req: GovernedOpen<'_>) -> axum::response::Response { - // The front door's own clock: the duration `finish` reports is this request's, end to end. - let started = std::time::Instant::now(); - let GovernedOpen { - rt, - host, - provider, - ingress, - owner, - call_id, - vkey, - body, - headers, - now, - } = req; - // (0) AUTHORIZATION — may this key open a session here at all? Asked FIRST, of the caller's own - // grant, so a key that is valid for this plane's audience but was never granted session scope is - // refused before any hook fires, any account is opened, any durable row exists or any provider is - // dialed. An ungoverned deployment resolves no key and has no grant to consult, so it proceeds - // exactly as it did before — the refusal is a narrowing of governed callers only. - if let Some(k) = vkey.as_ref() { - if !session_scope_allowed(k) { - return finish(&host, started, session_scope_refusal()); - } - } - // The `(id, name)` the hook gate reads — derived from the resolved key (or `None` ungoverned). - let key = vkey.as_ref().map(|k| (k.id.clone(), k.name.clone())); - // THE METER STEP's attribution: each closed turn's counts per class go to the presenting key's - // kernel account, which ledgers them through the one metering path and answers whether the carrier - // stays open; the same account refuses the open when the key's chain is already dry. `None` - // ungoverned — nothing to attribute, nothing to close on. - let meter = vkey.as_ref().map(|k| { - crate::runtime::metering::TurnMeter::new( - Arc::clone(&host), - k.clone(), - FRONT_DOOR_POOL, - crate::OPENAI_REALTIME, - ) - }); - let hosted = crate::runtime::build_runtime_hosted(rt, Arc::clone(&host)); - let rt = &hosted; - // The session-open params the hooks screen and (maybe) rewrite: the g711 lock for telephony, the - // plane-default session posture otherwise. One projection both the gate and the tap read. - let mut session_cfg = match ingress { - Ingress::Telephony => g711_config(), - Ingress::Mint | Ingress::Sdp | Ingress::Sideband | Ingress::Gemini => { - rt.session_defaults.clone() - } - }; - - // (1) HOOKS-GATE — refuse before any lease/mint/dial. Zero-cost / byte-identical when unattached. - if let Err(refused) = hook_gate(&host, key, &call_id, now, &session_cfg).await { - return finish(&host, started, *refused); - } - // (2) HOOKS-TAP — rewrite the session-open params before the credential is leased. Byte-identical - // (params untouched) when no rewrite hook is attached or the chain abstains. - match hook_tap(&host, &owner, &call_id, now, &session_cfg).await { - Ok(Some(rewritten)) => session_cfg = rewritten, - Ok(None) => {} - Err(refused) => return finish(&host, started, *refused), - } - - // (3) THE GOVERNED OPEN. Telephony has no durable handle to correlate; the sideband topologies keep - // the handle so the SDP broker can stamp the `rtc_` onto the row. `Gemini` never reaches - // this fn today (no HTTP one-shot route dispatches it — only the WS-accept seam does, see - // `ws_accept`); the arm exists only so this match stays exhaustive over `Ingress`. - let resp = match ingress { - Ingress::Telephony => match begin_telephony( - rt, - OpenAiRealtimeCodec, - owner, - call_id, - session_cfg, - meter, - now, - ) { - // The HTTP telephony open serves nothing: the media leg is the WS-accept seam's. A `501` - // is a failed open, so the session fee its open counted is given back. - Ok(proxy) => { - proxy.core().refund_open(); - sideband_pending() - } - Err(e) => start_refusal(&e), - }, - Ingress::Gemini => sideband_pending(), - Ingress::Mint | Ingress::Sdp | Ingress::Sideband => match begin_session( - rt, - OpenAiRealtimeCodec, - owner, - call_id, - Some(session_cfg.clone()), - Carrier::sideband(), - meter, - now, - ) { - // (4) THE SERVING LEG, past a clean governed open. A pass that answers success served the - // session; any other answer — a failed mint or SDP broker, an unreachable provider, a - // `501` with nothing composed to serve it — is a failed open, and the session fee its - // open counted is given back (TODO 17(b), ARCHITECT R4). - Ok((core, handle)) => { - let resp = match ingress { - Ingress::Mint => serve_mint(provider, handle.owner(), &session_cfg).await, - Ingress::Sdp => serve_sdp(provider, &headers, body, &handle, now).await, - // The inbound WS-accept seam (browser sideband) lands separately — no bare - // on_upgrade. - _ => sideband_pending(), - }; - if resp.status().is_success() { - core.served(); - } else { - core.refund_open(); - } - resp - } - Err(e) => start_refusal(&e), - }, - }; - finish(&host, started, resp) -} - -/// REPORT the front-door session-open through the host's `request_finished` seam — its count AND its -/// duration since `started` — and MARK the answer counted. On the engine's host that is the -/// plane-labelled `busbar_plane_requests_total` series (`plane = voice`, the voice-server cell; the -/// same family and labels this door emitted by hand) plus the `busbar_plane_request_duration_seconds` -/// sample the hand emit never recorded (item 566) — the same call the A2A door makes. The [`Counted`] -/// marker tells the core `plane::observe` middleware the request was already reported, so the -/// front-door series is never double-counted at the boundary. -fn finish( - host: &Arc, - started: std::time::Instant, - mut resp: axum::response::Response, -) -> axum::response::Response { - host.request_finished( - crate::PLANE_KEY, - crate::OPENAI_REALTIME, - FRONT_DOOR_POOL, - busbar_kernel::telemetry::outcome_of(resp.status().as_u16()), - started.elapsed().as_secs_f64(), - ); - resp.extensions_mut().insert(Counted); - resp -} - -/// The hooks-GATE leg (`plane_host::admission_gates_decide`) over the session-open params. `Ok(())` -/// proceeds; `Err` is a finished refusal. ZERO-COST / BYTE-IDENTICAL when no gate is attached: the -/// `admission_gates_attached` presence pre-filter short-circuits before any serialize or blocking hop. -async fn hook_gate( - host: &Arc, - key: Option<(String, String)>, - session_id: &str, - now: u64, - cfg: &SessionConfig, -) -> Result<(), Box> { - if !admission_gates_attached(&**host, crate::PLANE_DECLARATION.key, GATE_CONTAINER) { - return Ok(()); - } - // Serialized ONCE for the seam (only past the presence check). The host re-selects the gate set by - // `(plane_key, container)` and runs the same decision — the Seam-B inversion, so this plane body - // names no core hook symbol. Driven on a blocking thread (the host runs the async gate on a fresh - // runtime), so it MUST NOT run on a worker. - let args_json = serde_json::to_vec(cfg).unwrap_or_default(); - let sid = session_id.to_string(); - let host = Arc::clone(host); - let outcome = tokio::task::spawn_blocking(move || { - admission_gates_decide( - &*host, - crate::PLANE_DECLARATION.key, - GATE_CONTAINER, - now, - SESSION_OPEN_METHOD, - &args_json, - key.as_ref().map(|(id, name)| (id.as_str(), name.as_str())), - (!sid.is_empty()).then_some(sid.as_str()), - ) - }) - .await - .unwrap_or(GateOutcome::Reject { - status: 403, - message: String::new(), - hook: String::new(), - }); - match outcome { - GateOutcome::Proceed => Ok(()), - GateOutcome::Reject { - status, message, .. - } => Err(Box::new(hook_refusal(status, &message))), - } -} - -/// THE SESSION-OPEN PARAMS A COMMITTED REWRITE PRODUCED, or the reason they cannot be used — the -/// voice twin of the MCP plane's `committed_arguments` and the A2A plane's `committed_params`, with -/// the same signature for the same reason: the rule the plane owns is what happens when the seam -/// misbehaves, and it has to be drivable without a host that misbehaves. -/// -/// # Two fail-open readings this one function replaces -/// -/// **(1) Bytes busbar cannot read back.** The apply site was -/// `if let Ok(v) = serde_json::from_slice::(&args_json) { … }`, so a hook that -/// COMMITTED a rewrite (`applied: true`) and produced bytes that would not deserialize fell through -/// to `Ok(None)` — which both callers read as "the hook made no rewrite" — and the session opened -/// carrying the plane's OWN params. For the hook class this seam exists for that is fail-OPEN in the -/// precise sense: a redaction hook says "I have removed the secret from these session params", its -/// output is unreadable, and busbar opens the session with the secret still in it. The hook ran, -/// reported applied, and was silently undone. -/// -/// Unlike the MCP/A2A twins this arm is REACHABLE THROUGH CORE'S REAL HOST, which is why voice is the -/// severe instance rather than the third copy of a dormant one. There the target type is -/// `serde_json::Value` and `plane_host::transform_over_over` only ever commits a JSON object -/// re-serialised with `to_vec`, so the bytes always parse. Here the target is [`SessionConfig`], and -/// core's `apply_rewrite_to_invoke_args` admits ANY JSON object as the replacement: a hook whose -/// rewrite reply carries `{"voice": 7}` or `{"input_audio_format": "flac"}` makes core commit bytes -/// this type rejects — a type mismatch, and an audio format the plane does not speak. -/// -/// **(2) A patch that blanks everything it did not name.** `SessionConfig` derives `Deserialize` with -/// `#[serde(default)]` on every field, so an object that DOES parse — `{"voice": "marin"}` — parsed -/// into a config whose every OTHER field was the type default, wiping the plane's locked -/// `instructions` on its way through. That is the same fail-open pointed the other way: the plane's -/// own lock disappears because a hook touched an unrelated field. The wire contract of this type is -/// PATCH semantics (its own doc comment: "Every field is optional on the wire — a partial -/// `session.update` patches only what it names"), so the committed object is MERGED over the locked -/// params: a key the hook named wins, a key it did not name keeps the plane's value. Clearing stays -/// expressible, because naming a key with an explicit `null` is naming it. -/// -/// The merge happens BEFORE the decode, so rule (1) still judges the whole config: a patch that names -/// one bad field refuses the open rather than being quietly dropped. -/// -/// A well-formed JSON SCALAR is refused too. These params are an object on every wire the plane -/// speaks, and the value is substituted for the plane's locked [`SessionConfig`]; admitting `7` would -/// mean telling the hook its rewrite landed and then opening the session with something that is not a -/// session config at all. -pub(crate) fn committed_session_config( - locked: &SessionConfig, - args_json: &[u8], -) -> Result { - let patch: serde_json::Value = - serde_json::from_slice(args_json).map_err(|e| format!("the output is not JSON: {e}"))?; - let serde_json::Value::Object(patch) = patch else { - return Err("the output is JSON but not a session-params object".to_string()); - }; - // The locked params as the hook itself was handed them (`serde_json::to_vec(cfg)` at the call - // seam), so the merge is over exactly the key set the hook screened. - let Ok(serde_json::Value::Object(mut merged)) = serde_json::to_value(locked) else { - return Err( - "the plane's own locked session params did not project to an object".to_string(), - ); - }; - // A key the hook NAMED wins; a key it did not name keeps the plane's locked value. - merged.extend(patch); - serde_json::from_value::(serde_json::Value::Object(merged)) - .map_err(|e| format!("the output is not a session config: {e}")) -} - -/// The hooks-TAP leg (`plane_host::admission_transform`) over the session-open params. `Ok(Some(cfg))` is a -/// committed rewrite the caller substitutes for the locked params; `Ok(None)` is "no change" (no -/// attached rewrite, or an abstaining chain — BYTE-IDENTICAL); `Err` is a refusal — the rewrite -/// gate's own `Reject`, or a committed rewrite busbar could not read back (see -/// [`committed_session_config`]). -async fn hook_tap( - host: &Arc, - owner: &str, - session_id: &str, - now: u64, - cfg: &SessionConfig, -) -> Result, Box> { - if !admission_tap_attached(&**host, crate::PLANE_DECLARATION.key, GATE_CONTAINER) { - return Ok(None); - } - let args_json = serde_json::to_vec(cfg).unwrap_or_default(); - // The `'static` handle the blocking leg owns. Named apart from the `host` param rather than - // shadowing it, because the refusal below audits through the SAME host after the join. - let hook_host = Arc::clone(host); - let verdict = tokio::task::spawn_blocking(move || { - admission_transform( - &*hook_host, - crate::PLANE_DECLARATION.key, - GATE_CONTAINER, - now, - SESSION_OPEN_METHOD, - &args_json, - ) - }) - .await - // A join panic is FAIL-SAFE on the transform path (the gate already admitted): proceed unchanged. - .unwrap_or_else(|e| { - // ...but never SILENT. This is the one disposition on this seam where a hook failure used to - // leave no trace at all: `admission_transform` logs its own `Failed`/timeout arms, so a join - // panic is the only way a `prompt: rw` hook can stop answering without a line naming it. - tracing::error!( - session = %session_id, - error = %e, - "the streaming session-open rewrite (prompt: rw) leg did not join; the session-open proceeds \ - with the plane's own locked params (fail-safe: the admission gate already ran)" - ); - TransformVerdict::Proceed { - applied: false, - args_json: Vec::new(), - } - }); - match verdict { - TransformVerdict::Proceed { applied, args_json } => { - if !applied { - return Ok(None); - } - // A COMMITTED REWRITE BUSBAR CANNOT READ BACK REFUSES THE OPEN. There is no safe fallback - // here: `Ok(None)` is read by both callers as "the hook made no rewrite", so falling - // through with it opens the session carrying the params the hook said it had REPLACED — - // and that is the defect, not a degraded mode. The unreadable bytes are not a session - // config, so the answer is the same one the hook's own `Reject` gets. The MCP - // (`mcp/method.rs`) and A2A (`a2a/receive.rs`) twins answer the identical question the - // identical way; `busbar-llm`'s pipeline rejects rather than forward un-rewritten too. - match committed_session_config(cfg, &args_json) { - Ok(v) => Ok(Some(v)), - Err(why) => { - host.audit_record( - SESSION_AUDIT_ACTION, - &format!("streaming:{session_id}"), - "rejected", - owner, - ); - tracing::error!( - session = %session_id, - error = %why, - "a rewrite (prompt: rw) hook committed a rewrite of the streaming session-open \ - params whose output busbar could not read back; the session-open is REFUSED \ - rather than opened with the params the hook said it had replaced" - ); - Err(Box::new(refusal( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "a rewrite hook attached to this deployment committed a rewrite of the streaming \ - session-open params that busbar could not read back. The session-open is \ - refused rather than opened with the params the hook said it had replaced.", - ))) - } - } - } - TransformVerdict::Reject { - status, message, .. - } => Err(Box::new(hook_refusal(status, &message))), - } -} - -/// THE `ek_` MINT one-shot pass, past the governed open: mint through [`HttpsTokenMinter`] over the -/// configured provider (the real key held server-side) and return the browser-facing `ek_` as JSON. -/// `None` provider ⇒ `501` (governed, but no provider credential composed). -async fn serve_mint( - provider: Option<&ProviderEndpoint>, - owner: &str, - cfg: &SessionConfig, -) -> axum::response::Response { - let Some(p) = provider else { - return sideband_pending(); - }; - let minter = HttpsTokenMinter::new( - egress_client(), - &p.base_url, - p.api_key.expose_secret(), - owner, - None, - ); - match minter.mint(cfg).await { - Ok(token) => json_response( - axum::http::StatusCode::OK, - &serde_json::json!({ - "value": token.value, - "expires_at_unix": token.expires_at_unix, - }), - ), - Err(e) => text_response( - axum::http::StatusCode::BAD_GATEWAY, - format!("streaming ephemeral-secret mint failed: {e}"), - ), - } -} - -/// THE SDP BROKER one-shot pass, past the governed open: broker the browser's `application/sdp` offer -/// to the provider's `POST /v1/realtime/calls` under BUSBAR'S OWN provider credential (busbar brokers -/// the call server-side), PRESERVE the `Location: …/rtc_` response header verbatim, STAMP that -/// `rtc_` onto the durable session row (so busbar's governance and the brokered media call -/// name the SAME session), and return the SDP answer. `None` provider ⇒ `501`. -/// -/// `_inbound` (the caller's request headers) is deliberately NOT read for egress: the inbound -/// `Authorization` is the caller's `RouteAuth::Key` GOVERNANCE bearer (the Auth plugin already consumed -/// it at the door), and forwarding it upstream would leak busbar's own authority to the provider. The -/// provider hop authenticates ONLY through [`crate::voice_provider_bearer`] — the same lane-constant -/// builder the `egress_auth_headers` decl and the `egress_tests` battery pin — never a caller token. -async fn serve_sdp( - provider: Option<&ProviderEndpoint>, - _inbound: &axum::http::HeaderMap, - offer: Bytes, - handle: &SessionHandle, - now: u64, -) -> axum::response::Response { - let Some(p) = provider else { - return sideband_pending(); - }; - let uri = format!("{}/v1/realtime/calls", p.base_url.trim_end_matches('/')); - let mut builder = http::Request::builder() - .method(http::Method::POST) - .uri(&uri) - .header(http::header::CONTENT_TYPE, "application/sdp"); - // Busbar's OWN provider credential — never the caller's inbound governance bearer. - for (name, value) in crate::voice_provider_bearer(p.api_key.expose_secret()) { - builder = builder.header(name, value); - } - let req = match builder.body(Full::new(offer)) { - Ok(r) => r, - Err(e) => { - return text_response( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - format!("streaming SDP request did not build: {e}"), - ) - } - }; - let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(30); - let resp = match send_bounded(&egress_client(), req, deadline).await { - Ok(r) => r, - Err(e) => { - return text_response( - axum::http::StatusCode::BAD_GATEWAY, - format!("streaming SDP broker upstream failed: {}", e.into_cause()), - ) - } - }; - let status = resp.status(); - // PRESERVE the `Location` header verbatim, and derive the `rtc_` correlation key from it. - let location = resp.headers().get(http::header::LOCATION).cloned(); - let answer = match tokio::time::timeout_at(deadline, resp.into_body().collect()).await { - Ok(Ok(b)) => b.to_bytes(), - _ => { - return text_response( - axum::http::StatusCode::BAD_GATEWAY, - "streaming SDP answer was not read before the deadline".to_string(), - ) - } - }; - // STAMP the correlation key onto the durable row (owner-gated) — broker → row, the single key that - // ties governance here to the media that flows there. A mismatch/absence is left unstamped. - if let Some(rtc) = location - .as_ref() - .and_then(|v| v.to_str().ok()) - .and_then(rtc_call_id_of) - { - let _ = handle.set_rtc_call_id(&rtc, now); - } - let mut builder = axum::response::Response::builder() - .status(status) - .header(http::header::CONTENT_TYPE, "application/sdp"); - if let Some(loc) = location { - builder = builder.header(http::header::LOCATION, loc); - } - builder - .body(axum::body::Body::from(answer)) - .unwrap_or_else(|_| sideband_pending()) -} - -/// The last `rtc_` path segment of a brokered call's `Location` header — the correlation key -/// the SDP broker stamps onto the durable row. `None` when no segment carries the `rtc_` prefix. -fn rtc_call_id_of(location: &str) -> Option { - location - .rsplit('/') - .find(|seg| seg.starts_with("rtc_")) - .map(|seg| seg.split(['?', '#']).next().unwrap_or(seg).to_string()) -} - -/// The substrate egress client the one-shot HTTPS passes dial through (the same posture the concrete -/// minter uses). Built per pass; the composition root pools one once the provider config is threaded. -fn egress_client() -> EngineClient { - busbar_kernel::proxy::build_egress_client( - &busbar_kernel::egress::engine::EngineSpec::pooled_webpki(4, 300, false, false), - ) -} - -/// A GOVERNED-BUT-UNCOMPOSED answer for a WS-accept leg (browser sideband / telephony media): the -/// session opened and was governed, but the inbound WS-accept seam that upgrades the socket lands -/// separately, so no bare `on_upgrade` is taken here. -fn sideband_pending() -> axum::response::Response { - refusal( - axum::http::StatusCode::NOT_IMPLEMENTED, - "streaming session governed-open succeeded; the inbound WS-accept seam that upgrades the browser \ - sideband / telephony media socket lands separately", - ) -} - -/// The finished refusal for a `begin_session` / `begin_telephony` [`StartError`] — verify-before-charge -/// at the route layer (a gate refusal is `403` with zero charge). -fn start_refusal(e: &StartError) -> axum::response::Response { - match e { - StartError::DestinationRefused => refusal( - axum::http::StatusCode::FORBIDDEN, - "streaming session destination refused at the open-pass gate (fail closed)", - ), - StartError::BudgetRefused => refusal( - axum::http::StatusCode::PAYMENT_REQUIRED, - "streaming session budget refused (fail closed)", - ), - StartError::Durable(_) => refusal( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "streaming session durable open failed", - ), - } -} - -/// A finished plain-text response — the shape both a governed-but-uncomposed answer and a fail-closed -/// refusal are framed in, so a handler names no `IntoResponse` machinery. -fn refusal(status: axum::http::StatusCode, msg: &'static str) -> axum::response::Response { - axum::response::Response::builder() - .status(status) - .body(axum::body::Body::from(msg)) - .expect("a static-body response always builds") -} - -/// A finished plain-text response over an OWNED body (a hook's own message, an upstream failure cause). -fn text_response(status: axum::http::StatusCode, msg: String) -> axum::response::Response { - axum::response::Response::builder() - .status(status) - .body(axum::body::Body::from(msg)) - .expect("a text-body response always builds") -} - -/// A finished JSON response (the minted `ek_` payload). -fn json_response( - status: axum::http::StatusCode, - body: &serde_json::Value, -) -> axum::response::Response { - axum::response::Response::builder() - .status(status) - .header(http::header::CONTENT_TYPE, "application/json") - .body(axum::body::Body::from( - serde_json::to_vec(body).unwrap_or_default(), - )) - .expect("a json-body response always builds") -} - -/// A hook's OWN refusal, framed with its status (clamped to a valid code) and message — the shape both -/// the gate and the rewrite-gate reject in. -fn hook_refusal(status: u16, message: &str) -> axum::response::Response { - let status = - axum::http::StatusCode::from_u16(status).unwrap_or(axum::http::StatusCode::FORBIDDEN); - text_response( - status, - if message.is_empty() { - "streaming session-open refused by a hook gate".to_string() - } else { - message.to_string() - }, - ) -} - -/// Read the voice dispatch slot off the neutral `PlaneReqCtx::slot`, resolve the caller + a session id, -/// and drive the shared governed open. `slot` is the SAME `VoiceMount` `build` erased (the mount is -/// what creates the route), so the downcast never fails on a mounted route; the `Option` survives only -/// so a future refactor that mounted the route without the slot answers `500` rather than panicking. -async fn serve( - ctx: busbar_kernel::plane_routes::PlaneReqCtx, - ingress: Ingress, -) -> axum::response::Response { - let Some(mount) = ctx.slot.downcast_ref::() else { - return refusal( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "streaming route reached without its dispatch slot", - ); - }; - // The resolved caller the audience-checked key chain attached (the session owner), or the honest - // constant on an ungoverned deployment — the SAME asymmetry the MCP notification principal reads. - let owner = ctx - .caller_principal - .clone() - .unwrap_or_else(|| "".to_string()); - // The `{call_id}` capture for a WS accept, or a fresh id for a one-shot mint/SDP pass. - let call_id = ctx - .path_params - .iter() - .find(|(k, _)| k == "call_id") - .map(|(_, v)| v.clone()) - .unwrap_or_else(|| format!("streaming-{}", unix_secs(&*ctx.host))); - // The resolved presenting virtual key (the middleware-resolved, audience-checked key), or `None` - // ungoverned. The hook gate reads its `(id, name)`; the Meter step lands usage on its ledger. - let vkey = ctx.gov.as_ref().and_then(|g| g.key()).cloned(); - open_governed(GovernedOpen { - rt: &mount.runtime, - host: Arc::clone(&ctx.host), - provider: mount.provider(), - ingress, - owner, - call_id, - vkey, - body: ctx.body.clone(), - headers: ctx.headers.clone(), - now: unix_secs(&*ctx.host), - }) - .await -} - -/// Wall-clock unix seconds for the session's `charged_at` / genesis stamp, READ OFF THE HOST'S CLOCK -/// PORT (C10, state/store port): `ClockHost::clock_now_secs`, which every route already holds through -/// its `Arc`. This is what the old doc comment promised ("the gauntlet clock the -/// production pump reads off the host is threaded once the composition root wires it") and the -/// structural `SystemTime::now()` stood in for; the port's wired slot is that same `SystemTime` epoch -/// clock (`store::now_ms()` scaled to nanos and divided back to seconds), so the value — and with it -/// the session's `charged_at` money stamp — is byte-identical. Generic over the port rather than -/// taking `&dyn ClockHost`, so no trait-object upcast is needed at the call sites. -fn unix_secs(clock: &H) -> u64 { - clock.clock_now_secs() -} - -async fn mint_route(ctx: busbar_kernel::plane_routes::PlaneReqCtx) -> axum::response::Response { - serve(ctx, Ingress::Mint).await -} -async fn sdp_route(ctx: busbar_kernel::plane_routes::PlaneReqCtx) -> axum::response::Response { - serve(ctx, Ingress::Sdp).await -} -/// A DIALED LEG WHOSE PROVIDER DIAL FAILED: nothing to relay the client's frames to. Settle the -/// just-opened durable row terminal and evict it at `now`, then drop the proxy rather than serve a -/// client socket with no upstream — fail closed, no orphaned row. The handle has no drop path of its -/// own. The session never served, so the session fee its open counted is given back first (TODO -/// 17(b), ARCHITECT R4). -pub(crate) fn settle_undialed(proxy: crate::topology::telephony::TelephonyProxy, now: u64) -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - proxy.core().refund_open(); - proxy.handle.finish(now); -} - -/// THE INBOUND WS-ACCEPT FN for the browser-sideband / telephony / Gemini-Live media legs — what -/// replaces the `501` stub, moving the WS legs onto the neutral inbound WS-accept seam. Generic over -/// the dialect `codec` (the second-dialect route): [`voice_ws_arrivals`] instantiates it once per dialect -/// ([`OpenAiRealtimeCodec`] for the sideband/telephony legs, [`GeminiLiveCodec`] for the Gemini leg) so -/// every leg runs the SAME choke point rather than a per-dialect copy. It builds the `GauntletRequest` -/// and [`SessionGauntlet`] EXACTLY as [`begin_session`] does (gov threaded from the audience-checked -/// auth layer, `destination` = the locked upstream model) and hands the upgrade to [`accept_gauntlet`] -/// — the ONLY path that consumes the upgrade into a live socket, and never a bare `on_upgrade`. -/// -/// GAUNTLET-BEFORE-UPGRADE: `accept_gauntlet` runs `run_gauntlet_session` SYNCHRONOUSLY and, on a -/// refused destination, returns the gate's own `403` WITHOUT upgrading a socket, spawning a task, or -/// opening a durable row. Only on admit is the socket upgraded and `on_socket` spawned. -/// -/// VERIFY-BEFORE-CHARGE, NO ORPHANED ROW: the kernel account's budget check + durable session open happen INSIDE -/// `on_socket` — AFTER the gauntlet admitted and BEFORE the pump reads a byte — through the gauntlet- -/// free [`open_admitted_session`] / [`open_admitted_telephony`] (the gauntlet already ran; re-running -/// it would double the gate). A refused accept opens nothing; a post-admit budget/durable refusal -/// commits no durable row and simply closes the just-upgraded socket. So no refused-or-aborted accept -/// ever leaves a live session row. -/// -/// THE PROVIDER DIAL (the provider-dial leg): for `Telephony` and `Gemini`, when the ingress's dialect has a COMPOSED -/// provider, the leg opens a [`crate::topology::telephony::TelephonyProxy`] (the same thin-duplex -/// shape for both) and dials the provider through [`dial_provider`] — the net-guarded, breaker-admitted -/// path — before pumping either socket. A dial failure drops the just-admitted session rather than -/// serving a client socket with nowhere to -/// relay to. With NO provider composed, both legs fall back to serving the client socket only (the -/// documented "governed but not yet dialing" posture) exactly as before. -/// -/// EVERY SERVED SESSION IS A GOVERNED SESSION: all three legs below — the dialed proxy, the -/// uplink-only fallback and the WebRTC sideband — take their binding from -/// [`served_governed_session`], which mints this session's identifier and hands it the node's own -/// open-call table. That is the join the tool moat was missing: the composition root entered a wait -/// where it planned a call's leg, and until this binding existed there was nothing on a socket that -/// could wake it or sweep it. A deployment with no root composed reads `None` here and keeps exactly -/// the behaviour it had. -pub(crate) async fn ws_accept( - arrival: WsArrival, - ingress: Ingress, - codec: C, -) -> axum::response::Response -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - let Some(mount) = arrival.slot.downcast_ref::() else { - return refusal( - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "streaming WS-accept reached without its dispatch slot", - ); - }; - // The dialect this leg speaks — the `ingress_protocol` label + the composed-provider table a - // `Telephony`/`Gemini` dial reads from. Every other leg (Sideband) stays OpenAI-labelled: it has no - // Gemini analogue today. - let dialect = match ingress { - Ingress::Gemini => crate::GEMINI_LIVE, - _ => crate::OPENAI_REALTIME, - }; - // The neutral host seam the operator hooks fire through — the SAME seam the one-shot passes read in - // `open_governed`, so the WS-accept front door screens a session-open identically to mint/SDP. - let host = Arc::clone(&arrival.host); - // The presenting key, resolved once: the authorization gate, the hook projection, the budget - // ceiling and the turn attribution all read it. - let vkey = arrival.gov.as_ref().and_then(|g| g.key()).cloned(); - // (0) AUTHORIZATION, before the upgrade — a key that holds no session scope on this pool is refused - // with the plane's own answer and no socket is bound, exactly as the one-shot passes refuse. - if let Some(k) = vkey.as_ref() { - if !session_scope_allowed(k) { - return session_scope_refusal(); - } - } - // The per-generation runtime, bound to the live host this session journals through. - let rt = Arc::new(crate::runtime::build_runtime_hosted( - &mount.runtime, - Arc::clone(&host), - )); - // The caller `(id, name)` the hook gate/tap read — the middleware-resolved key, or `None` ungoverned. - let key = vkey.as_ref().map(|k| (k.id.clone(), k.name.clone())); - // The resolved caller the audience-checked key chain attached (the session owner), or the honest - // constant on an ungoverned deployment. - let owner = arrival - .caller_principal - .clone() - .unwrap_or_else(|| "".to_string()); - // The `{call_id}` capture the accept route matched. - let call_id = arrival - .path_params - .iter() - .find(|(k, _)| k == "call_id") - .map(|(_, v)| v.clone()) - .unwrap_or_else(|| format!("streaming-{}", unix_secs(&*host))); - let now = unix_secs(&*host); - // The locked session posture: g711 for telephony, the plane-default otherwise (including Gemini — - // the Gemini leg carries no media-format lock of its own). The destination the gauntlet judges is - // this config's model, exactly as `begin_session` derives it. - let mut session_cfg = match ingress { - Ingress::Telephony => g711_config(), - _ => rt.session_defaults.clone(), - }; - // (1) HOOKS-GATE — a rejecting operator gate refuses the session-open BEFORE the upgrade: a - // pre-upgrade refusal Response, no socket bound, no lease/durable open. This closes the intra-plane - // gap where telephony (which has no preceding `ek_` mint pass) reached the media leg screened by - // nothing but the destination gauntlet; both WS legs now honor the operator gate exactly as the - // one-shot mint/SDP passes do in `open_governed`. - if let Err(refused) = hook_gate(&host, key, &call_id, now, &session_cfg).await { - return *refused; - } - // (2) HOOKS-TAP — a committed rewrite replaces the locked session posture BEFORE the gauntlet judges - // the destination and BEFORE the socket binds; byte-identical when no rewrite hook is attached. - match hook_tap(&host, &owner, &call_id, now, &session_cfg).await { - Ok(Some(rewritten)) => session_cfg = rewritten, - Ok(None) => {} - Err(refused) => return *refused, - } - let destination = session_cfg.model.clone().unwrap_or_default(); - let gov = arrival.gov.clone().unwrap_or_default(); - let gauntlet_req = GauntletRequest { - gov: &gov, - destination: &destination, - correlation_id: 0, - charged_at: now, - started: std::time::Instant::now(), - }; - let gate: Box = Box::new(SessionGauntlet); - // THE METER STEP's attribution for this WS session — the presenting key each turn's usage is - // landed on through the core seam, under THIS LEG'S OWN dialect label (K4: no longer a plane-wide - // constant). Built from the resolved key (or `None` ungoverned) and moved into the post-upgrade - // open below, exactly as `open_governed` does for the one-shot passes. - let meter = vkey.clone().map(|k| { - crate::runtime::metering::TurnMeter::new(Arc::clone(&host), k, FRONT_DOOR_POOL, dialect) - }); - // The composed provider FOR THIS LEG'S DIALECT — read off the PROCESS-WIDE composed statics - // directly (not `mount.provider()`/`mount.gemini_provider()`, whose per-slot override is borrowed - // from `mount` and so cannot outlive it) so the `'static` `on_socket` closure below captures a - // plain `Option<&'static ProviderEndpoint>` rather than a reference into the (non-'static) `arrival` - // this fn is about to move out of. - let provider = match ingress { - Ingress::Gemini => composed_gemini_provider(), - _ => composed_provider(), - }; - // The clock each served session's teardown is stamped with — the same host clock its open was. - let teardown_clock = Arc::clone(&host); - accept_gauntlet( - arrival.upgrade, - gauntlet_req, - gate, - move |stream, sink| async move { - match ingress { - // TELEPHONY / GEMINI: a thin duplex proxy. With a composed provider, dial it and pump - // both sockets through `TelephonyProxy::run` (the provider-dial leg); with none, fall back to serving the - // client socket only (the documented "governed but not dialing" posture). - Ingress::Telephony | Ingress::Gemini => match provider { - Some(p) => match open_admitted_telephony( - &rt, - codec, - owner, - call_id, - session_cfg, - meter, - now, - served_governed_session(), - ) { - Ok(proxy) => { - let pool = stream_breaker_key(dialect); - let url = p.ws_url(dialect); - match dial_provider( - host.as_ref(), - &pool, - 0, - &url, - GuardPolicy::default(), - Some(p.ws_credential(dialect)), - ) - .await - { - Ok((provider_in, provider_out)) => { - let core = Arc::clone(proxy.core()); - serve_with_sweep( - core, - proxy.run(provider_in, provider_out, stream, sink), - ) - .await; - } - Err(e) => { - // The dial failed: nothing to relay client frames to. - settle_undialed(proxy, unix_secs(&*teardown_clock)); - tracing::warn!( - error = %e, - dialect, - "streaming: provider dial failed; the just-admitted session is \ - dropped rather than served with no upstream" - ); - } - } - } - Err(_) => { /* budget/durable refusal: no durable row, socket closes */ } - }, - // NO PROVIDER COMPOSED: serve the client socket only. The uplink plane forwards - // client→server frames to a channel whose receiver is DROPPED (bare `_`, not a - // named binding), so `unbounded_send` fails fast via `is_disconnected` and each - // frame is discarded with ZERO buffering — client uplink is decoded + metered with - // no upstream to funnel to, and no unbounded queue grows for the session's life. - None => { - if let Ok((core, handle)) = open_admitted_session( - &rt, - codec, - owner, - call_id, - Some(session_cfg), - Carrier::sideband(), - meter, - now, - served_governed_session(), - ) { - let (upstream_tx, _) = futures::channel::mpsc::unbounded::>(); - serve_to_teardown( - Arc::clone(&core), - handle, - serve_messages( - stream, - sink, - Arc::new(UplinkForwarder::new(core, upstream_tx)), - ), - || unix_secs(&*teardown_clock), - ) - .await; - } - } - }, - // BROWSER-WEBRTC SIDEBAND: media is peer-to-peer by design (see `crate::topology::webrtc` - // docs) — this socket is control-only, so there is no provider leg to dial here. - Ingress::Sideband => { - if let Ok((core, handle)) = open_admitted_session( - &rt, - codec, - owner, - call_id, - Some(session_cfg), - Carrier::sideband(), - meter, - now, - served_governed_session(), - ) { - serve_to_teardown( - Arc::clone(&core), - handle, - serve_messages(stream, sink, Arc::new(VoiceSession::new(core))), - || unix_secs(&*teardown_clock), - ) - .await; - } - } - Ingress::Mint | Ingress::Sdp => { - // Never reached: the WS-accept seam mounts only Sideband/Telephony/Gemini (see - // `voice_ws_arrivals`); Mint/Sdp are the one-shot HTTP passes (`voice_routes`). - } - } - }, - ) -} - -// THIS PLANE'S SERVED-LEG WITNESSES — exported through `crate::testkit::SERVED` for a composition -// root's test build to run through its registered session runner (see the file's own header). -#[cfg(any(test, feature = "test-support"))] -#[path = "tests/served_witness.rs"] -pub(crate) mod served_witness; - -#[cfg(test)] -#[path = "tests/mount_tests.rs"] -mod mount_tests; - -// The egress-credential cell reads only `DECLS` — no host, no live money hop. -#[cfg(test)] -#[path = "tests/egress_tests.rs"] -mod egress_tests; - -// The remaining cells drive a real `EngineHost` double (breaker cell store / hook gate maps / the -// process-global metrics recorder) or a loopback provider, so they gate on `test-support`. -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/breaker_tests.rs"] -mod breaker_tests; -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/hook_gate_tests.rs"] -mod hook_gate_tests; -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/hook_tap_tests.rs"] -mod hook_tap_tests; -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/metrics_tests.rs"] -mod metrics_tests; -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/sdp_tests.rs"] -mod sdp_tests; - -// THE THREE COMPOSITION-ROOT CELLS: the provider credential the mint pass serves under, the real -// per-key metering lease a session reserves on, and the session-scope grant the door enforces. All -// three drive a real `EngineHost` double (and, for the mint, a loopback provider), so they gate on -// `test-support` exactly as the cells above do. -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/metering_lease_tests.rs"] -mod metering_lease_tests; -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/mint_tests.rs"] -mod mint_tests; -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/scope_gate_tests.rs"] -mod scope_gate_tests; - -// AUDIT-CHAIN + GOVERNANCE-BUDGET capability cells, re-homed to a gate-valid `/tests/` path (the -// equality gate rejects `runtime/tests.rs`). Runtime-gated so the durable engine / D2 lease compile; -// no `test-support` needed — they drive the durable session chain and the host metering lease only. -#[cfg(all(test, feature = "runtime"))] -#[path = "tests/audit_chain_tests.rs"] -mod audit_chain_tests; -#[cfg(all(test, feature = "runtime"))] -#[path = "tests/governance_budget_tests.rs"] -mod governance_budget_tests; - -// THE BINDING CELLS: a served session and a governed session are the same session. They pin the -// deadline against the plane crate's own declaration, so they gate on `test-support` (which is what -// carries that crate edge) as well as on the runtime the served open needs. -#[cfg(all(test, feature = "runtime", feature = "test-support"))] -#[path = "tests/governed_binding_tests.rs"] -mod governed_binding_tests; - -// BEHAVIORAL billing proof: drive a voice turn's usage through the SHIPPED Meter seam over a REAL -// governed `App` host and read the spend back off the one ledger (`GovState::usage_for`) — the -// ledger-level twin of the LLM plane's crossproto_delivery_billing oracle. Needs `test-support` for -// the real `App`/`engine_host` and `runtime` for the session metering types. -#[cfg(all(test, feature = "runtime", feature = "test-support"))] -#[path = "tests/billing_ledger_tests.rs"] -mod billing_ledger_tests; diff --git a/crates/busbar-voice/src/runtime/carrier.rs b/crates/busbar-voice/src/runtime/carrier.rs deleted file mode 100644 index 5b3d67a178..0000000000 --- a/crates/busbar-voice/src/runtime/carrier.rs +++ /dev/null @@ -1,110 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CARRIER — the live session's downlink write side and its HARD-CLOSE latch. -//! -//! A voice session is an open-ended full-duplex carrier. `busbar_kernel::ingress::byte_duplex` -//! pumps the UPSTREAM socket (server→client events in, client→server events out through the handler's -//! `out`), but the DOWNLINK toward the actual client (the browser / telephony leg) is a separate sink -//! the runtime holds here. The [`Carrier`] also owns the one thing post-hoc metering structurally -//! cannot do: a HARD CLOSE the metering lease trips the instant the budget is dry — after which no -//! further downlink audio reaches the client and the session's supervisor tears the socket down. - -use futures::channel::mpsc::UnboundedSender; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use tokio::sync::Notify; - -/// THE DOWNLINK + HARD-CLOSE handle shared across a session's per-frame handlers. Cheap to clone (an -/// `Arc` of the latch + the client sender), so every concurrent handler holds one. -#[derive(Debug, Clone)] -pub struct Carrier { - inner: Arc, -} - -#[derive(Debug)] -struct Inner { - /// Set once when the carrier hard-closes; every later downlink is dropped and the latch stays set. - closed: AtomicBool, - /// Woken on hard-close so a supervisor awaiting [`Carrier::closed`] can abort the serve task. - gate: Notify, - /// The DOWNLINK sink toward the client — one wire-framed message per send. `None` for a sideband - /// carrier (topology A) that relays no media (the browser's media path is peer-to-peer). - downlink: Option>>, -} - -impl Carrier { - /// A carrier with a downlink sink toward the client (the telephony proxy leg / a browser that - /// takes media over the same socket). - #[must_use] - pub fn with_downlink(downlink: UnboundedSender>) -> Self { - Carrier { - inner: Arc::new(Inner { - closed: AtomicBool::new(false), - gate: Notify::new(), - downlink: Some(downlink), - }), - } - } - - /// A SIDEBAND carrier that relays no media (topology A): the browser establishes the media path - /// peer-to-peer, so busbar's downlink is control-only and drops audio frames. - #[must_use] - pub fn sideband() -> Self { - Carrier { - inner: Arc::new(Inner { - closed: AtomicBool::new(false), - gate: Notify::new(), - downlink: None, - }), - } - } - - /// HARD-CLOSE the carrier: latch closed, wake any supervisor, and stop relaying downlink. Idempotent - /// — a second close is a no-op. Returns `true` the first time (the transition), `false` after. - pub fn hard_close(&self) -> bool { - let first = !self.inner.closed.swap(true, Ordering::SeqCst); - if first { - self.inner.gate.notify_waiters(); - } - first - } - - /// Whether the carrier has hard-closed. - #[must_use] - pub fn is_closed(&self) -> bool { - self.inner.closed.load(Ordering::SeqCst) - } - - /// Relay ONE wire-framed downlink message toward the client. A no-op that returns `false` once the - /// carrier is closed (the hard-close guarantee — nothing reaches the client after exhaustion) or on - /// a sideband carrier with no media path. - pub fn send_downlink(&self, frame: Vec) -> bool { - if self.is_closed() { - return false; - } - match &self.inner.downlink { - Some(tx) => tx.unbounded_send(frame).is_ok(), - None => false, - } - } - - /// Resolve when the carrier hard-closes — the await a session supervisor parks on to abort the - /// serve loop and drop the upstream socket. Returns immediately if already closed. - /// - /// REGISTER WITH THE GATE BEFORE READING THE LATCH. The wake is `notify_waiters`, which wakes the - /// waiters that are registered at the instant it runs and stores no permit for one that arrives - /// after — so a close landing between a "not closed yet" read and the registration that followed - /// it would wake nobody, and the supervisor would park for the life of the socket on a carrier - /// that had already hard-closed. Enabling first and re-reading the latch second leaves no such - /// window: a close either finds this waiter registered, or has already set the latch this reads. - pub async fn closed(&self) { - let notified = self.inner.gate.notified(); - futures::pin_mut!(notified); - notified.as_mut().enable(); - if self.is_closed() { - return; - } - notified.await; - } -} diff --git a/crates/busbar-voice/src/runtime/metering.rs b/crates/busbar-voice/src/runtime/metering.rs deleted file mode 100644 index bce2394a25..0000000000 --- a/crates/busbar-voice/src/runtime/metering.rs +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! A LIVE SESSION'S METERING — the streaming plane's per-class counts, handed to the kernel. -//! -//! A live voice carrier cannot be billed after the fact alone: the session must be HARD-CLOSED -//! mid-stream the moment the caller's budget is dry. OWNER RULING Q21b settles how, and it is the -//! same way every other plane is governed. Each closed turn's raw counts per declared class -//! (`busbar_plane_streaming::session::class_counts` — the plane's one reading of a turn) go to the -//! kernel's [`SessionAccount`], which appends them to the ledger through the one metering path and -//! answers [`TurnVerdict::Live`] or [`TurnVerdict::MustClose`] off the kernel's own budget view. The -//! session is priced at READ, against the `streams` plane's card (#47, #71); nothing here names a -//! rate, a price or a stored figure (#43). -//! -//! The D2 lease this replaces (reserve an estimate, price each turn through the flat llm card into -//! nanodollars, settle and hard-close at a cap read once at the open) is deleted; the snapshot is -//! under `~/Downloads/busbar-1.6.0-snapshots/P2-voice/`. - -use busbar_contract::billing::Usage; -pub use busbar_kernel::plane_host::session_meter::{BudgetRefused, SessionAccount, TurnVerdict}; -use busbar_kernel::plane_host::EngineHost; -use busbar_plane_streaming::session::{class_counts, TurnCounters}; -use std::sync::Arc; - -/// THE PRESENTING KEY a live session is metered for, before the session opens: the live host, the -/// resolved key, the front-door pool its buckets are filtered by, and the upstream's provider label -/// (the lane's subject when the session names no model). Built at the governed open (where the key -/// and the host are both in hand); `None` on an ungoverned deployment, which has no key to attribute -/// anything to. -pub struct TurnMeter { - host: Arc, - key: busbar_contract::records::VirtualKey, - pool: &'static str, - provider: &'static str, -} - -impl TurnMeter { - /// Bind the attribution over a live host + resolved key. - #[must_use] - pub fn new( - host: Arc, - key: busbar_contract::records::VirtualKey, - pool: &'static str, - provider: &'static str, - ) -> Self { - TurnMeter { - host, - key, - pool, - provider, - } - } - - /// OPEN the session's account at the kernel, for turns of `model`. `Err` is a chain the kernel's - /// budget view already reads dry — the session must not open. `Ok(None)` is a host with - /// governance off: nothing is ledgered and nothing closes the session on budget. - /// - /// The ledger lane is `voice` + U+001F + the model (the provider label when the session names no - /// model), so the view prices it with the `streams` card and never with the llm plane's flat one. - /// That lane is also the session's ONE metering row: the kernel's accrual mirrors each count onto - /// the row keyed `(model: , provider: voice)` — this plane's key, the way - /// an MCP or A2A row carries theirs — so `GET /admin/usage` reads it as a streams row. - pub fn open(self, model: &str) -> Result, BudgetRefused> { - let name = if model.is_empty() { - self.provider - } else { - model - }; - let lane = format!( - "{}{}{name}", - crate::PLANE_KEY, - busbar_kernel::governance::PLANE_LANE_SEP - ); - let account = - SessionAccount::open(Arc::clone(&self.host), Some(&self.key), self.pool, lane)?; - Ok(account.map(|account| SessionMetering { account })) - } -} - -/// ONE OPEN SESSION'S METERING, plane-side: the kernel account its turns are reported to. -pub struct SessionMetering { - account: SessionAccount, -} - -impl SessionMetering { - /// Report ONE closed turn — `usage` is the upstream's report (`None` for a turn that ended on an - /// error or with the session), `counters` the plane's own bookkeeping — to the kernel account, - /// which ledgers its raw counts per class and answers whether the carrier stays open. - /// - /// THAT IS THE TURN'S WHOLE METERING (#47, OWNER RULING Q32). A turn writes no series row of its - /// own: a series row counts a REQUEST, and a turn is not one — the session is admitted and paid - /// for once at its open (`fees.per_session`), and a turn of a conversation already opened draws - /// no per-request fee in the budget book. The series row this used to write carried the UPSTREAM - /// dialect label (`openai_realtime`) as its provider, so `GET /admin/usage` read it as a pools - /// row and charged the pools plane's flat `per_request_fee:` per turn; keyed by this plane - /// instead, it would have charged `streams.fees.per_request` per turn. The budget book charges - /// neither, and the view now agrees with it: the turn's counts reach the metering row through the - /// kernel's one accrual, on the plane-keyed row [`TurnMeter::open`] names. - pub fn report_turn( - &self, - usage: Option<&crate::ir::usage::IrDuplexUsage>, - counters: TurnCounters, - ) -> TurnVerdict { - self.account.report_turn(&plane_counts(usage, counters)) - } - - /// The session is SERVED: its provider leg is up (a socket session) or its one-shot pass - /// answered success (a mint, an SDP broker). The session fee the kernel counted at the open is - /// final; marking it again changes nothing. - pub fn served(&self) { - self.account.served(); - } - - /// The session's open failed after its account counted it — its mint, SDP broker, provider dial - /// or durable open: the kernel gives the session fee back (TODO 17(b), ARCHITECT R4), exactly - /// once, since a session that never opened charges nothing. A no-op once the session served. - pub fn refund_open(&self) { - self.account.refund_open(); - } -} - -/// A closed turn's raw counts per class, as the streaming plane reads them, on the neutral carrier -/// the ledger takes. -#[must_use] -pub fn plane_counts( - usage: Option<&crate::ir::usage::IrDuplexUsage>, - counters: TurnCounters, -) -> Usage { - Usage { - usage_units: class_counts(usage, counters) - .into_iter() - .map(|(class, n)| (class.as_str().to_string(), n)) - .collect(), - } -} diff --git a/crates/busbar-voice/src/runtime/mod.rs b/crates/busbar-voice/src/runtime/mod.rs deleted file mode 100644 index 49044050cd..0000000000 --- a/crates/busbar-voice/src/runtime/mod.rs +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE T2 VOICE SESSION RUNTIME — the live duplex engine (design `BUSBAR-1.6.0.md` #18/#45). Behind the -//! `runtime` cargo feature (OFF by default, HARD RULE 4): the default / prod build compiles the IR + -//! declarations only, so the workspace is unaffected regardless of this module's state. -//! -//! The runtime binds the neutral byte-duplex pump (`serve_messages`), the codec's -//! `DuplexReader`/`DuplexWriter` pair, the durable `SessionScope`, and the kernel session account -//! (per-class counts, ledgered and budget-checked kernel-side) into one governed carrier — exposed to -//! both topologies (`crate::topology`). - -pub mod carrier; -pub mod metering; -pub mod scope; -pub mod session; -/// THE SERVER-SIDE TOOL EXECUTOR PORT, RE-EXPORTED FROM `busbar-plane-streaming`. `ToolExecutor` and -/// `EchoToolExecutor` moved to the plane crate for the same reason the governed-call port below did: -/// the port is what a tool call MEANS to this plane, and it names nothing this crate owns. Re-exported -/// as a MODULE, not just its items, so `crate::runtime::tools::EchoToolExecutor` — the spelling the -/// topology and governed-binding cells use — resolves exactly what it always did. -pub use busbar_plane_streaming::tools; - -pub use carrier::Carrier; -// THE GOVERNED-CALL PORT, RE-EXPORTED FROM `busbar-plane-streaming`. `GovernedCalls` / `ReplyRefusal` / -// `GovernedSession` moved to the plane crate, beside the two declarations the composition root -// already pairs them with (`busbar_plane_streaming::plane::{FACT_TOOL_CORRELATION, -// TOOL_REPLY_DEADLINE_SECS}`) — the port and the key a wait is entered under are now one crate, so -// they cannot drift apart in silence. Re-exported HERE under the old name so every caller that -// spells `busbar_voice::runtime::GovernedCalls` resolves exactly what it always did. The split is a -// MOVE: no item changed shape crossing it. -pub use busbar_plane_streaming::governed::{GovernedCalls, GovernedSession, ReplyRefusal}; -pub use metering::{BudgetRefused, SessionMetering, TurnMeter, TurnVerdict}; -pub use scope::{SessionHandle, VoiceSessionRow}; -pub use session::{ - serve_to_teardown, serve_with_sweep, Outbound, SessionCore, UplinkForwarder, VoiceSession, -}; -pub use tools::{EchoToolExecutor, ToolExecutor}; - -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use std::sync::Arc; - -/// THE PLANE'S PER-GENERATION RUNTIME OBJECT — the type-erased slot `PLANE_HOOKS.build_runtime` builds -/// (see `crate::PLANE_HOOKS`). It carries the process-wide dependencies a session is assembled from: the -/// durable-handle engine sessions bind into and the server-side tool executor. The plane holds no -/// pricing of its own: a session's metering is the kernel account its governed open binds. A session -/// (either topology) is constructed FROM this object; it holds no per-session state itself. -pub struct VoiceRuntime { - /// The process-wide durable-handle engine every session's [`SessionHandle`] binds into. - pub engine: Arc, - /// The server-side tool executor (the tool moat) shared across sessions. - pub tools: Arc, - /// THE LOCKED SESSION DEFAULTS every session opens with, read from the operator's `streams.session:` - /// (VAD/media/tool set). Seeded from [`crate::config::StreamsCfg`] at [`build_runtime`]; the pump - /// re-applies it server-side so a client `session.update` is reconciled against it, never trusted - /// blind. - pub session_defaults: crate::ir::config::SessionConfig, - /// The hard session wall-clock ceiling (`streams.session_max_secs:`), bound onto every session - /// core at open and compared on the sweep tick beside the pump (`SessionCore::enforce_ceiling`). - /// `None` — the default, and what an operator who writes nothing gets — is no ceiling (Q21a). - pub session_max_secs: Option, - /// The context-window ceiling (`streams.context_window_tokens:`). - pub context_window_tokens: u32, - /// The per-response output-token ceiling (`streams.max_output_tokens:`). - pub max_output_tokens: u32, - /// THE LIVE HOST — bound only by [`build_runtime_hosted`] once a mounted route hands the runtime a - /// real `Arc`. `None` on the pre-host/dev-default runtime (no admin-audit trail to - /// write to). Carried so a governed session mutation can land ONE admin-audit row through the SAME - /// seam the host's other planes journal through (see [`VoiceRuntime::audit_session`]). - pub host: Option>, -} - -impl VoiceRuntime { - /// Assemble a runtime object from its dependencies. - #[must_use] - pub fn new(engine: Arc, tools: Arc) -> Self { - let defaults = crate::config::StreamsCfg::default(); - VoiceRuntime { - engine, - tools, - session_defaults: defaults.session, - session_max_secs: defaults.session_max_secs, - context_window_tokens: defaults.context_window_tokens, - max_output_tokens: defaults.max_output_tokens, - host: None, - } - } - - /// SEED the locked session config + the three session ceilings from the operator's `streams:` - /// section. Called by [`build_runtime`] with the plane's own typed [`crate::config::StreamsCfg`] - /// (an absent section falls back to `StreamsCfg::default()`), so the runtime a session is built - /// FROM carries the operator's real posture rather than the dev defaults. - #[must_use] - pub fn with_streams(mut self, cfg: &crate::config::StreamsCfg) -> Self { - self.session_defaults = cfg.session.clone(); - self.session_max_secs = cfg.session_max_secs; - self.context_window_tokens = cfg.context_window_tokens; - self.max_output_tokens = cfg.max_output_tokens; - self - } - - /// Bind a fresh [`SessionHandle`] for `(owner, id)` into this runtime's durable engine. - #[must_use] - pub fn bind_session(&self, owner: impl Into, id: impl Into) -> SessionHandle { - SessionHandle::bind(Arc::clone(&self.engine), owner, id) - } - - /// Land ONE admin-audit row for a voice-plane mutation through the live host's `JournalHost` leg — - /// a no-op on a runtime with no bound host (the pre-host/dev-default runtime, or an ungoverned - /// deployment resolving no key has nothing to attribute the row to; callers pass a real principal - /// only when one was resolved). `outcome` is a fixed vocabulary literal, matching every other - /// plane's `audit_record` call shape. - pub(crate) fn audit_session( - &self, - action: &str, - resource: &str, - outcome: &'static str, - principal: &str, - ) { - if let Some(host) = &self.host { - host.audit_record(action, resource, outcome, principal); - } - } -} - -/// THE `PLANE_HOOKS.build_runtime` HOOK BODY — builds the plane's per-generation runtime object -/// (type-erased as `Arc`), the seam the composition root composes the voice -/// runtime slot through (see `crate::PLANE_HOOKS`). Wired behind the `runtime` feature; the default -/// (feature-off) build leaves the hook `None`. -/// -/// WHAT IS WIRED, AND WHAT REMAINS A DEV DEFAULT. This entry reads the REAL `streams:` config (session -/// posture / ceilings, via `with_streams`). A session's metering is not a runtime dependency at all: -/// it is the kernel account the governed open binds for the presenting key over the live host. What is -/// still a dev default is the durable engine (a fresh [`DurableHandleEngine`]) and the tool executor -/// ([`EchoToolExecutor`]): deriving the config-driven engine/tool set is a SEPARATE, tracked slice. -/// `prior` (carry-over) is ignored today; the signature is the real one so binding those -/// config-derived dependencies is a body change, not an ABI change. -pub fn build_runtime( - section: &dyn std::any::Any, - _prior: Option<&dyn busbar_kernel::plane_host::PlaneSlots>, -) -> Arc { - // READ THE REAL `streams:` CONFIG: core passes the plane's own typed section as `cfg.streams.as_any()` - // (the `PlaneCfg::as_any` of `StreamsCfg`). An absent/other section downcasts to `None` and falls back - // to the plane default, so a deployment with no `streams:` block still builds a runtime — with the - // plane's own default posture, not an empty one. - let streams = section - .downcast_ref::() - .cloned() - .unwrap_or_default(); - Arc::new( - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ) - .with_streams(&streams), - ) -} - -/// THE PRODUCTION composition entry — take a generation's runtime and bind the live host onto it. -/// Called by every mounted route the moment the live host is in hand (the route layer is where an -/// `Arc` first exists — the per-generation slot is built before any request), so the -/// session's admin-audit row journals through the host. -/// -/// Everything else is SHARED with `base`, not rebuilt: the same durable-handle engine (so a session -/// opened on one request is the same durable working set another request sees), the same tool -/// executor, and the same operator session posture and ceilings. -#[must_use] -pub fn build_runtime_hosted( - base: &VoiceRuntime, - host: Arc, -) -> VoiceRuntime { - VoiceRuntime { - engine: Arc::clone(&base.engine), - tools: Arc::clone(&base.tools), - session_defaults: base.session_defaults.clone(), - session_max_secs: base.session_max_secs, - context_window_tokens: base.context_window_tokens, - max_output_tokens: base.max_output_tokens, - host: Some(host), - } -} - -#[cfg(test)] -mod tests; - -// The open-call table as a served session pump reaches it (the table itself is the plane's). -#[cfg(test)] -#[path = "../tests/governed_table_tests.rs"] -mod governed_table_tests; - -// THE VOICE PLANE-UNIT BILLING ORACLE — the voice money-path regression backstop that replaces the D2 -// lease oracle (Q21b): one fixed session script, every per-class count it ledgers and every verdict -// the kernel answers pinned. -#[cfg(test)] -#[path = "voice_plane_unit_oracle.rs"] -mod voice_plane_unit_oracle; diff --git a/crates/busbar-voice/src/runtime/scope.rs b/crates/busbar-voice/src/runtime/scope.rs deleted file mode 100644 index 1624da2d78..0000000000 --- a/crates/busbar-voice/src/runtime/scope.rs +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CONNECTION-LIFETIME SESSION STATE — a thin binding over the neutral -//! `busbar_kernel::plane_host::SessionScope` (design `BUSBAR-1.6.0.md` #18/#45). One voice session owns ONE durable handle -//! in the process-wide `DurableHandleEngine`, keyed by `(owner, id)`. The owner is load-bearing: a -//! second session bound to the same `id` under a DIFFERENT owner collapses to the exact same -//! indistinguishable refusal (`HandleDenied::NotYours` / `ScopedMutateError::NotYours`) — a foreign -//! owner can neither read, resume, nor evict the handle, and cannot even tell it exists. That -//! anti-enumeration contract is carried up from the engine unchanged; this module only stamps the -//! session's `(owner, id)` into the row and drives open → bump → close. - -pub use crate::plane_session_row::VoiceSessionRow; -use crate::plane_session_row::STREAMING_SESSION_KIND; -use busbar_contract::records::RecordStoreResult; -use busbar_kernel::plane::handle_engine::{ - ChainPosition, DurableHandleEngine, HandleEngineError, HandleMeta, Mutation, RehydrateCounts, - RehydrateOutcome, ScopedMutateError, SealedEvent, SubmitRecord, SweepBounds, -}; -use busbar_kernel::plane::store::PlaneStore; -use busbar_kernel::plane_host::SessionScope; -use std::sync::Arc; - -/// Retain a settled session for an hour past terminal, and cap the settled population — plain, -/// generous bounds for a long-lived carrier (the plane's own retention policy, not a wire fact). A -/// live session is never retired by retention; it ends at its teardown. -fn session_bounds() -> SweepBounds { - SweepBounds { - terminal_ttl_secs: 3_600, - max_retained: 4_096, - } -} - -/// The row's handle metadata, as the kernel's handle engine indexes it. -fn row_meta(row: &VoiceSessionRow) -> HandleMeta { - HandleMeta { - owner: row.owner.clone(), - updated_at: row.updated_at, - terminal: row.terminal, - cursor: row.turns, - } -} - -/// The row as the opaque value the handle engine stores. -fn row_arc(row: VoiceSessionRow) -> Arc { - Arc::new(row) -} - -/// A VOICE SESSION'S DURABLE BINDING — the [`SessionScope`] plus the plane's row shape. Open it at -/// session start, bump it per settled turn, close it (owner-gated, terminal-only) at teardown. -pub struct SessionHandle { - scope: SessionScope, -} - -impl SessionHandle { - /// Bind a session to the durable handle keyed by `id`, attributed to `owner`, in `engine`. Pure - /// binding — touches the engine only on [`open`](Self::open) / [`bump_turn`](Self::bump_turn) / - /// [`close`](Self::close). Use to attach to a boot-rehydrated handle, or before [`open`](Self::open) - /// submits a fresh one. - #[must_use] - pub fn bind( - engine: Arc, - owner: impl Into, - id: impl Into, - ) -> Self { - SessionHandle { - scope: SessionScope::new(engine, owner, id), - } - } - - /// The principal this session's handle is attributed to. - #[must_use] - pub fn owner(&self) -> &str { - self.scope.owner() - } - - /// The opaque handle id this session is bound to. - #[must_use] - pub fn id(&self) -> &str { - self.scope.id() - } - - /// OPEN the session's durable handle at genesis, stamping the row with THIS session's `(owner, id)` - /// — the binding contract the scope documents (a genesis under a diverging owner/id would leave the - /// session unable to read what it just opened). - pub fn open(&self, now: u64) -> Result<(), HandleEngineError> { - let row = VoiceSessionRow { - id: self.scope.id().to_string(), - owner: self.scope.owner().to_string(), - turns: 0, - updated_at: now, - terminal: false, - rtc_call_id: None, - }; - self.scope.open( - now, - session_bounds(), - |_pos: &ChainPosition| { - let record = row.record(); - let meta = row_meta(&row); - Ok(SubmitRecord { - id: row.id.clone(), - row: row_arc(row.clone()), - meta, - row_record: record.clone(), - event: Some(SealedEvent { - record, - tail_hash: format!("streaming-genesis-{}", row.id), - }), - }) - }, - // No durable sink attached in this build ⇒ no rollback failures to report. - |_id, _e| {}, - )?; - Ok(()) - } - - /// Read the current durable row through the engine's SCOPED read — a foreign-owner session is - /// refused with the one indistinguishable `NotYours`, never learning whether the handle exists. - pub fn get(&self) -> Option { - self.scope - .get() - .ok() - .and_then(|row| row.downcast_ref::().cloned()) - } - - /// BUMP the turn cursor (owner-gated mutate) — the per-turn durable checkpoint. Returns the new - /// turn count, or the scoped mutate error (`NotYours` for a foreign owner). - pub fn bump_turn(&self, now: u64) -> Result { - let row = self.scope.mutate(|row, _pos| { - let cur = row - .downcast_ref::() - .expect("streaming session row"); - let mut next = cur.clone(); - next.turns += 1; - next.updated_at = now; - Ok(Some(mutation_for(next))) - })?; - Ok(row - .downcast_ref::() - .map(|r| r.turns) - .unwrap_or_default()) - } - - /// STAMP the `rtc_` correlation key (owner-gated mutate) — the SDP broker calls this with - /// the `Location` header it preserved from `POST /v1/realtime/calls`, so the durable row and the - /// sideband dial provably name the same brokered media call. A foreign owner is refused with the one - /// indistinguishable `NotYours`, exactly as every other mutate on this handle. - pub fn set_rtc_call_id(&self, rtc_call_id: &str, now: u64) -> Result<(), ScopedMutateError> { - self.scope.mutate(|row, _pos| { - let cur = row - .downcast_ref::() - .expect("streaming session row"); - let mut next = cur.clone(); - next.rtc_call_id = Some(rtc_call_id.to_string()); - next.updated_at = now; - Ok(Some(mutation_for(next))) - })?; - Ok(()) - } - - /// Drive the session TERMINAL (owner-gated) so it can be evicted — the settle step before close. - pub fn settle_terminal(&self, now: u64) -> Result<(), ScopedMutateError> { - self.scope.mutate(|row, _pos| { - let cur = row - .downcast_ref::() - .expect("streaming session row"); - let mut next = cur.clone(); - next.terminal = true; - next.updated_at = now; - Ok(Some(mutation_for(next))) - })?; - Ok(()) - } - - /// TEAR the session DOWN: drive it terminal, then evict it — the one call a serving site makes - /// when its pump returns. Returns `true` when the handle was this session's and is now gone. - /// - /// Every serving site used to drop its handle unsettled, so its durable row stayed ACTIVE after - /// the socket was gone and nothing but the retention sweep could ever end it. - pub fn finish(&self, now: u64) -> bool { - self.settle_terminal(now).is_ok() && self.close() - } - - /// CLOSE the session: evict the terminal handle from the working set (owner-gated, terminal-only), - /// leaving durable rows behind. Returns `true` only when this session owns the handle AND it was - /// terminal; a foreign owner or a still-active handle returns `false`. - pub fn close(&self) -> bool { - self.scope.close() - } -} - -/// BOOT-REHYDRATE the durable `voice_session` working-set from `store` into `engine` — the -/// [`crate::PLANE_HOOKS`] `hydrate` step, mirroring the A2A task-set restore. Reads every persisted -/// `voice_session` row through the neutral [`DurableHandleEngine::rehydrate`] seam and, per row: -/// installs an ACTIVE session back into the working set, counts (and leaves) a TERMINAL one, and counts -/// (and skips) a row whose durable body cannot be decoded — so one unreadable row never aborts the -/// whole restore. Only a store-level list failure is fatal (propagated). Returns the neutral counts. -/// -/// This restores the DURABLE binding a resumed session reattaches to via [`SessionHandle::bind`]; the -/// live carrier/socket of a session cannot survive a restart, so a rehydrated session is a durable -/// record to reattach or reconcile, not a live pump — exactly the A2A task-restore contract. -pub fn rehydrate_sessions( - engine: &DurableHandleEngine, - store: &dyn PlaneStore, -) -> RecordStoreResult { - engine.rehydrate(store, STREAMING_SESSION_KIND, |_store, body| { - // Decode the row the durable body IS; an undecodable body is counted unreadable, never fatal. - match serde_json::from_slice::(body) { - Ok(row) if row.terminal => Ok(RehydrateOutcome::Terminal), - Ok(row) => Ok(RehydrateOutcome::Active { - id: row.id.clone(), - meta: row_meta(&row), - row: row_arc(row.clone()), - // Voice sessions carry no resumable event chain beyond genesis in this build. - pos: ChainPosition::genesis(), - event_unreadable: 0, - }), - Err(_) => Ok(RehydrateOutcome::Unreadable), - } - }) -} - -/// Wall-clock Unix seconds — the clock a serving site stamps its teardown with. The retention sweep -/// ages a row by these same seconds, so a teardown stamped on another clock would age wrongly. -#[must_use] -pub fn unix_now_secs() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0) -} - -fn mutation_for(next: VoiceSessionRow) -> Mutation { - let record = next.record(); - let meta = row_meta(&next); - Mutation { - row: Some(row_arc(next)), - meta: Some(meta), - row_record: Some(record), - event: None, - } -} diff --git a/crates/busbar-voice/src/runtime/session.rs b/crates/busbar-voice/src/runtime/session.rs deleted file mode 100644 index ffbb6a0004..0000000000 --- a/crates/busbar-voice/src/runtime/session.rs +++ /dev/null @@ -1,390 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE LIVE DUPLEX SESSION RUNTIME — the pump body, behind the `runtime` feature. -//! -//! Binds the neutral byte-duplex pump (`busbar_kernel::ingress::byte_duplex::serve_messages`), the -//! codec's `DuplexReader`/`DuplexWriter` pair, the durable `SessionScope`, and the kernel session -//! account (each closed turn's per-class counts, ledgered and budget-checked kernel-side) into one -//! governed carrier. The runtime is GENERIC over the codec traits (HARD RULE 3) so it does -//! not depend on WHICH dialect codec is present — the Gemini codec drops in unchanged. -//! -//! CONCURRENCY POSTURE. The neutral pump `tokio::spawn`s one `handle` per inbound frame, so the -//! decode+act logic lives in a SYNCHRONOUS, self-contained core ([`SessionCore::on_server_frame`] / -//! [`SessionCore::on_client_frame`]) that returns an [`Outbound`] plan; the `DuplexPlane` glue merely -//! drives that plan onto `out` (upstream) and the [`Carrier`] (downlink / hard-close). That keeps the -//! marquee behaviours — tool correlation, barge-in truncate, metered hard-close — deterministically -//! unit-testable without the async plumbing, while the pump integration is tested separately. - -use crate::ir::codec::{DuplexReader, DuplexWriter, WireEvent}; -use crate::ir::config::SessionConfig; -use crate::ir::usage::IrDuplexUsage; -use crate::runtime::carrier::Carrier; -use crate::runtime::metering::{SessionMetering, TurnVerdict}; -use crate::runtime::tools::ToolExecutor; -use busbar_plane_streaming::governed::GovernedSession; -use busbar_plane_streaming::session::TurnCounters; -use busbar_plane_streaming::session_pump::{SessionPump, TurnSink}; -use bytes::Bytes; -use std::sync::{Arc, Mutex}; - -use busbar_kernel::ingress::byte_duplex::{CallRef as WireCallRef, DuplexHandle, DuplexPlane}; - -/// THE FRAME PLAN one decoded inbound frame produces — what to write UPSTREAM (client→server events: -/// tool results, barge-in cancel/truncate, `response.create`), what to relay DOWNLINK to the client, -/// and whether the kernel's budget verdict tripped a HARD CLOSE this frame. Re-exported with the named reason a -/// session closed at its configured wall-clock ceiling is told under. -pub use busbar_plane_streaming::session_pump::{Outbound, SESSION_CEILING_REASON}; - -/// A closed turn's way to the kernel account: the session's metering, when it has one. A turn the -/// account answers `MustClose` for cuts the session. -struct MeterSink<'m>(Option<&'m SessionMetering>); - -impl TurnSink for MeterSink<'_> { - fn turn_closed(&mut self, usage: Option<&IrDuplexUsage>, counters: TurnCounters) -> bool { - match self.0 { - Some(metering) => metering.report_turn(usage, counters) != TurnVerdict::MustClose, - None => true, - } - } -} - -/// THE GOVERNED SESSION CORE — the synchronous heart shared across the concurrent frame handlers. It -/// holds the plane's session pump (the codec, the locked config the browser cannot override, the -/// in-flight tool calls, the open turn's counters, the node's open-call binding) behind one lock, -/// and around it the session's metering (each closed turn's counts go to the kernel account), the -/// tool executor, the carrier and the session's wall-clock ceiling. Generic over the codec `C` -/// (HARD RULE 3); the tool executor is a dependency-inverted port. -pub struct SessionCore { - pump: Mutex>, - /// The session's metering: each closed turn's raw counts per class go to the kernel account, - /// which ledgers them through the one metering path and answers whether the carrier stays open. - /// `None` on an ungoverned deployment — nothing to attribute, nothing to close on. - metering: Option, - tools: Arc, - carrier: Carrier, - /// When this session opened — the start of the wall clock [`Self::ceiling`] bounds. - opened: std::time::Instant, - /// The hard session wall-clock ceiling (`streams.session_max_secs:`), `None` until a runtime - /// binds one ([`Self::with_session_ceiling`]). Compared on the sweep tick beside the pump - /// ([`Self::enforce_ceiling`]): a session past it is hard-closed exactly as a dry budget is. - ceiling: Option, -} - -impl SessionCore -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - /// Assemble a session core. `metering` is the session's OPEN kernel account (opened, and its - /// budget checked, at session start); `locked_config` is the plane's authoritative - /// tools+instructions. - pub fn new( - codec: C, - metering: Option, - tools: Arc, - carrier: Carrier, - locked_config: Option, - ) -> Self { - SessionCore { - pump: Mutex::new(SessionPump::new(codec, locked_config)), - metering, - tools, - carrier, - opened: std::time::Instant::now(), - ceiling: None, - } - } - - /// The session is SERVED: the session fee its account counted at the open is final. Every - /// socket session is marked by [`serve_with_sweep`], the one loop each serves through; a one-shot - /// pass marks it when its answer succeeds. Idempotent. - pub fn served(&self) { - if let Some(metering) = &self.metering { - metering.served(); - } - } - - /// The session's open FAILED after its account counted the session fee — its mint, SDP broker or - /// provider dial failed, or its one-shot pass answered anything but success: give the fee back - /// (TODO 17(b), ARCHITECT R4), exactly once. A no-op on a served session, or an ungoverned one. - pub fn refund_open(&self) { - if let Some(metering) = &self.metering { - metering.refund_open(); - } - } - - /// Bind the hard session wall-clock ceiling (`streams.session_max_secs:`). - #[must_use] - pub fn with_session_ceiling(mut self, secs: u32) -> Self { - self.ceiling = Some(std::time::Duration::from_secs(u64::from(secs))); - self - } - - /// Close the session when it has run past its ceiling at `now`: the caller is told why, in its - /// dialect, and the carrier hard-closes. `true` when it closed; a session with no ceiling never - /// does. - pub fn enforce_ceiling(&self, now: std::time::Instant) -> bool { - let Some(ceiling) = self.ceiling else { - return false; - }; - if now.saturating_duration_since(self.opened) < ceiling { - return false; - } - if !self.carrier.is_closed() { - let told = self.lock().ceiling_error(ceiling.as_secs()); - if let Some(frame) = told { - self.carrier.send_downlink(frame.0.to_vec()); - } - } - self.carrier.hard_close(); - true - } - - /// Bind the node's open-call table for this session. - #[must_use] - pub fn with_governed(self, governed: GovernedSession) -> Self { - self.lock().bind_governed(governed); - self - } - - /// The node's session id, when a table is bound. - #[must_use] - pub fn governed_session(&self) -> Option { - self.lock().governed_session() - } - - /// Sweep the table's unanswered calls past their deadline at `now_ms`; how many ended. - pub fn sweep_expired(&self, now_ms: u64) -> usize { - self.lock().sweep_expired(now_ms) - } - - /// The session's carrier. - pub fn carrier(&self) -> &Carrier { - &self.carrier - } - - fn lock(&self) -> std::sync::MutexGuard<'_, SessionPump> { - self.pump.lock().expect("session inner poisoned") - } - - /// A FRAME FROM THE SERVED SOCKET's far side. Decoded and answered by the plane's pump; the - /// calls this session serves itself run here, off the lock, in close order. - pub async fn on_server_frame(&self, frame: WireEvent) -> Outbound { - if self.carrier.is_closed() { - return Outbound::default(); - } - let tools = Arc::clone(&self.tools); - let (mut out, to_exec) = self.lock().on_server_frame( - frame, - now_ms(), - &mut MeterSink(self.metering.as_ref()), - &|name: &str| tools.executes_here(name), - ); - for run in to_exec { - let output = self.tools.execute(&run.name, &run.args).await; - self.lock().tool_executed(run, output, &mut out); - } - if out.close { - self.carrier.hard_close(); - } - out - } - - /// A FRAME FROM THE CALLER, decoded and answered by the plane's pump. - pub fn on_client_frame(&self, frame: WireEvent) -> Outbound { - if self.carrier.is_closed() { - return Outbound::default(); - } - self.lock().on_client_frame(frame) - } - - /// The session ends: the table forgets every call it had open. - pub fn forget_governed_calls(&self) { - self.lock().forget_governed_calls(); - } - - /// The session ends with a turn open: its counters settle, once. - pub fn settle_open_turn(&self) { - self.lock() - .settle_open_turn(&mut MeterSink(self.metering.as_ref())); - } -} - -/// How often the tick beside a session's pump sweeps its governed calls. -/// -/// One second, against a reply deadline the plane declares in tens of seconds: fine enough that a -/// call ends within a second of the deadline it was given, coarse enough that an idle session's tick -/// is not a cost. The deadline itself is not this crate's to name — the leg the root plans carries it, -/// and the sweep only asks the table which of them have passed. -const SWEEP_EVERY: std::time::Duration = std::time::Duration::from_secs(1); - -/// **THE TICK BESIDE THE PUMP.** Run one session's frame loop with its governed-call sweep beside it, -/// and end when the pump does. -/// -/// The wake has a frame to arrive on. The sweep does not: a client that simply never replies sends -/// nothing, so there is no frame on which anyone would notice, and a wait nobody sweeps is a hold -/// nobody settles. This is the only place in the session's life where time passing is itself an -/// event. -/// -/// It rides each session's own pump rather than a process-wide loop, which is the shape this plane -/// already has — voice spawns no boot task, and a sweep that outlived the socket it was sweeping for -/// would be exactly the background loop the plane's start hook says it does not have. When the pump -/// returns, the tick is dropped with it. -pub async fn serve_with_sweep(core: Arc>, pump: F) -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, - F: std::future::Future, -{ - // The session starts to serve: the fee its open counted is final, and no refund can follow. - core.served(); - let sweep = async { - loop { - tokio::time::sleep(SWEEP_EVERY).await; - // A closed carrier is a conversation that is over; nothing left on it can be answered and - // the pump is on its way out anyway. - if core.carrier().is_closed() { - return; - } - // The session wall-clock ceiling is time passing, too, and so is noticed here: a session - // past it is hard-closed and the pump ends with the sweep. - if core.enforce_ceiling(std::time::Instant::now()) { - return; - } - core.sweep_expired(now_ms()); - } - }; - futures::pin_mut!(pump); - futures::pin_mut!(sweep); - // The sweep never completes on its own, so this ends when — and only when — the pump does. - futures::future::select(pump, sweep).await; -} - -/// **SERVE A SESSION TO ITS TEARDOWN.** [`serve_with_sweep`], then settle the session's durable row -/// terminal and evict it, stamped at `now()` taken when the pump returns. -/// -/// Every serving site used to drop its [`crate::runtime::SessionHandle`] unsettled once the pump -/// returned, so the row outlived the socket as an ACTIVE session. This is the one shape a serving -/// site takes instead, so none of them can forget the second half. -pub async fn serve_to_teardown( - core: Arc>, - handle: crate::runtime::SessionHandle, - pump: F, - now: N, -) where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, - F: std::future::Future, - N: FnOnce() -> u64, -{ - serve_with_sweep(Arc::clone(&core), pump).await; - core.settle_open_turn(); - core.forget_governed_calls(); - handle.finish(now()); -} - -/// Wall-clock milliseconds, for the sweep's "which deadlines have passed" question. -/// -/// The reading is handed to the node's table rather than compared here: which calls are past their -/// deadline is the table's judgement, and a clock read on this side that the table then re-derived -/// would be two clocks deciding one deadline. -fn now_ms() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX)) - .unwrap_or(0) -} - -/// THE UPSTREAM-FACING PLANE — bound to the socket busbar holds to the provider (OpenAI Realtime). The -/// neutral pump reads server→client events off it; each `handle` decodes one and drives the plan onto -/// `out` (the client→server write side of the SAME socket) and the carrier (downlink to the client). -pub struct VoiceSession { - core: Arc>, -} - -impl VoiceSession { - /// Bind the plane to a session core. - pub fn new(core: Arc>) -> Self { - VoiceSession { core } - } - - /// The shared session core. - pub fn core(&self) -> &Arc> { - &self.core - } -} - -#[async_trait::async_trait] -impl DuplexPlane for VoiceSession -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - fn classify(&self, _frame: &[u8]) -> Option { - // OpenAI Realtime events are fire-and-forget notifications, never a reply to a transport-level - // call busbar issued — correlation is done at the IR layer (voice `CallRef`), not here. - None - } - - async fn handle(self: Arc, frame: Vec, out: DuplexHandle) { - let plan = self - .core - .on_server_frame(WireEvent(Bytes::from(frame))) - .await; - for up in plan.upstream { - // THIS leg's policy for a write that fails: DIAGNOSE AND STOP SENDING. The upstream - // socket is a session-long conversation, so a frame that never lands leaves the far end - // holding a state busbar no longer shares — and every later frame of this plan would be - // written into the same dead socket. Abandon the rest of the plan rather than emit - // frames the peer will never see; the downlink and the close below still run, so the - // client is still told. - if let Err(e) = out.emit(up.0.to_vec()).await { - tracing::warn!( - error = %e, - "streaming: an upstream frame could not be written; abandoning the rest of this plan's upstream" - ); - break; - } - } - for down in plan.downlink { - self.core.carrier.send_downlink(down.0.to_vec()); - } - if plan.close { - self.core.carrier.hard_close(); - } - } -} - -/// THE UPLINK-FACING PLANE — bound to the socket busbar holds to the CLIENT (the telephony leg). The -/// neutral pump reads client→server frames off it; each `handle` decodes one, reconciles it against the -/// locked config, and forwards it to the UPSTREAM socket through the shared `upstream` sink (NOT this -/// socket's `out`, which is the downlink toward the client). -pub struct UplinkForwarder { - core: Arc>, - upstream: futures::channel::mpsc::UnboundedSender>, -} - -impl UplinkForwarder { - /// Bind the uplink plane to a session core and the shared upstream sink. - pub fn new( - core: Arc>, - upstream: futures::channel::mpsc::UnboundedSender>, - ) -> Self { - UplinkForwarder { core, upstream } - } -} - -#[async_trait::async_trait] -impl DuplexPlane for UplinkForwarder -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - fn classify(&self, _frame: &[u8]) -> Option { - None - } - - async fn handle(self: Arc, frame: Vec, _out: DuplexHandle) { - let plan = self.core.on_client_frame(WireEvent(Bytes::from(frame))); - for up in plan.upstream { - // Funnel to the single upstream writer shared with the downlink-facing plane. - let _ = self.upstream.unbounded_send(up.0.to_vec()); - } - } -} diff --git a/crates/busbar-voice/src/runtime/tests.rs b/crates/busbar-voice/src/runtime/tests.rs deleted file mode 100644 index aafc5fd9bf..0000000000 --- a/crates/busbar-voice/src/runtime/tests.rs +++ /dev/null @@ -1,1013 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! T2 RUNTIME TESTS (behind `runtime`): pump lifecycle, tool-call correlation under interleaving, -//! SessionScope reattach + foreign-owner refusal, and the session's METERING — each closed turn's raw -//! counts per class handed to the kernel's session account (Q21b), and the HARD CLOSE the kernel's -//! budget view answers when a turn dries the caller's chain. The WS/transport is mocked with in-memory -//! `futures` channel pairs; the host is the fixture host, whose budget view a test sets or caps by -//! count (what a count is WORTH is proven over the real engine in the composition root's tests). - -use crate::ir::codec::{OpenAiRealtimeCodec, WireEvent}; -use crate::runtime::carrier::Carrier; -use crate::runtime::metering::{SessionMetering, TurnMeter, TurnVerdict}; -use crate::runtime::scope::SessionHandle; -use crate::runtime::session::{SessionCore, VoiceSession}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::ingress::byte_duplex::serve_messages; -use busbar_kernel::plane::handle_engine::{DurableHandleEngine, HandleDenied, ScopedMutateError}; -use bytes::Bytes; -use futures::channel::mpsc::{unbounded, UnboundedReceiver}; -use futures::StreamExt; -use std::collections::BTreeMap; -use std::sync::Arc; - -// ── fixtures ────────────────────────────────────────────────────────────────────────────────────── - -/// The presenting key every metered cell opens its session for. -fn caller() -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: "vk-voice".to_string(), - name: "voice".to_string(), - ..Default::default() - } -} - -/// The lane an unmodelled OpenAI Realtime session's counts are ledgered under: the voice plane's key, -/// the separator, and the provider label (the dialect carries its model server-side). -const LANE: &str = "streaming\u{1f}openai_realtime"; - -/// A governed fixture host whose budget view is ONE bucket capped at `cap` counts (`None`: uncapped). -fn governed_host(cap: Option) -> Arc { - let host = FixtureHost::new().governed(); - Arc::new(match cap { - Some(cap) => host.with_count_cap(cap), - None => host, - }) -} - -/// Open the presenting key's kernel account over `host`. -fn metering(host: &Arc) -> SessionMetering { - TurnMeter::new( - Arc::clone(host) as Arc, - caller(), - "streaming-server", - crate::OPENAI_REALTIME, - ) - .open("") - .expect("a chain with room opens") - .expect("a governed host opens an account") -} - -/// A downlink-facing core with a real downlink sink, the echo tool executor, and the session's -/// metering over `host` (`None`: an ungoverned session). Returns the core plus the downlink receiver -/// the client would read. -fn core_on( - host: Option<&Arc>, -) -> ( - Arc>, - UnboundedReceiver>, -) { - let (dtx, drx) = unbounded::>(); - let core = Arc::new(SessionCore::new( - OpenAiRealtimeCodec, - host.map(metering), - Arc::new(crate::runtime::tools::EchoToolExecutor), - Carrier::with_downlink(dtx), - None, - )); - (core, drx) -} - -/// A downlink-facing core metered over a count-capped host (`None`: ungoverned). -fn core_with_downlink( - cap: Option, -) -> ( - Arc>, - UnboundedReceiver>, -) { - match cap { - Some(cap) => core_on(Some(&governed_host(Some(cap)))), - None => core_on(None), - } -} - -fn wire(json: serde_json::Value) -> WireEvent { - WireEvent(Bytes::from(serde_json::to_vec(&json).unwrap())) -} - -fn usage_frame(audio_out: u64) -> WireEvent { - wire(serde_json::json!({ - "type": "response.done", - "response": { "usage": { - "total_tokens": audio_out, - "output_token_details": { "audio_tokens": audio_out }, - }}, - })) -} - -fn audio_delta(b64: &str) -> WireEvent { - wire(serde_json::json!({ "type": "response.output_audio.delta", "delta": b64 })) -} - -/// `ms` milliseconds of PCM16 uplink audio (24 kHz mono, 48 bytes a millisecond) as the client sends it. -fn uplink_audio(ms: usize) -> WireEvent { - let pcm = Bytes::from(vec![0u8; ms * 48]); - wire(serde_json::json!({ - "type": "input_audio_buffer.append", - "audio": busbar_contract::media::base64_encode(&pcm), - })) -} - -/// The upstream opening one tool call. -fn call_opened(call_id: &str) -> WireEvent { - wire(serde_json::json!({"type":"response.output_item.added", - "item":{"type":"function_call","call_id":call_id,"name":"lookup"}})) -} - -/// The rows `host` holds for the caller, as `(class, count)` under [`LANE`]. -fn rows(host: &FixtureHost) -> BTreeMap { - host.ledger_rows(&caller().id) - .into_iter() - .map(|((lane, class), n)| { - assert_eq!(lane, LANE, "every count is ledgered under the voice lane"); - (class, n) - }) - .collect() -} - -fn row_set(pairs: &[(&str, u64)]) -> BTreeMap { - pairs.iter().map(|(c, n)| ((*c).to_string(), *n)).collect() -} - -// ── metering: the plane's counts, the kernel's verdict ────────────────────────────────────────── - -/// Q21b EXIT TEST — a served voice session writes the streaming plane's count rows. One turn: a -/// second of uplink audio, one tool call opened, a usage report naming all five token classes. The -/// ledger holds exactly the plane's SIX billable classes, under the voice lane, at the counts the -/// turn carried — never a price, and never the llm plane's reserved token keys. -/// -/// #71 EXIT TEST (money-model integrity, P2-voicefix): the upstream's `cached_tokens` figure is a -/// SUBSET of `input_token_details.audio_tokens`/`text_tokens`, not a class beside them, so it must -/// NOT produce a ledger row — a streams card that priced it would double-bill the same cached input. -/// This turn reports `cached_tokens: 1` and the ledger holds no `cached_tokens` row for it (before -/// this fix it did, at count 1, RED against this exact assertion). -#[tokio::test] -async fn a_served_turn_ledgers_the_planes_counts_per_class() { - let host = governed_host(None); - let (core, _drx) = core_on(Some(&host)); - let _ = core.on_client_frame(uplink_audio(1_000)); - let _ = core.on_server_frame(call_opened("call_1")).await; - let done = wire(serde_json::json!({ - "type": "response.done", - "response": { "usage": { - "input_token_details": { "audio_tokens": 10, "text_tokens": 3, "cached_tokens": 1 }, - "output_token_details": { "audio_tokens": 20, "text_tokens": 4 }, - }}, - })); - let plan = core.on_server_frame(done).await; - assert!(!plan.close, "an uncapped chain never closes the carrier"); - assert_eq!( - rows(&host), - row_set(&[ - ("audio_seconds_in", 1), - ("audio_tokens_in", 10), - ("audio_tokens_out", 20), - ("text_tokens_in", 3), - ("text_tokens_out", 4), - ("tool_calls", 1), - ]), - "no cached_tokens row: it is a subset of audio_tokens_in/text_tokens_in, not a class beside \ - them (architect ruling #71) — pricing it too would double-bill the same cached input", - ); - assert!( - !rows(&host).contains_key("cached_tokens"), - "cached_tokens must never reach the ledger as a billable class (#71)", - ); - // The turn's counters closed with it: the next report carries only what the next turn did. - let _ = core.on_server_frame(usage_frame(2)).await; - assert_eq!(rows(&host)["audio_tokens_out"], 22); - assert_eq!(rows(&host)["audio_seconds_in"], 1); - assert_eq!(rows(&host)["tool_calls"], 1); -} - -/// A turn that ends on an upstream error, or with the session, was served all the same: the audio -/// the caller spoke and the tool calls the upstream opened reach the ledger either way. -#[tokio::test] -async fn an_errored_or_abandoned_turn_still_ledgers_what_it_served() { - let host = governed_host(None); - let (core, _drx) = core_on(Some(&host)); - let _ = core.on_client_frame(uplink_audio(1_500)); - let _ = core.on_server_frame(call_opened("call_1")).await; - let error = wire(serde_json::json!({ - "type": "error", - "error": { "type": "server_error", "code": "boom", "message": "upstream failed" }, - })); - let _ = core.on_server_frame(error).await; - assert_eq!( - rows(&host), - row_set(&[("audio_seconds_in", 2), ("tool_calls", 1)]), - "an error-ended turn ledgers its own counts (1.5 s rounds up to 2)" - ); - // 600 ms more: the session has spoken 2 100 ms, which converts once to 3 s (MONEY-AUDIT STR-2: - // the session is one unit; rounding each turn up on its own would also read 3 here, but 20 turns - // of 1 050 ms read 40 s that way instead of 21 s). - let _ = core.on_client_frame(uplink_audio(600)); - core.settle_open_turn(); - assert_eq!( - rows(&host)["audio_seconds_in"], - 3, - "the teardown settles the open turn" - ); - core.settle_open_turn(); - assert_eq!(rows(&host)["audio_seconds_in"], 3, "and settles it once"); -} - -/// THE HARD CLOSE (the marquee guarantee), now the kernel's count-based check: the turn that dries -/// the caller's chain is delivered and ledgered, and the verdict cancels the in-flight response and -/// closes the carrier. Replaces `settle_past_cap_hard_closes_the_carrier` (the D2 lease). -#[tokio::test] -async fn a_turn_that_dries_the_chain_hard_closes_the_carrier() { - // A chain of 5 counts; each usage frame ledgers 3. - let (core, mut drx) = core_with_downlink(Some(5)); - - let plan = core.on_server_frame(usage_frame(3)).await; - assert!(!plan.close, "3 of 5: no hard close"); - assert!(!core.carrier().is_closed()); - - let plan = core.on_server_frame(usage_frame(3)).await; - assert!(plan.close, "6 of 5: the frame plan demands a hard close"); - assert!( - plan.upstream - .iter() - .any(|w| String::from_utf8_lossy(&w.0).contains("response.cancel")), - "a dry chain cancels the in-flight response upstream" - ); - assert!(core.carrier().is_closed(), "the carrier hard-closed"); - - let plan = core.on_server_frame(audio_delta("AAAA")).await; - assert!(plan.downlink.is_empty(), "closed carrier processes nothing"); - assert!( - !core.carrier().send_downlink(vec![1, 2, 3]), - "the carrier drops downlink after hard close" - ); - drx.close(); - assert!( - drx.next().await.is_none(), - "no downlink audio leaked to the client" - ); -} - -/// The chain a turn meets is the LIVE one: spend the key takes elsewhere between two turns counts, -/// which the lease's once-read cap never saw. -#[tokio::test] -async fn a_chain_dried_elsewhere_closes_the_session_at_its_next_turn() { - let host = governed_host(None); - let (core, _drx) = core_on(Some(&host)); - assert!(!core.on_server_frame(usage_frame(1)).await.close); - host.set_budget_chain(vec![busbar_contract::hooks::BudgetBucketState { - bucket_id: "group:g@day".to_string(), - budget_group: Some("g".to_string()), - pool: None, - spend_micros_at_current_rate: 10_000, - remaining_micros: Some(0), - window_start: 0, - budget_period: "day".to_string(), - }]); - assert!(core.on_server_frame(usage_frame(1)).await.close); -} - -/// Q21b EXIT TEST (the budget gate) — a key whose chain is already dry is refused at the open; one -/// with room, or with nothing capped, opens; an ungoverned host opens no account at all. -#[test] -fn a_dry_chain_refuses_the_open() { - let open = |host: Arc| { - TurnMeter::new(host, caller(), "streaming-server", crate::OPENAI_REALTIME) - .open("gpt-realtime") - .map(|m| m.is_some()) - }; - assert!( - open(governed_host(Some(0))).is_err(), - "a spent chain refuses" - ); - assert_eq!(open(governed_host(Some(1))), Ok(true), "room opens"); - assert_eq!(open(governed_host(None)), Ok(true), "uncapped opens"); - assert_eq!( - open(Arc::new(FixtureHost::new())), - Ok(false), - "an ungoverned host opens no account" - ); - // A pool-scoped bucket for another pool does not govern this one. - let other_pool = FixtureHost::new().governed().with_budget_chain(vec![ - busbar_contract::hooks::BudgetBucketState { - bucket_id: "group:g@day#other-pool".to_string(), - budget_group: Some("g".to_string()), - pool: Some("other-pool".to_string()), - spend_micros_at_current_rate: 1, - remaining_micros: Some(0), - window_start: 0, - budget_period: "day".to_string(), - }, - ]); - assert_eq!(open(Arc::new(other_pool)), Ok(true)); -} - -/// A turn on an ungoverned session ledgers nothing and is never closed on budget. -#[tokio::test] -async fn an_ungoverned_session_is_never_closed_on_budget() { - let (core, _drx) = core_with_downlink(None); - for _ in 0..3 { - assert!(!core.on_server_frame(usage_frame(u64::MAX / 4)).await.close); - } - let _ = TurnVerdict::Live; -} - -// ── pump lifecycle: frames both ways, close ends cleanly ──────────────────────────────────────── - -#[tokio::test] -async fn pump_relays_downlink_audio_and_close_ends_cleanly() { - let (core, mut drx) = core_with_downlink(None); - let session = Arc::new(VoiceSession::new(Arc::clone(&core))); - - // Feed two server audio frames then EOF (drop the sender). - let (in_tx, in_rx) = unbounded::>(); - let (out_tx, _out_rx) = unbounded::>(); - in_tx - .unbounded_send( - serde_json::to_vec(&serde_json::json!({ - "type": "response.output_audio.delta", "delta": "AAAA" - })) - .unwrap(), - ) - .unwrap(); - in_tx - .unbounded_send( - serde_json::to_vec(&serde_json::json!({ - "type": "response.output_audio.done", "item_id": "it1" - })) - .unwrap(), - ) - .unwrap(); - drop(in_tx); // EOF ends the pump cleanly. - - // serve_messages returns when the stream ends and the drain completes. - serve_messages(in_rx, out_tx, session).await; - - // The client received the two downlink frames, in order. - drx.close(); - let mut kinds = Vec::new(); - while let Some(f) = drx.next().await { - let v: serde_json::Value = serde_json::from_slice(&f).unwrap(); - kinds.push(v["type"].as_str().unwrap().to_string()); - } - assert_eq!( - kinds, - vec![ - "response.output_audio.delta".to_string(), - "response.output_audio.done".to_string() - ] - ); -} - -// ── tool-call correlation under interleaving ──────────────────────────────────────────────────── - -#[tokio::test] -async fn tool_calls_correlate_under_interleaving() { - let (core, _drx) = core_with_downlink(None); - - // Two calls, A (call_id "ca") and B (call_id "cb"), their OPEN/ARGS/CLOSE interleaved. - let frames = [ - wire( - serde_json::json!({"type":"response.output_item.added","item":{"type":"function_call","call_id":"ca","name":"alpha"}}), - ), - wire( - serde_json::json!({"type":"response.output_item.added","item":{"type":"function_call","call_id":"cb","name":"beta"}}), - ), - wire( - serde_json::json!({"type":"response.function_call_arguments.delta","call_id":"ca","delta":"{\"x\":1}"}), - ), - wire( - serde_json::json!({"type":"response.function_call_arguments.delta","call_id":"cb","delta":"{\"y\":2}"}), - ), - wire(serde_json::json!({"type":"response.function_call_arguments.done","call_id":"cb"})), - wire(serde_json::json!({"type":"response.function_call_arguments.done","call_id":"ca"})), - ]; - - let mut upstream = Vec::new(); - for f in frames { - let plan = core.on_server_frame(f).await; - upstream.extend(plan.upstream); - } - - // Each close produced a function_call_output correlating the RIGHT call_id to the RIGHT args, plus - // a response.create to continue. - let texts: Vec = upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect(); - let joined = texts.join("\n"); - assert!( - joined.contains("\"call_id\":\"cb\"") - && joined.contains("beta") - && joined.contains("{\\\"y\\\":2}"), - "call B's result correlates its own id + args: {joined}" - ); - assert!( - joined.contains("\"call_id\":\"ca\"") - && joined.contains("alpha") - && joined.contains("{\\\"x\\\":1}"), - "call A's result correlates its own id + args: {joined}" - ); - assert_eq!( - texts - .iter() - .filter(|t| t.contains("function_call_output")) - .count(), - 2, - "exactly two tool results, one per call" - ); - assert_eq!( - texts - .iter() - .filter(|t| t.contains("response.create")) - .count(), - 2, - "each result asks the model to continue" - ); -} - -// ── barge-in: speech_started cancels + truncates at the heard position ─────────────────────────── - -#[tokio::test] -async fn barge_in_cancels_and_truncates_at_heard_ms() { - let (core, _drx) = core_with_downlink(None); - // pcm16 default: 48 bytes/ms. Play 96 bytes of downlink audio = 2 ms heard. - // 96 raw bytes base64-encodes to 128 chars; use a 96-byte payload. - let payload = vec![0u8; 96]; - let b64 = busbar_contract::media::base64_encode(&Bytes::from(payload)); - let _ = core.on_server_frame(audio_delta(&b64)).await; - - let plan = core - .on_server_frame(wire(serde_json::json!({ - "type":"input_audio_buffer.speech_started","audio_start_ms":0,"item_id":"it7" - }))) - .await; - let joined: String = plan - .upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect::>() - .join("\n"); - assert!( - joined.contains("response.cancel"), - "barge-in cancels the response" - ); - assert!( - joined.contains("conversation.item.truncate") && joined.contains("\"audio_end_ms\":2"), - "truncate carries the 2 ms actually heard: {joined}" - ); -} - -// ── SessionScope: reattach + foreign-owner refusal ────────────────────────────────────────────── - -#[test] -fn session_scope_reattach_and_foreign_owner_refusal() { - let engine = Arc::new(DurableHandleEngine::new()); - let alice = SessionHandle::bind(Arc::clone(&engine), "alice", "call-1"); - alice.open(1).expect("alice opens her session"); - - // A per-turn checkpoint bumps the durable cursor. - assert_eq!(alice.bump_turn(2).expect("owner bump"), 1); - assert_eq!(alice.bump_turn(3).expect("owner bump"), 2); - - // REATTACH: a fresh binding for the SAME (owner, id) reads the live row through the scoped path. - let alice_again = SessionHandle::bind(Arc::clone(&engine), "alice", "call-1"); - assert_eq!( - alice_again.get().map(|r| r.turns), - Some(2), - "reattach sees the durable turns" - ); - - // FOREIGN OWNER: a session bound to the same id under a different owner is refused identically to a - // missing handle — cannot read, resume, or evict, and cannot even tell it exists. - let mallory = SessionHandle::bind(Arc::clone(&engine), "mallory", "call-1"); - assert!(mallory.get().is_none(), "foreign owner cannot read"); - assert!( - matches!( - mallory.scope_mutate_probe(), - Err(ScopedMutateError::NotYours) - ), - "foreign owner cannot resume/mutate" - ); - assert!(!mallory.close(), "foreign owner evicts nothing"); - - // The rightful owner drives terminal, then closes (owner-gated, terminal-only). - assert!(!alice.close(), "an active handle is not evicted"); - alice.settle_terminal(4).expect("owner settles terminal"); - assert!(alice.close(), "a terminal handle is evicted by its owner"); - assert!( - alice.get().is_none(), - "closed session is gone from the working set" - ); -} - -#[test] -fn session_scope_rtc_call_id_correlation_is_owner_gated_and_durable() { - let engine = Arc::new(DurableHandleEngine::new()); - let alice = SessionHandle::bind(Arc::clone(&engine), "alice", "call-1"); - alice.open(1).expect("alice opens her session"); - assert_eq!( - alice.get().and_then(|r| r.rtc_call_id), - None, - "no correlation key until the SDP broker sets it" - ); - - // The SDP broker stamps the provider's rtc_ preserved from the Location header. - alice - .set_rtc_call_id("rtc_abc123", 2) - .expect("owner stamps the correlation key"); - assert_eq!( - alice.get().and_then(|r| r.rtc_call_id).as_deref(), - Some("rtc_abc123"), - "the correlation key is durable on the row" - ); - - // It survives a subsequent turn checkpoint (the mutate clones the live row). - alice.bump_turn(3).expect("owner bump"); - let row = alice.get().expect("row present"); - assert_eq!(row.turns, 1); - assert_eq!( - row.rtc_call_id.as_deref(), - Some("rtc_abc123"), - "a turn bump preserves the correlation key" - ); - - // A foreign owner cannot stamp a correlation key — the same indistinguishable refusal. - let mallory = SessionHandle::bind(Arc::clone(&engine), "mallory", "call-1"); - assert!( - matches!( - mallory.set_rtc_call_id("rtc_evil", 4), - Err(ScopedMutateError::NotYours) - ), - "a foreign owner cannot forge the media correlation" - ); -} - -// A tiny probe used only by the foreign-owner test — proves the mutate path refuses NotYours without -// the test needing to reconstruct a full mutation. -impl SessionHandle { - fn scope_mutate_probe(&self) -> Result { - self.bump_turn(9) - } -} - -#[test] -fn read_denied_maps_to_notyours() { - // Belt-and-braces: the raw scoped read for a foreign owner is exactly HandleDenied::NotYours. - let engine = Arc::new(DurableHandleEngine::new()); - let alice = SessionHandle::bind(Arc::clone(&engine), "alice", "s"); - alice.open(1).unwrap(); - let raw: Result<_, HandleDenied> = SessionScopeRawProbe::probe(&engine); - assert!(matches!(raw, Err(HandleDenied::NotYours))); -} - -// Helper that reaches the raw scoped read to assert the exact read-denial variant. -struct SessionScopeRawProbe; -impl SessionScopeRawProbe { - fn probe( - engine: &Arc, - ) -> Result, HandleDenied> { - busbar_kernel::plane_host::SessionScope::new(Arc::clone(engine), "mallory", "s").get() - } -} - -// ── the governed wait: a client-served tool call ──────────────────────────────────────────────── -// -// The other half of the tool moat. A call for a tool this node does NOT serve cannot be answered -// in-process — the answer is the client's — so the runtime opens no execution for it and routes the -// client's reply to the node's own table instead. Three facts, and each of them failed before this: -// the reply woke the wait, a reply naming a call nobody is waiting on is refused rather than carried -// upstream, and a call nobody answered is swept by the tick. - -/// A tool executor that serves exactly one tool and leaves the rest to the client. -#[derive(Debug)] -struct ServesOnly(&'static str); - -#[async_trait::async_trait] -impl crate::runtime::ToolExecutor for ServesOnly { - fn executes_here(&self, name: &str) -> bool { - name == self.0 - } - async fn execute(&self, name: &str, _arguments: &[u8]) -> Vec { - format!(r#"{{"served":"{name}"}}"#).into_bytes() - } -} - -/// The node's table, as the runtime is allowed to see it: which identifiers are open (entered as the -/// runtime plans them), and a sweep. -#[derive(Debug, Default)] -struct TableFake { - sessions: std::sync::Mutex>>, -} - -impl crate::runtime::GovernedCalls for TableFake { - fn planned(&self, session: u64, call_id: &str, _now_ms: u64) -> bool { - let mut s = self.sessions.lock().unwrap(); - s.entry(session).or_default().push(call_id.to_string()); - true - } - - fn replied(&self, session: u64, call_id: &str) -> Result<(), crate::runtime::ReplyRefusal> { - let mut s = self.sessions.lock().unwrap(); - let open = s - .get_mut(&session) - .ok_or(crate::runtime::ReplyRefusal::NoSuchSession)?; - let i = open - .iter() - .position(|c| c == call_id) - .ok_or(crate::runtime::ReplyRefusal::UnknownCall)?; - open.remove(i); - Ok(()) - } - - fn expired(&self, _now_ms: u64) -> usize { - let mut s = self.sessions.lock().unwrap(); - s.values_mut().map(|open| open.drain(..).count()).sum() - } -} - -/// A core bound to `table` as session 7, serving only the tool `local`. -fn governed_core(table: Arc) -> Arc> { - let (dtx, _drx) = unbounded::>(); - Arc::new( - SessionCore::new( - OpenAiRealtimeCodec, - None, - Arc::new(ServesOnly("local")), - Carrier::with_downlink(dtx), - None, - ) - .with_governed(crate::runtime::GovernedSession { - session: 7, - calls: table as Arc, - }), - ) -} - -/// The three server→client frames that announce, stream and close one call for `tool`. -fn call_frames(call_id: &str, tool: &str) -> [WireEvent; 3] { - [ - wire(serde_json::json!({"type":"response.output_item.added", - "item":{"type":"function_call","call_id":call_id,"name":tool}})), - wire( - serde_json::json!({"type":"response.function_call_arguments.delta", - "call_id":call_id,"delta":"{}"}), - ), - wire(serde_json::json!({"type":"response.function_call_arguments.done","call_id":call_id})), - ] -} - -/// The client's own `function_call_output` for `call_id`. -fn client_reply(call_id: &str) -> WireEvent { - wire(serde_json::json!({"type":"conversation.item.create", - "item":{"type":"function_call_output","call_id":call_id,"output":"42"}})) -} - -#[tokio::test] -async fn a_client_served_call_waits_and_its_reply_wakes_the_unit() { - let table = Arc::new(TableFake::default()); - let core = governed_core(Arc::clone(&table)); - - // The node serves `local` and does not serve `remote`. Both calls close in the same turn. - let mut upstream = Vec::new(); - for f in call_frames("cl", "local") - .into_iter() - .chain(call_frames("cr", "remote")) - { - upstream.extend(core.on_server_frame(f).await.upstream); - } - let joined: String = upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect::>() - .join("\n"); - assert!( - joined.contains("\"call_id\":\"cl\""), - "the tool the node serves is still executed in-process: {joined}" - ); - assert!( - !joined.contains("\"call_id\":\"cr\""), - "the node never authors an answer for a call it does not serve: {joined}" - ); - - // The root entered the wait where it planned the leg. The runtime is what tells it the answer - // arrived — and the reply goes on upstream only because the wait was woken. - table - .sessions - .lock() - .unwrap() - .insert(7, vec!["cr".to_string()]); - let plan = core.on_client_frame(client_reply("cr")); - assert!( - !plan.refused_reply, - "the wait was open, so nothing is refused" - ); - let up: String = plan - .upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect::>() - .join("\n"); - assert!( - up.contains("function_call_output") && up.contains("response.create"), - "the woken reply reaches the model and asks it to continue: {up}" - ); - assert!( - table.sessions.lock().unwrap()[&7].is_empty(), - "the table no longer holds the call" - ); -} - -#[tokio::test] -async fn a_reply_naming_no_open_call_is_refused_rather_than_carried_upstream() { - let table = Arc::new(TableFake::default()); - let core = governed_core(Arc::clone(&table)); - for f in call_frames("cr", "remote") { - core.on_server_frame(f).await; - } - // The session's table holds `cr`; the client answers `cz`. - table - .sessions - .lock() - .unwrap() - .insert(7, vec!["cr".to_string()]); - let plan = core.on_client_frame(client_reply("cz")); - assert!(plan.refused_reply, "a reply matching nothing is refused"); - assert!( - plan.upstream.is_empty(), - "and a refused reply reaches the model on no wire at all" - ); - assert_eq!( - table.sessions.lock().unwrap()[&7], - vec!["cr".to_string()], - "the call it did not answer is still waiting" - ); -} - -#[tokio::test] -async fn an_unanswered_client_served_call_is_swept_by_the_tick() { - let table = Arc::new(TableFake::default()); - let core = governed_core(Arc::clone(&table)); - for f in call_frames("cr", "remote") { - core.on_server_frame(f).await; - } - table - .sessions - .lock() - .unwrap() - .insert(7, vec!["cr".to_string()]); - - // The tick is what runs beside the pump; without it the wait is never ended and the call's hold - // is held open by a client that simply never replied. - assert_eq!( - core.sweep_expired(1_000), - 1, - "the tick swept the one open call" - ); - // And a reply that turns up after the sweep is refused, not paid out against a settled call. - let plan = core.on_client_frame(client_reply("cr")); - assert!(plan.refused_reply, "a late reply answers nothing"); - assert!(plan.upstream.is_empty()); -} - -/// **The tick actually runs, and it ends with the pump.** -/// -/// `sweep_expired` is the sweep; this is whether anything on a served session ever calls it. A wait -/// nobody sweeps is a hold nobody settles, and a client that never replies sends no frame on which -/// anyone would notice — so time passing has to be the event, and it has to stop being one when the -/// conversation does. -/// Real time rather than a paused clock: pausing needs tokio's `test-util`, and shipping a test -/// feature into the runtime build to make one assertion cheaper is a worse trade than a slow test. -#[tokio::test] -async fn the_sweep_rides_the_pump_and_ends_with_it() { - let table = Arc::new(TableFake::default()); - let core = governed_core(Arc::clone(&table)); - table - .sessions - .lock() - .unwrap() - .insert(7, vec!["cr".to_string()]); - - // A pump that runs past one tick and then returns, exactly as a socket closing would. - // - // THE MARGIN IS THE POINT. `SWEEP_EVERY` is one second and this is real time, not a paused - // clock, so the pump has to outlive the tick by enough that a loaded CI runner's scheduler - // jitter cannot land the tick after the pump returns — 400 ms of headroom was not enough, and - // the failure would have read as "the sweep does not run" rather than "the box was busy". - let pump = tokio::time::sleep(std::time::Duration::from_millis(2_500)); - crate::runtime::serve_with_sweep(Arc::clone(&core), pump).await; - - assert!( - table.sessions.lock().unwrap()[&7].is_empty(), - "the tick beside the pump swept the call nobody answered" - ); - - // And once the pump is gone so is the tick: a fresh call opened afterwards is swept by nobody. - table - .sessions - .lock() - .unwrap() - .insert(7, vec!["cs".to_string()]); - tokio::time::sleep(std::time::Duration::from_millis(1_400)).await; - assert_eq!( - table.sessions.lock().unwrap()[&7], - vec!["cs".to_string()], - "a sweep that outlived its session would be the background loop this plane does not have" - ); -} - -/// A HARD CLOSE THAT LANDS WHILE THE SUPERVISOR IS ARRIVING AT THE GATE STILL WAKES IT. -/// -/// The topology's teardown `select!` parks one arm on `Carrier::closed()`, and that await is the only -/// thing that turns a dry budget into a torn-down session. The gate's wake reaches the waiters that -/// are registered when it fires and stores nothing for one that registers a moment later — so a close -/// racing the supervisor's arrival is the one ordering in which the marquee guarantee can be dropped -/// on the floor, and the session runs on to socket EOF instead. -/// -/// Driven as a race rather than a sequence, because the window is between two instructions and no -/// sequential ordering can enter it: each round starts a fresh carrier, hands it to a thread that -/// closes it immediately, and demands the await finish. A run that parks forever is the defect. -#[tokio::test(flavor = "multi_thread", worker_threads = 4)] -async fn a_close_racing_the_supervisors_arrival_still_wakes_it() { - for round in 0..2_000 { - let carrier = Carrier::sideband(); - let closer = carrier.clone(); - std::thread::spawn(move || { - closer.hard_close(); - }); - tokio::time::timeout(std::time::Duration::from_secs(5), carrier.closed()) - .await - .unwrap_or_else(|_| { - panic!( - "round {round}: the carrier hard-closed and the supervisor never woke — a \ - session whose budget is dry that nothing tears down" - ) - }); - } -} - -// ── Item 137: a voice session's durable row is settled, never leaked ACTIVE ───────────────────── - -/// The retention sweep never retires an idle ACTIVE session (`BUSBAR-1.6.0.md` THE DESIGN §1, "an -/// active work handle is never evicted"; ARCHITECT 2026-10-07 K2-H5 (ii)): another session's open -/// claims the sweep, and the idle session is still resident and ACTIVE after it. Its teardown -/// ([`SessionHandle::finish`]) is what ends it. -#[test] -fn an_idle_voice_session_stays_active_until_its_teardown() { - let engine = Arc::new(DurableHandleEngine::new()); - let idle = SessionHandle::bind(Arc::clone(&engine), "alice", "call-idle"); - idle.open(1).expect("the idle session opens"); - - // Three days on another session opens, which claims the retention sweep. - let next = SessionHandle::bind(Arc::clone(&engine), "bob", "call-next"); - next.open(1 + 3 * 86_400).expect("the next session opens"); - assert_eq!( - idle.get().map(|row| row.terminal), - Some(false), - "the sweep retired an idle ACTIVE session" - ); - - // Its own teardown settles and evicts it. - assert!(idle.finish(2 + 3 * 86_400), "the teardown ends the session"); - assert!( - idle.get().is_none(), - "the finished session leaves the working set" - ); -} - -/// A served session's pump returning settles its durable row terminal and evicts it. -#[tokio::test] -async fn serving_a_session_to_its_teardown_settles_and_evicts_its_row() { - let rt = VoiceRuntimeFixture::runtime(); - let (core, handle) = crate::topology::begin_session( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-served", - None, - Carrier::sideband(), - None, - 1, - ) - .expect("the session opens"); - crate::runtime::serve_to_teardown(core, handle, async {}, || 2).await; - assert!( - SessionHandle::bind(Arc::clone(&rt.engine), "acct-1", "call-served") - .get() - .is_none(), - "the served session's row is settled and evicted when its pump returns" - ); -} - -/// OWNER RULING Q21a — NO CONFIG, NO LIMIT. A runtime whose operator configured no -/// `streams.session_max_secs` binds no ceiling, so a session that has run far past the hour the -/// retired default cut at is still open. (The test clock is the `now` handed to the comparison.) -#[test] -fn an_unconfigured_session_is_never_cut_by_a_ceiling() { - let rt = VoiceRuntimeFixture::runtime(); - assert_eq!( - rt.session_max_secs, None, - "nothing configured, nothing bound" - ); - let (core, _handle) = crate::topology::begin_session( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-unbounded", - None, - Carrier::sideband(), - None, - 1, - ) - .expect("the session opens"); - let much_later = std::time::Instant::now() + std::time::Duration::from_secs(3_600 * 5); - assert!( - !core.enforce_ceiling(much_later), - "five hours in, an unconfigured session is not closed" - ); - assert!(!core.carrier().is_closed()); -} - -/// Q21a — A CONFIGURED ceiling closes the session at the limit, with the dialect's own error frame -/// under the named reason, and the turn it cut is settled with the session. -/// -/// The ceiling was once declared, defaulted, parsed and plumbed onto the runtime and then compared -/// with nothing; it is compared, and only when configured. -#[tokio::test] -async fn a_configured_ceiling_closes_the_session_and_settles_its_counts() { - let mut rt = VoiceRuntimeFixture::runtime(); - rt.session_max_secs = std::num::NonZeroU32::new(1); - let host = governed_host(None); - let (dtx, mut drx) = unbounded::>(); - let (core, handle) = crate::topology::begin_session( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-long", - None, - Carrier::with_downlink(dtx), - Some(TurnMeter::new( - Arc::clone(&host) as Arc, - caller(), - "streaming-server", - crate::OPENAI_REALTIME, - )), - 1, - ) - .expect("the session opens"); - let _ = core.on_client_frame(uplink_audio(2_000)); - // A pump that never ends on its own: only the ceiling can end this session. - let served = tokio::time::timeout( - std::time::Duration::from_secs(5), - crate::runtime::serve_to_teardown( - Arc::clone(&core), - handle, - futures::future::pending::<()>(), - || 2, - ), - ) - .await; - assert!( - served.is_ok(), - "a one-second ceiling ends the session well inside five seconds" - ); - assert!(core.carrier().is_closed(), "the ceiling closes the carrier"); - drx.close(); - let told: Vec = drx - .collect::>() - .await - .iter() - .map(|f| serde_json::from_slice(f).expect("a json frame")) - .collect(); - assert!( - told.iter().any(|v| v["type"] == "error" - && v["error"]["code"] == crate::runtime::session::SESSION_CEILING_REASON), - "the client is told why, in its dialect's own error frame: {told:?}" - ); - assert_eq!( - rows(&host), - row_set(&[("audio_seconds_in", 2)]), - "the turn the ceiling cut is settled with the session" - ); -} - -/// The runtime these three cells open sessions on. -struct VoiceRuntimeFixture; -impl VoiceRuntimeFixture { - fn runtime() -> crate::runtime::VoiceRuntime { - crate::runtime::VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(crate::runtime::tools::EchoToolExecutor), - ) - } -} diff --git a/crates/busbar-voice/src/runtime/voice_plane_unit_oracle.rs b/crates/busbar-voice/src/runtime/voice_plane_unit_oracle.rs deleted file mode 100644 index 4e7e7558a0..0000000000 --- a/crates/busbar-voice/src/runtime/voice_plane_unit_oracle.rs +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE VOICE PLANE-UNIT BILLING ORACLE — the regression backstop for the voice money path, replacing -//! the D2 lease oracle (`voice_d2_lease_billing_oracle`, deleted with the lease under OWNER RULING -//! Q21b; snapshot under `~/Downloads/busbar-1.6.0-snapshots/P2-voice/`). -//! -//! It drives the SAME fixed session script the lease oracle pinned — a refused open, then four turns -//! against a ceiling of 15 — through the path that now ships: each turn's raw counts per streaming- -//! plane class to the kernel's session account, which ledgers them and answers the verdict off the -//! caller's budget view. Every count ledgered and every verdict is pinned. The fixture host's view is -//! a count cap (each count worth one unit of the ceiling), the plane-unit twin of the lease oracle's -//! one-nano-per-unit mock, so the arithmetic stays legible; what a count is worth under a real -//! `streams` card is pinned over the engine in the composition root's tests. -//! -//! What moved, stated once: the lease folded the five token classes onto the llm plane's reserved -//! keys (so a turn's text and audio were one `output` figure) and stored a nanodollar sum; the plane -//! ledgers each class as itself, plus the two counts the lease never billed (`audio_seconds_in`, -//! `tool_calls`), and stores no figure. - -use crate::ir::usage::IrDuplexUsage; -use crate::runtime::metering::{TurnMeter, TurnVerdict}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_plane_streaming::session::TurnCounters; -use std::collections::BTreeMap; -use std::sync::Arc; - -const LANE: &str = "streaming\u{1f}gpt-realtime"; - -fn key() -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: "vk-oracle".to_string(), - ..Default::default() - } -} - -fn meter(host: &Arc) -> TurnMeter { - TurnMeter::new( - Arc::clone(host) as Arc, - key(), - "streaming-server", - crate::OPENAI_REALTIME, - ) -} - -fn rows(host: &FixtureHost) -> BTreeMap { - host.ledger_rows(&key().id) - .into_iter() - .map(|((lane, class), n)| { - assert_eq!(lane, LANE); - (class, n) - }) - .collect() -} - -fn pinned(pairs: &[(&str, u64)]) -> BTreeMap { - pairs.iter().map(|(c, n)| ((*c).to_string(), *n)).collect() -} - -/// THE ORACLE — one session's whole metering life, every count and verdict pinned. -#[test] -fn voice_plane_unit_billing_oracle() { - // ── LEG 0 — A DRY CHAIN IS REFUSED AT THE OPEN, AND NOTHING IS LEDGERED ───────────────────────── - let dry = Arc::new(FixtureHost::new().governed().with_count_cap(0)); - assert!( - meter(&dry).open("gpt-realtime").is_err(), - "a chain with nothing left refuses the open" - ); - assert!(rows(&dry).is_empty(), "a refused session ledgers nothing"); - - // ── THE SESSION — a ceiling of 15 ─────────────────────────────────────────────────────────────── - let host = Arc::new(FixtureHost::new().governed().with_count_cap(15)); - let session = meter(&host) - .open("gpt-realtime") - .expect("room opens") - .expect("governed"); - let no_counters = TurnCounters::default(); - - // ── LEG 1 — turn 1: audio_out 3, text_out 4 → 7 of 15, Live ───────────────────────────────────── - let v1 = session.report_turn( - Some(&IrDuplexUsage { - audio_out: 3, - text_out: 4, - ..IrDuplexUsage::default() - }), - no_counters, - ); - assert_eq!(v1, TurnVerdict::Live); - assert_eq!( - rows(&host), - pinned(&[("audio_tokens_out", 3), ("text_tokens_out", 4)]), - "each class ledgered as itself — not one folded `output` of 7" - ); - - // ── LEG 2 — turn 2: audio_out 2, text_in 3, plus a second of uplink audio and a tool call ─────── - let mut counters = TurnCounters::default(); - counters.admit_audio(crate::ir::media::AudioFormat::Pcm16, 48_000); - counters.open_tool_call(); - let v2 = session.report_turn( - Some(&IrDuplexUsage { - audio_out: 2, - text_in: 3, - ..IrDuplexUsage::default() - }), - counters, - ); - assert_eq!(v2, TurnVerdict::Live, "7 + 5 + 1 s + 1 call = 14 of 15"); - assert_eq!( - rows(&host), - pinned(&[ - ("audio_seconds_in", 1), - ("audio_tokens_out", 5), - ("text_tokens_in", 3), - ("text_tokens_out", 4), - ("tool_calls", 1), - ]), - "the two counts the lease never billed are ledgered beside the tokens" - ); - - // ── LEG 3 — turn 3: audio_out 5 → 19 of 15, MustClose (delivered, ledgered, then closed) ──────── - let v3 = session.report_turn( - Some(&IrDuplexUsage { - audio_out: 5, - ..IrDuplexUsage::default() - }), - no_counters, - ); - assert_eq!(v3, TurnVerdict::MustClose); - assert_eq!(rows(&host)["audio_tokens_out"], 10); - - // ── LEG 4 — once dry, stays dry: a late turn is still ledgered exactly and still closes ───────── - let late = session.report_turn( - Some(&IrDuplexUsage { - audio_out: 1, - ..IrDuplexUsage::default() - }), - no_counters, - ); - assert_eq!(late, TurnVerdict::MustClose); - assert_eq!(rows(&host)["audio_tokens_out"], 11); - assert_eq!( - host.ledger_usage(&key().id).map(|u| u.tokens), - Some(20), - "every count ledgered once: 7 + 7 + 5 + 1" - ); - - // ── LEG 5 — an empty turn ledgers nothing ─────────────────────────────────────────────────────── - let _ = session.report_turn(None, no_counters); - assert_eq!(host.ledger_usage(&key().id).map(|u| u.tokens), Some(20)); -} diff --git a/crates/busbar-voice/src/testkit/fixture_host.rs b/crates/busbar-voice/src/testkit/fixture_host.rs deleted file mode 100644 index 5ea449c9f7..0000000000 --- a/crates/busbar-voice/src/testkit/fixture_host.rs +++ /dev/null @@ -1,804 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE NEUTRAL FIXTURE HOST — an in-memory [`EngineHost`] a plane crate's tests drive INSTEAD of -//! minting a host over the engine's `App`. A plane is a plugin on the plane ABI, and its tests must -//! not reach into core any more than its production code does; before this fixture a plane test that -//! needed a host had to build `busbar_kernel::test_support::TestApp` and call -//! `busbar_kernel::plane_host::engine_host(&app)` — the exact backwards reach the purity lint forbids. -//! -//! This host models, in memory, just the host-side state a plane's production path drives through the -//! seam and a test then reads back: -//! -//! * the `(pool, lane)` BREAKER cells: the kernel's own breaker store (`breaker_store`), which the -//! kernel's breaker functions (`plane_host::breaker::{admit, record_*, retry_after_secs}`) drive -//! exactly as they drive a deployment's, readable through [`FixtureHost::breaker_state`]; -//! * the OPERATOR HOOK gate / rewrite chains keyed by `(plane_key, container)`, attached as scripted -//! verdicts ([`FixtureHost::attach_gate`] / [`FixtureHost::attach_rewrite`]): a gate is filed as a -//! resolved policy on the hooks seam (`plane_gates_of`), so the kernel's own -//! `admission_gates_attached` / `admission_gates_decide` fire it exactly as they fire a configured -//! deployment's, and a rewrite is filed as a resolved rewrite chain (`plane_rewrites_of`), so the -//! kernel's own `admission_tap_attached` / `admission_transform` run it; -//! * the per-key usage LEDGER the metering seams land on (`meter_ledger` / `meter_series`), readable -//! through [`FixtureHost::ledger_usage`], per `(lane, class)` through [`FixtureHost::ledger_rows`], -//! and per series row through [`FixtureHost::series_rows`] once the host is -//! [`governed`](FixtureHost::governed); -//! * the key's BUDGET VIEW (`budget_state`): a chain a test sets outright -//! ([`FixtureHost::with_budget_chain`] / [`FixtureHost::set_budget_chain`]), or a count cap -//! ([`FixtureHost::with_count_cap`]) whose remaining is the cap less every count ledgered — so the -//! kernel's session account, which reads this view after each turn, can be driven dry. -//! -//! The fixture implements ONLY the plane-facing seam: it names no cost or price type (#43 — a plane is -//! pricing-blind, and so is the host its tests stand in). What a count is WORTH is the kernel's -//! read-time view, proven over the real engine in the composition root's tests. -//! -//! Everything else on the seam answers the neutral "nothing configured" value (no pools, no secrets, -//! no identity chain). It is a test double: a leg the fixture does not model -//! answers its documented empty value rather than pretending to be the engine. - -use busbar_contract::abi::hot::{AdmissionId, Signal}; -use busbar_contract::hooks::TransformOutcome; -use busbar_contract::records::{PlaneRequestCtx, VirtualKey}; -use busbar_kernel::billing::{TokenUsage, Usage}; -use busbar_kernel::hooks::{RequestedSignals, ResolvedPolicy, TapEntry}; -use busbar_kernel::plane_host::{ - AdmissionHost, AdmitHandle, BreakerHost, BudgetHost, ClockHost, DispatchScope, EngineHost, - GateOutcome, GovAdmit, GovHandle, HookConfigHost, IdentityHost, JournalHost, LanePoolHost, - MeterPin, MountHost, RegistryHost, TelemetryHost, -}; -use busbar_kernel::store::{BreakerState, HealthState, LaneRuntime, PlaneBreakers}; -use std::collections::BTreeMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; - -/// A scripted gate verdict for one `(plane_key, container)`: handed the content the kernel's gate -/// projected for the hook (the prompt projection's message texts, newline-joined), answers the -/// [`GateOutcome`] the real hook would. A `Reject` reaches the kernel gate as the hook's reject reply -/// (the gate clamps its status); a `Proceed` is the hook abstaining. -pub type GateScript = Arc GateOutcome + Send + Sync>; - -/// A [`GateScript`] filed as the hook policy a resolved gate carries, so the kernel's gate decision -/// drives it through the same `decide` call it makes on a configured hook. -struct ScriptedGate(GateScript); - -#[async_trait::async_trait] -impl busbar_contract::hooks::RoutingPolicy for ScriptedGate { - async fn decide( - &self, - req: &busbar_contract::hooks::RoutingRequest<'_>, - _candidates: &[busbar_contract::hooks::Candidate<'_>], - _ctx: &busbar_contract::hooks::RoutingContext<'_>, - _budget: std::time::Duration, - ) -> busbar_contract::hooks::PolicyResult { - Ok(match (self.0)(prompt_content(req).as_bytes()) { - GateOutcome::Proceed => busbar_contract::hooks::RoutingDecision::Abstain, - GateOutcome::Reject { - status, message, .. - } => busbar_contract::hooks::RoutingDecision::Reject { status, message }, - }) - } - - fn name(&self) -> &'static str { - "fixture-gate" - } -} - -/// A scripted rewrite reply for one `(plane_key, container)`: handed the content the kernel's transform -/// projected for the hook (the prompt projection's message texts, newline-joined — the serialized -/// payload), answers the [`TransformOutcome`] the real `prompt: rw` hook would (a rewrite reply, an -/// abstain, or a reject). The kernel's transform applies it exactly as it applies a configured hook's. -pub type RewriteScript = Arc TransformOutcome + Send + Sync>; - -/// A [`RewriteScript`] filed as the hook a resolved rewrite chain carries, so the kernel's transform -/// drives it through the same `transform` call it makes on a configured hook. -struct ScriptedRewrite(RewriteScript); - -#[async_trait::async_trait] -impl busbar_contract::hooks::RoutingPolicy for ScriptedRewrite { - async fn decide( - &self, - _req: &busbar_contract::hooks::RoutingRequest<'_>, - _candidates: &[busbar_contract::hooks::Candidate<'_>], - _ctx: &busbar_contract::hooks::RoutingContext<'_>, - _budget: std::time::Duration, - ) -> busbar_contract::hooks::PolicyResult { - Ok(busbar_contract::hooks::RoutingDecision::Abstain) - } - - async fn transform( - &self, - req: &busbar_contract::hooks::RoutingRequest<'_>, - _budget: std::time::Duration, - ) -> TransformOutcome { - (self.0)(prompt_content(req).as_bytes()) - } - - fn name(&self) -> &'static str { - "fixture-rewrite" - } -} - -/// The content a hook is shown: the prompt projection's message texts, newline-joined. -fn prompt_content(req: &busbar_contract::hooks::RoutingRequest<'_>) -> String { - req.prompt - .as_ref() - .map(|p| { - p.messages - .iter() - .map(|(_, text)| text.as_ref()) - .collect::>() - .join("\n") - }) - .unwrap_or_default() -} - -/// The usage a key has ledgered through the fixture's metering seams — the read-back twin of the -/// engine's `usage_for(key)`: `tokens` is every unit `meter_ledger` accrued, `requests` the billable -/// request count the ADMIT step's per-request fee increments (never the Meter step). -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct LedgerUsage { - pub tokens: u64, - pub requests: u64, - /// Sessions the kernel counted as opened, on the plane's own fee lane (#47 `fees.per_session`) — - /// kept apart from the turn counts above and from [`FixtureHost::ledger_rows`]. - pub sessions: u64, -} - -/// One admin-audit row [`JournalHost::audit_record`] landed on the fixture — the read-back a plane's -/// exit-path/mutation test asserts against (action literal, resource, outcome, principal), the fixture -/// twin of the engine's in-process admin audit ring. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct FixtureAuditEntry { - pub action: String, - pub resource: String, - pub outcome: String, - pub principal: String, -} - -#[derive(Default)] -struct Inner { - gates: BTreeMap<(String, String), GateScript>, - rewrites: BTreeMap<(String, String), RewriteScript>, - ledger: BTreeMap, - slots: BTreeMap>, - audit: Vec, - /// The budget chain `budget_state` answers for every key — empty (uncapped) unless a test sets one. - budget: Vec, - /// A count cap `budget_state` answers as one bucket, remaining the cap less the key's counts. - count_cap: Option, - /// Every count `meter_ledger` landed, per key, per `(lane, class)`. - rows: BTreeMap>, - /// Every session count `meter_ledger` landed on a plane's fee lane, per key: the METERING ROW's - /// session count, which a fee refund never touches. - session_rows: BTreeMap, - /// Every series row `meter_series` wrote, per key, as `(model, provider)`, in order. - series: BTreeMap>, - /// Every request a plane reported through `request_finished`, in order. - finished: Vec, -} - -/// One request a plane reported through [`TelemetryHost::request_finished`] — the read-back twin of the -/// engine's plane-labelled request counter and duration sample. -#[derive(Clone, Debug, PartialEq)] -pub struct FinishedRequest { - pub plane: String, - pub ingress_protocol: String, - pub pool: String, - pub outcome: &'static str, - pub seconds: f64, -} - -/// The in-memory engine host a plane's tests drive through the neutral seam. Build one with -/// [`FixtureHost::new`], configure it with the builder methods, then hand it to the plane as an -/// `Arc` (an `Arc` coerces) while keeping your own `Arc` to read the -/// state back. -pub struct FixtureHost { - inner: Mutex, - governed: bool, - next_request_id: AtomicU64, - /// The lane store `lane_store` hands out: the kernel's OWN `LaneRuntime` implementor with no - /// lanes configured, so this double never re-implements (and never drifts from) that trait. - lanes: HealthState, - /// The breaker store `breaker_store` hands out: the kernel's OWN provisioned `PlaneBreakers`, so - /// the kernel's breaker functions run the real cell FSM against it rather than a model of it. - breakers: Arc, - /// No hook on the fixture requests a candidate signal (the all-zero mask). - signals: RequestedSignals, -} - -impl Default for FixtureHost { - fn default() -> Self { - Self::new() - } -} - -impl FixtureHost { - /// A bare host: no governance, no hooks attached, every breaker cell Closed. - #[must_use] - pub fn new() -> Self { - FixtureHost { - inner: Mutex::new(Inner::default()), - governed: false, - next_request_id: AtomicU64::new(1), - lanes: HealthState::new(Vec::new()), - breakers: Arc::new(PlaneBreakers::provisioned()), - signals: RequestedSignals::default(), - } - } - - /// Answer `chain` as every key's budget chain. - #[must_use] - pub fn with_budget_chain(self, chain: Vec) -> Self { - self.set_budget_chain(chain); - self - } - - /// Replace the budget chain every key reads, mid-test — the view a live session's next turn meets. - pub fn set_budget_chain(&self, chain: Vec) { - self.lock().budget = chain; - } - - /// Answer every key's budget view as ONE bucket capped at `cap` counts: its remaining is the cap - /// less every count the key has ledgered. A stand-in for a card that makes each count worth one - /// unit, without this host naming a card. - #[must_use] - pub fn with_count_cap(self, cap: i64) -> Self { - self.lock().count_cap = Some(cap); - self - } - - /// Turn governance ON: `governance()` mints a handle and the metering seams land on the fixture's - /// per-key ledger (read back with [`Self::ledger_usage`]). Off, `governance()` is `None` and the - /// metering seams are never reached — exactly an ungoverned deployment. - #[must_use] - pub fn governed(mut self) -> Self { - self.governed = true; - self - } - - /// Attach a scripted request-admission GATE to `container` on plane `plane_key`, filed on the hooks - /// seam (`plane_gates_of`) so `admission_gates_attached` answers true and `admission_gates_decide` - /// runs `script` through the kernel's gate decision. - #[must_use] - pub fn attach_gate(self, plane_key: &str, container: &str, script: GateScript) -> Self { - self.lock() - .gates - .insert((plane_key.to_string(), container.to_string()), script); - self - } - - /// Attach a scripted `prompt: rw` REWRITE to `container` on plane `plane_key`, filed on the hooks - /// seam (`plane_rewrites_of`) so `admission_tap_attached` answers true and `admission_transform` - /// runs `script` through the kernel's transform. - #[must_use] - pub fn attach_rewrite(self, plane_key: &str, container: &str, script: RewriteScript) -> Self { - self.lock() - .rewrites - .insert((plane_key.to_string(), container.to_string()), script); - self - } - - /// Finish the builder as the `Arc` a plane's production path takes. - #[must_use] - pub fn into_host(self) -> Arc { - Arc::new(self) - } - - /// The `(pool, lane)` breaker cell's current state — Closed until something is recorded. - #[must_use] - pub fn breaker_state(&self, pool: &str, lane: usize) -> BreakerState { - self.breakers.state_at(pool, lane) - } - - /// What `key_id` has ledgered through the metering seams, or `None` if nothing ever landed. - #[must_use] - pub fn ledger_usage(&self, key_id: &str) -> Option { - self.lock().ledger.get(key_id).copied() - } - - /// Every session count the kernel's account landed for `key_id` on the plane's fee lane: the - /// METERING ROW's session count. A fee refund gives a count back on the budget book - /// ([`LedgerUsage::sessions`]) and leaves this row in place, as v1.5.5's fee refund left its row - /// (TODO 17(b)): a failed open reads one here and none there. - #[must_use] - pub fn session_rows(&self, key_id: &str) -> u64 { - self.lock().session_rows.get(key_id).copied().unwrap_or(0) - } - - /// Every request reported through `request_finished`, in order. - #[must_use] - pub fn finished_requests(&self) -> Vec { - self.lock().finished.clone() - } - - /// Every count `key_id` has ledgered, per `(lane, class)` — the rows a plane's metering wrote. - #[must_use] - pub fn ledger_rows(&self, key_id: &str) -> BTreeMap<(String, String), u64> { - self.lock().rows.get(key_id).cloned().unwrap_or_default() - } - - /// Every series row `key_id`'s metering wrote through `meter_series`, as `(model, provider)`, in - /// order — the rows whose `provider` column decides which plane `GET /admin/usage` prices them as. - #[must_use] - pub fn series_rows(&self, key_id: &str) -> Vec<(String, String)> { - self.lock().series.get(key_id).cloned().unwrap_or_default() - } - - /// Every admin-audit row [`JournalHost::audit_record`] has landed on this host, in emission order — - /// the read-back a plane's audit/exit-path test asserts an exact count and shape against. - #[must_use] - pub fn audit_log(&self) -> Vec { - self.lock().audit.clone() - } - - fn lock(&self) -> std::sync::MutexGuard<'_, Inner> { - self.inner.lock().unwrap_or_else(|e| e.into_inner()) - } - - /// The ADMIT step's per-request FEE, landed on the presenting key's ledger row — the write side of - /// [`LedgerUsage::requests`]. Governance off, or a request that carries no resolved key, is charged - /// nothing (the ungoverned posture: there is no key to bill). Called once per admission decision, - /// so a plane that runs the door twice for one request reads back two fees, exactly as a - /// double-charging deployment would. - fn charge_request_fee(&self, gov: &PlaneRequestCtx) { - if !self.governed { - return; - } - let Some(key) = gov.key() else { return }; - let mut inner = self.lock(); - let entry = inner.ledger.entry(key.id.clone()).or_default(); - entry.requests = entry.requests.saturating_add(1); - } - - /// The host wall clock, read through the contract's host service - /// (`busbar_contract::codec::wall_clock_now`) — the one clock a plane reads. This fixture stands - /// in for the host, so it arms that service with its own system-clock reader first (first install - /// wins: a process whose real host already armed it keeps the host's clock). - fn now() -> u64 { - busbar_contract::codec::install_wall_clock(system_clock_secs); - busbar_contract::codec::wall_clock_now().unwrap_or_else(system_clock_secs) - } -} - -/// The fixture host's system clock: whole seconds since the Unix epoch — the reading the real host -/// installs into the contract's wall-clock service. -fn system_clock_secs() -> u64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() -} - -// ── The breaker slice: the kernel's own breaker store ───────────────────────────────────────────── - -impl BreakerHost for FixtureHost { - fn breaker_store(&self) -> &Arc { - &self.breakers - } - - fn breaker_settle( - &self, - scope: &DispatchScope, - admission: AdmissionId, - signal: &Signal, - ) -> busbar_contract::abi::hot::StatusClass { - scope - .settle_admission(admission, signal) - .unwrap_or(signal.class.class()) - } -} - -// ── The lane/pool slice: no pools configured ───────────────────────────────────────────────────── - -impl LanePoolHost for FixtureHost { - fn lane_store(&self) -> &dyn LaneRuntime { - &self.lanes - } - fn default_probe_interval_secs(&self) -> u64 { - 30 - } - fn default_probe_timeout_secs(&self) -> u64 { - 5 - } - fn plane_pool_members(&self, _plane_key: &str, _member: &str) -> Option<(String, Vec)> { - None - } -} - -impl ClockHost for FixtureHost { - fn clock_now_secs(&self) -> u64 { - Self::now() - } - /// The same host wall clock in milliseconds. The contract's service reads whole seconds, so this - /// is that reading scaled; no voice seam reads this fixture's millisecond clock. - fn clock_now_ms(&self) -> u64 { - Self::now().saturating_mul(1000) - } -} - -// ── Telemetry / journal: the fixture records nothing (the plane's own emits are what tests read) ── - -impl TelemetryHost for FixtureHost { - fn request_finished( - &self, - plane: &str, - ingress_protocol: &str, - pool: &str, - outcome: &'static str, - seconds: f64, - ) { - self.lock().finished.push(FinishedRequest { - plane: plane.to_string(), - ingress_protocol: ingress_protocol.to_string(), - pool: pool.to_string(), - outcome, - seconds, - }); - } - fn telemetry_upstream_attempt(&self, _pool_label: &str, _lane: usize) {} - fn telemetry_upstream_failure(&self, _pool_label: &str, _lane: usize, _d: &'static str) {} - fn telemetry_breaker_trip(&self, _pool_label: &str, _lane: usize) {} - fn telemetry_failover(&self, _pool_label: &str, _reason: &'static str) {} - fn telemetry_translation(&self, _from: &str, _to: &str) {} - fn pool_label<'a>(&self, _model: &'a str) -> &'a str { - "unresolved" - } -} - -impl JournalHost for FixtureHost { - fn audit_emit(&self, _action: &str, _resource: &str, _outcome: &str, _principal: &str) {} - fn audit_record(&self, action: &str, resource: &str, outcome: &'static str, principal: &str) { - // Recorded (not a no-op): this is the ONE JournalHost leg a plane's exit-path test needs read - // back — every other JournalHost leg stays a no-op fixture default (nothing today reads them). - self.lock().audit.push(FixtureAuditEntry { - action: action.to_string(), - resource: resource.to_string(), - outcome: outcome.to_string(), - principal: principal.to_string(), - }); - } -} - -impl MountHost for FixtureHost { - fn arrival_envelope_dialect(&self, _path: &str) -> &'static str { - "" - } - fn arrival_fallback_error( - &self, - _path: &str, - status: axum::http::StatusCode, - kind: &str, - message: &str, - ) -> axum::response::Response { - axum::response::Response::builder() - .status(status) - .header(axum::http::header::CONTENT_TYPE, "application/json") - .body(axum::body::Body::from( - serde_json::json!({ "error": { "type": kind, "message": message } }).to_string(), - )) - .expect("a static response builds") - } -} - -/// A resolver with no secrets behind it: every reference fails closed. -struct NoSecrets; - -impl busbar_contract::secret::SecretResolve for NoSecrets { - fn resolve(&self, _secret: &busbar_contract::secret_ref::SecretRef) -> Result, String> { - Err("the fixture host resolves no secrets".to_string()) - } - fn resolve_string( - &self, - _secret: &busbar_contract::secret_ref::SecretRef, - ) -> Result { - Err("the fixture host resolves no secrets".to_string()) - } -} - -impl RegistryHost for FixtureHost { - fn next_request_id(&self) -> u64 { - self.next_request_id.fetch_add(1, Ordering::Relaxed) - } - fn plane_slot(&self, key: &str) -> Option> { - self.lock().slots.get(key).cloned() - } - fn secret_resolver(&self) -> Arc { - Arc::new(NoSecrets) - } - fn subkey_sign(&self, _signing_input: &[u8]) -> Option<[u8; 64]> { - None - } - fn plane_defs(&self) -> Arc { - Arc::new(()) - } -} - -impl HookConfigHost for FixtureHost { - fn caller_in_hook_groups(&self, _caller_group: Option<&str>, _hook_groups: &[String]) -> bool { - false - } - fn pool_rewrites( - &self, - _pool: &str, - ) -> &[( - std::time::Duration, - Arc, - )] { - &[] - } - fn rewrite_hooks( - &self, - ) -> &[( - std::time::Duration, - Arc, - )] { - &[] - } - fn any_content_hook(&self) -> bool { - false - } - fn tap_hooks(&self) -> &[TapEntry] { - &[] - } - fn tap_hooks_response(&self) -> &[TapEntry] { - &[] - } - fn tap_hooks_routing(&self) -> &[TapEntry] { - &[] - } - fn tap_hooks_candidate(&self) -> &[TapEntry] { - &[] - } - fn plane_gates_of(&self, plane_key: &str, container: &str) -> Vec<(u16, ResolvedPolicy)> { - let script = self - .lock() - .gates - .get(&(plane_key.to_string(), container.to_string())) - .cloned(); - script - .map(|s| { - vec![( - 0, - ResolvedPolicy::Policy { - policy: Arc::new(ScriptedGate(s)), - on_error: busbar_kernel::config::PolicyOnError::Reject, - on_error_chain: Vec::new(), - timeout: std::time::Duration::from_secs(10), - send_prompt: true, - send_user: false, - on_empty: busbar_kernel::config::PolicyOnError::Reject, - }, - )] - }) - .unwrap_or_default() - } - fn plane_rewrites_of( - &self, - plane_key: &str, - container: &str, - ) -> Vec<( - std::time::Duration, - Arc, - )> { - let script = self - .lock() - .rewrites - .get(&(plane_key.to_string(), container.to_string())) - .cloned(); - script - .map(|s| { - vec![( - std::time::Duration::from_secs(10), - Arc::new(ScriptedRewrite(s)) as Arc, - )] - }) - .unwrap_or_default() - } - fn pool_gates(&self, _pool: &str) -> &[(u16, ResolvedPolicy)] { - &[] - } - fn global_gates(&self) -> &[(u16, ResolvedPolicy)] { - &[] - } - fn pool_policy(&self, _pool: &str) -> Option<&ResolvedPolicy> { - None - } - fn requested_signals(&self) -> &RequestedSignals { - &self.signals - } -} - -// ── The budget slice: the per-key ledger the metering seams land on ───────────────────────────── - -impl BudgetHost for FixtureHost { - fn governance_enabled(&self) -> bool { - self.governed - } - fn meter_charge( - &self, - _scope: &DispatchScope, - _caller: &PlaneRequestCtx, - _usage: &busbar_contract::abi::hot::Usage, - ) { - } - fn rate_headroom( - &self, - _pin: &MeterPin, - _key: &VirtualKey, - _pool: Option<&str>, - _now: u64, - ) -> Option { - None - } - fn budget_state( - &self, - _pin: &MeterPin, - key: &VirtualKey, - _now: u64, - ) -> Vec { - let inner = self.lock(); - let Some(cap) = inner.count_cap else { - return inner.budget.clone(); - }; - let counted = inner.ledger.get(&key.id).map_or(0, |u| u.tokens); - let counted = i64::try_from(counted).unwrap_or(i64::MAX); - vec![busbar_contract::hooks::BudgetBucketState { - bucket_id: key.id.clone(), - budget_group: None, - pool: None, - spend_micros_at_current_rate: counted, - remaining_micros: Some(cap.saturating_sub(counted).max(0)), - window_start: 0, - budget_period: "day".to_string(), - }] - } - fn governance(&self) -> Option { - self.governed.then(|| GovHandle(Arc::new(()))) - } - // No card here, so nothing a card could miss. - fn cost_model_unpriced(&self, _model: &str) -> bool { - false - } - // The kernel's session-fee refund, on the plane's fee lane: one session count back on the budget - // book, floored; the metering row keeps its count (`session_rows`), as the engine's refund does. - fn meter_refund_fee( - &self, - _pin: &MeterPin, - key: &VirtualKey, - _pool: &str, - _plane: &str, - _fee_unit: &str, - _now: u64, - ) { - let mut inner = self.lock(); - if let Some(entry) = inner.ledger.get_mut(&key.id) { - entry.sessions = entry.sessions.saturating_sub(1); - } - } - fn meter_ledger( - &self, - _pin: &MeterPin, - key: &VirtualKey, - _pool: &str, - lane: &str, - usage: &Usage, - _now: u64, - ) { - if lane.ends_with(busbar_kernel::governance::PLANE_LANE_SEP) { - // The kernel's session count on the plane's fee lane: not a turn's count. - let n = usage.usage_units.values().sum::(); - let mut inner = self.lock(); - let entry = inner.ledger.entry(key.id.clone()).or_default(); - entry.sessions = entry.sessions.saturating_add(n); - let row = inner.session_rows.entry(key.id.clone()).or_default(); - *row = row.saturating_add(n); - return; - } - let tokens: u64 = usage.usage_units.values().sum(); - if tokens == 0 { - return; - } - let mut inner = self.lock(); - let entry = inner.ledger.entry(key.id.clone()).or_default(); - entry.tokens = entry.tokens.saturating_add(tokens); - let rows = inner.rows.entry(key.id.clone()).or_default(); - for (class, n) in &usage.usage_units { - let row = rows.entry((lane.to_string(), class.clone())).or_default(); - *row = row.saturating_add(*n); - } - } - fn meter_series( - &self, - _gov: &GovHandle, - key_id: &str, - model: &str, - provider: &str, - _usage: Option<&TokenUsage>, - _now: u64, - ) { - self.lock() - .series - .entry(key_id.to_string()) - .or_default() - .push((model.to_string(), provider.to_string())); - } -} - -impl IdentityHost for FixtureHost {} - -// ── The admission slice ──────────────────────────────── - -impl AdmissionHost for FixtureHost { - fn govern_admit_reason( - &self, - _scope: &DispatchScope, - _caller: &PlaneRequestCtx, - _pool: &[u8], - ) -> GovAdmit { - GovAdmit::Admitted - } - fn destination_guard( - &self, - _gov: &PlaneRequestCtx, - _proto: &'static str, - _pool: &str, - _started: std::time::Instant, - _charged_at: u64, - ) -> Result<(), Box> { - Ok(()) - } - fn admission_door( - &self, - gov: &PlaneRequestCtx, - _proto: &'static str, - _pool: &str, - _started: std::time::Instant, - _charged_at: u64, - ) -> Result<(Option, Option), Box> { - self.charge_request_fee(gov); - Ok((None, None)) - } - fn admission_check( - &self, - gov: &PlaneRequestCtx, - _proto: &'static str, - _pool: &str, - _charged_at: u64, - ) -> Result<(Option, Option), Box> { - self.charge_request_fee(gov); - Ok((None, None)) - } - fn finish_admitted( - &self, - _gov: &PlaneRequestCtx, - _ingress_protocol: &str, - _pool: &str, - _started: std::time::Instant, - _charged_at: u64, - resp: axum::response::Response, - _charged: Option<&AdmitHandle>, - ) -> axum::response::Response { - resp - } - fn finish_rejected( - &self, - _gov: &PlaneRequestCtx, - _ingress_protocol: &str, - _pool: &str, - _started: std::time::Instant, - _charged_at: u64, - resp: axum::response::Response, - ) -> axum::response::Response { - resp - } - fn plane_audience_bound(&self, _plane_key: &str) -> bool { - false - } -} - -impl EngineHost for FixtureHost {} - -#[cfg(test)] -#[path = "tests/fixture_host_tests.rs"] -mod tests; diff --git a/crates/busbar-voice/src/testkit/loopback_http.rs b/crates/busbar-voice/src/testkit/loopback_http.rs deleted file mode 100644 index b0047ac4fb..0000000000 --- a/crates/busbar-voice/src/testkit/loopback_http.rs +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! A LOOPBACK HTTP "PROVIDER" for plane tests: a real listener on `127.0.0.1` that answers the -//! queued replies and RECORDS what it was dialed with (path, headers, body), so a plane's egress leg -//! is exercised over the production request path and the test asserts on the bytes the upstream -//! actually saw. The neutral twin of the mock server core's own test support carries, so a plane crate -//! drives a loopback provider without naming `busbar_kernel::test_support`. - -use axum::{ - body::Body, - extract::State, - http::{header, Request, Response, StatusCode}, - routing::any, - Router, -}; -use std::net::SocketAddr; -use std::sync::{Arc, Mutex}; -use tokio::task::JoinHandle; - -/// One queued reply the loopback provider serves. -#[derive(Debug, Clone)] -pub enum MockResponse { - /// A JSON body under `status`. - Ok { - status: StatusCode, - body: serde_json::Value, - }, -} - -impl Default for MockResponse { - fn default() -> Self { - MockResponse::Ok { - status: StatusCode::OK, - body: serde_json::json!({ "ok": true }), - } - } -} - -/// The reply queue plus the last request the provider received. -#[derive(Debug, Default)] -pub struct MockServerState { - queued_replies: Mutex>, - last_auth_header: Mutex>, - last_request_body: Mutex>>, - last_request_headers: Mutex>, - last_request_path: Mutex>, -} - -impl MockServerState { - pub fn new() -> Self { - Self::default() - } - - /// Queue a reply (served last-queued-first, matching the core fixture). - pub fn push(&self, response: MockResponse) { - self.queued_replies.lock().unwrap().push(response); - } - - pub fn next_response(&self) -> Option { - self.queued_replies.lock().unwrap().pop() - } - - /// The `Authorization` header the provider was last dialed with. - pub fn get_last_auth_header(&self) -> Option { - self.last_auth_header.lock().unwrap().clone() - } - - /// The request path the provider last received. - pub fn get_last_request_path(&self) -> Option { - self.last_request_path.lock().unwrap().clone() - } - - /// The raw body bytes the provider last received. - pub fn get_last_request_body(&self) -> Option> { - self.last_request_body.lock().unwrap().clone() - } - - /// One request header the provider last received, by name (case-insensitive). - pub fn get_last_request_header(&self, name: &str) -> Option { - self.last_request_headers - .lock() - .unwrap() - .as_ref() - .and_then(|h| h.get(name)) - .and_then(|v| v.to_str().ok()) - .map(str::to_owned) - } -} - -/// The running loopback provider; `shutdown` signals its serve task to wind down cooperatively. -pub struct MockServer { - addr: SocketAddr, - handle: Option>, - stop: Option>, -} - -impl MockServer { - /// Bind an ephemeral loopback port and serve every path through `state`'s reply queue. - pub async fn new(state: Arc) -> Self { - let app = Router::new().fallback(any(mock_handler)).with_state(state); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - // Cooperative shutdown rather than `JoinHandle::abort`: `with_graceful_shutdown` is a signal - // `axum::serve` races inside its own accept loop, never a forced mid-flight interruption. - let (stop_tx, stop_rx) = tokio::sync::oneshot::channel::<()>(); - let handle = tokio::spawn(async move { - axum::serve(listener, app) - .with_graceful_shutdown(async move { - let _ = stop_rx.await; - }) - .await - .unwrap(); - }); - Self { - addr, - handle: Some(handle), - stop: Some(stop_tx), - } - } - - pub fn address(&self) -> SocketAddr { - self.addr - } - - pub fn base_url(&self) -> String { - format!("http://{}", self.addr) - } - - pub async fn shutdown(mut self) { - if let Some(stop) = self.stop.take() { - let _ = stop.send(()); - } - if let Some(handle) = self.handle.take() { - let _ = handle.await; - } - } -} - -async fn mock_handler( - State(state): State>, - request: Request, -) -> Response { - let (parts, body) = request.into_parts(); - *state.last_request_path.lock().unwrap() = Some(parts.uri.path().to_string()); - *state.last_request_headers.lock().unwrap() = Some(parts.headers.clone()); - if let Some(auth) = parts - .headers - .get(header::AUTHORIZATION) - .and_then(|v| v.to_str().ok()) - { - *state.last_auth_header.lock().unwrap() = Some(auth.to_string()); - } - let body_bytes = axum::body::to_bytes(body, usize::MAX) - .await - .unwrap_or_default(); - *state.last_request_body.lock().unwrap() = Some(body_bytes.to_vec()); - - match state.next_response().unwrap_or_default() { - MockResponse::Ok { status, body } => Response::builder() - .status(status) - .header(header::CONTENT_TYPE, "application/json") - .body(Body::from(body.to_string())) - .unwrap(), - } -} diff --git a/crates/busbar-voice/src/testkit/metrics_capture.rs b/crates/busbar-voice/src/testkit/metrics_capture.rs deleted file mode 100644 index 341e06b7cb..0000000000 --- a/crates/busbar-voice/src/testkit/metrics_capture.rs +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! AN IN-MEMORY `metrics` RECORDER for plane tests: installs once as the process-global recorder the -//! `metrics::counter!` macros emit into, and renders what landed as Prometheus-style exposition lines -//! (`name{label="value",...} count`), so a plane asserts its emits on a real scrape without naming -//! core's exporter. In a plane crate's own test binary nothing else installs a recorder, so the -//! capture IS the process recorder. - -use metrics::{ - Counter, CounterFn, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit, -}; -use std::collections::BTreeMap; -use std::sync::{Arc, Mutex, OnceLock}; - -#[derive(Default)] -struct Store { - counters: Mutex>, -} - -static STORE: OnceLock> = OnceLock::new(); - -fn store() -> &'static Arc { - STORE.get_or_init(|| Arc::new(Store::default())) -} - -/// The rendered series identity: the metric name plus its labels in the order they were emitted. -fn series_of(key: &Key) -> String { - let mut labels = key.labels().peekable(); - if labels.peek().is_none() { - return key.name().to_string(); - } - let rendered: Vec = labels - .map(|l| format!("{}=\"{}\"", l.key(), l.value())) - .collect(); - format!("{}{{{}}}", key.name(), rendered.join(",")) -} - -struct CounterCell { - store: Arc, - series: String, -} - -impl CounterFn for CounterCell { - fn increment(&self, value: u64) { - let mut counters = self.store.counters.lock().unwrap(); - *counters.entry(self.series.clone()).or_insert(0) += value; - } - fn absolute(&self, value: u64) { - let mut counters = self.store.counters.lock().unwrap(); - counters.insert(self.series.clone(), value); - } -} - -struct CaptureRecorder(Arc); - -impl Recorder for CaptureRecorder { - fn describe_counter(&self, _key: KeyName, _unit: Option, _description: SharedString) {} - fn describe_gauge(&self, _key: KeyName, _unit: Option, _description: SharedString) {} - fn describe_histogram(&self, _key: KeyName, _unit: Option, _description: SharedString) {} - fn register_counter(&self, key: &Key, _metadata: &Metadata<'_>) -> Counter { - Counter::from_arc(Arc::new(CounterCell { - store: Arc::clone(&self.0), - series: series_of(key), - })) - } - fn register_gauge(&self, _key: &Key, _metadata: &Metadata<'_>) -> Gauge { - Gauge::noop() - } - fn register_histogram(&self, _key: &Key, _metadata: &Metadata<'_>) -> Histogram { - Histogram::noop() - } -} - -/// Install the capture as the process-global recorder. Idempotent: a second call (or one after another -/// recorder already took the slot) is a no-op, and [`render`] reads whatever this capture holds. -pub fn install() { - let _ = metrics::set_global_recorder(CaptureRecorder(Arc::clone(store()))); -} - -/// Every counter series that landed since [`install`], one `name{labels} value` line each, sorted. -pub fn render() -> String { - let counters = store().counters.lock().unwrap(); - let mut out = String::new(); - for (series, value) in counters.iter() { - out.push_str(series); - out.push(' '); - out.push_str(&value.to_string()); - out.push('\n'); - } - out -} diff --git a/crates/busbar-voice/src/testkit/mod.rs b/crates/busbar-voice/src/testkit/mod.rs deleted file mode 100644 index 0a6c36fe7e..0000000000 --- a/crates/busbar-voice/src/testkit/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE VOICE PLANE'S TEST-KIT — the fixture surface this plane's own batteries drive, kept ON THE -//! PLANE. A plane is a plugin on the plane ABI and it tests ITSELF: the kernel ships no scaffolding -//! for it (1.6.0 Locked Decision #33, "the kernel never tests a plugin"), so the three doubles the -//! voice cells reach — the in-memory [`fixture_host::FixtureHost`], the -//! [`loopback_http`] provider and the [`metrics_capture`] recorder — live here rather than in -//! `busbar_kernel::testkit`, which is deleted. -//! -//! Every double reaches the engine through the SAME neutral seam production uses -//! (`busbar_kernel::plane_host::EngineHost` and friends); nothing here names a kernel *test* item. - -/// The in-memory [`busbar_kernel::plane_host::EngineHost`] this plane's cells drive when no engine -/// `App` is in their closure at all: scripted hook gates/rewrites, breaker cells and a per-key ledger -/// behind the same seam production reaches. -pub mod fixture_host; - -/// A loopback HTTP provider that records what it was dialed with, for this plane's egress legs. -pub mod loopback_http; - -/// An in-memory `metrics` recorder + exposition render, for asserting this plane's counter emits. -pub mod metrics_capture; - -/// THIS PLANE'S SERVED-LEG WITNESSES — `(capability key, [(loop step or core capability, witness)])`. -/// -/// What a test binary that links this crate without naming it runs through ITS registered session -/// runner for this plane's key (the composition root's kernel-loop session rider): each witness opens -/// a session at the served door, asserts one capability or one loop step on the session it opened, -/// and returns how many session opens it expects to have reached that runner. See -/// `tests/served_witness.rs`. -pub const SERVED: (&str, &[(&str, crate::mount::served_witness::Witness)]) = - (crate::PLANE_KEY, crate::mount::served_witness::WITNESSES); diff --git a/crates/busbar-voice/src/testkit/tests/fixture_host_tests.rs b/crates/busbar-voice/src/testkit/tests/fixture_host_tests.rs deleted file mode 100644 index 3209e75a47..0000000000 --- a/crates/busbar-voice/src/testkit/tests/fixture_host_tests.rs +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE FIXTURE'S OWN LEDGER PROOF — the fixture host is what a plane's money-path tests read their -//! verdict off, so its `requests` counter must MOVE when the Admit door is driven. Before these -//! tests the counter was write-never: every `ledger_usage(key).requests` read back 0 whether the -//! plane charged once, twice, or not at all, so a double-charging plane passed its own billing test. -//! Two admissions must read back two fees; an ungoverned host, and a request presenting no key, must -//! read back nothing at all. - -use super::{FixtureHost, LedgerUsage}; -use busbar_contract::records::{PlaneRequestCtx, VirtualKey}; -use busbar_kernel::plane_host::AdmissionHost; -use std::sync::Arc; - -const PROTO: &str = "test-proto"; - -fn ctx(key_id: &str) -> PlaneRequestCtx { - PlaneRequestCtx { - key: Some(Arc::new(VirtualKey { - id: key_id.to_string(), - name: key_id.to_string(), - ..Default::default() - })), - } -} - -fn requests_for(host: &FixtureHost, key_id: &str) -> u64 { - host.ledger_usage(key_id).map_or(0, |u| u.requests) -} - -#[test] -fn two_admissions_charge_two_request_fees() { - let host = FixtureHost::new().governed(); - let gov = ctx("vk-payer"); - let started = std::time::Instant::now(); - - assert_eq!( - requests_for(&host, "vk-payer"), - 0, - "a key that has not been admitted owes nothing" - ); - - host.admission_door(&gov, PROTO, "pool", started, 0) - .expect("the fixture door admits"); - assert_eq!( - requests_for(&host, "vk-payer"), - 1, - "one admission is one billable request" - ); - - host.admission_check(&gov, PROTO, "pool", 0) - .expect("the fixture check admits"); - assert_eq!( - requests_for(&host, "vk-payer"), - 2, - "a second admission is a SECOND billable request — a plane that runs the door twice for one \ - request reads back two fees here" - ); - - // The fee is the only thing Admit moves: the token ledger is the Meter step's to write. - assert_eq!( - host.ledger_usage("vk-payer"), - Some(LedgerUsage { - tokens: 0, - requests: 2, - sessions: 0, - }), - "the Admit step charges the per-request fee and no tokens" - ); -} - -#[test] -fn the_fee_lands_on_the_presenting_key_only() { - let host = FixtureHost::new().governed(); - let started = std::time::Instant::now(); - - host.admission_door(&ctx("vk-a"), PROTO, "pool", started, 0) - .expect("the fixture door admits"); - host.admission_door(&ctx("vk-b"), PROTO, "pool", started, 0) - .expect("the fixture door admits"); - host.admission_door(&ctx("vk-b"), PROTO, "pool", started, 0) - .expect("the fixture door admits"); - - assert_eq!(requests_for(&host, "vk-a"), 1, "vk-a presented once"); - assert_eq!(requests_for(&host, "vk-b"), 2, "vk-b presented twice"); -} - -#[test] -fn an_ungoverned_host_and_a_keyless_request_are_charged_nothing() { - // Governance off: there is no ledger to bill, exactly as an ungoverned deployment. - let ungoverned = FixtureHost::new(); - ungoverned - .admission_door( - &ctx("vk-payer"), - PROTO, - "pool", - std::time::Instant::now(), - 0, - ) - .expect("the fixture door admits"); - assert_eq!( - ungoverned.ledger_usage("vk-payer"), - None, - "an ungoverned host materialises no ledger row" - ); - - // Governed, but the request carries no resolved key: nobody to charge. - let governed = FixtureHost::new().governed(); - governed - .admission_check(&PlaneRequestCtx::default(), PROTO, "pool", 0) - .expect("the fixture check admits"); - assert!( - governed.ledger_usage("vk-payer").is_none(), - "a keyless request charges no key" - ); -} diff --git a/crates/busbar-voice/src/tests/audit_chain_tests.rs b/crates/busbar-voice/src/tests/audit_chain_tests.rs deleted file mode 100644 index 5cc599e003..0000000000 --- a/crates/busbar-voice/src/tests/audit_chain_tests.rs +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! AUDIT-CHAIN capability cells (voice-client + voice-server) — GATE-VALID location. -//! -//! The proving assertions already live and pass in `runtime/tests.rs` -//! (`session_scope_reattach_and_foreign_owner_refusal`), but that path clears neither the equality -//! gate's `/tests/` directory rule nor its `_tests.rs` suffix rule. These are the SAME assertions, -//! re-homed under `src/tests/` so the capability-equality gate's location rule is met. They drive a -//! durable session open → seal a genesis `SealedEvent`/`tail_hash` → bump the per-turn cursor → -//! reattach, reading the durable row back (a non-empty read is the pass; an empty read is a failure). -//! Runtime-gated so the durable engine compiles. - -use crate::runtime::scope::SessionHandle; -use busbar_kernel::plane::handle_engine::{DurableHandleEngine, ScopedMutateError}; -use std::sync::Arc; - -/// voice-client cell: an opened session seals a genesis event on the durable chain and survives a -/// reattach — a fresh binding for the same (owner, id) reads the live turns back through the scoped -/// path, and a foreign owner is refused indistinguishably from a missing handle. -#[test] -fn an_opened_voice_session_seals_a_genesis_event_and_survives_reattach_on_the_durable_chain() { - let engine = Arc::new(DurableHandleEngine::new()); - let alice = SessionHandle::bind(Arc::clone(&engine), "alice", "call-1"); - // OPEN seals the genesis event onto the durable chain. - alice.open(1).expect("alice opens her session"); - - // A per-turn checkpoint bumps the durable cursor onto the genesis chain. - assert_eq!(alice.bump_turn(2).expect("owner bump"), 1); - assert_eq!(alice.bump_turn(3).expect("owner bump"), 2); - - // REATTACH: a fresh binding for the SAME (owner, id) reads the live row through the scoped path. - let alice_again = SessionHandle::bind(Arc::clone(&engine), "alice", "call-1"); - assert_eq!( - alice_again.get().map(|r| r.turns), - Some(2), - "reattach sees the durable turns — a non-empty read of the sealed chain" - ); - - // FOREIGN OWNER: a session bound to the same id under a different owner is refused identically to - // a missing handle — cannot read, resume, or evict, and cannot even tell it exists. - let mallory = SessionHandle::bind(Arc::clone(&engine), "mallory", "call-1"); - assert!( - mallory.get().is_none(), - "foreign owner cannot read the chain" - ); - assert!( - matches!(mallory.bump_turn(9), Err(ScopedMutateError::NotYours)), - "foreign owner cannot resume/mutate the chain" - ); - assert!(!mallory.close(), "foreign owner evicts nothing"); -} - -/// voice-server cell: an inbound session-open (the front door) lands a sealed genesis event — the -/// durable read-back is non-empty. An empty read is a FAILURE, not a pass. -#[test] -fn an_inbound_session_open_lands_a_sealed_genesis_event_the_front_door_wrote() { - let engine = Arc::new(DurableHandleEngine::new()); - // The inbound front door opens a governed session under the presenting caller. - let front_door = SessionHandle::bind(Arc::clone(&engine), "caller", "inbound-1"); - front_door - .open(1) - .expect("the front door opens the session"); - - // The genesis event the front door wrote is readable back on the durable chain (non-empty). - let row = front_door - .get() - .expect("the sealed genesis event reads back — a non-empty durable chain"); - assert_eq!(row.turns, 0, "a fresh genesis row carries no turns yet"); - - // A checkpoint the front door drives bumps onto the same sealed chain. - assert_eq!(front_door.bump_turn(2).expect("front-door bump"), 1); - assert_eq!( - front_door.get().map(|r| r.turns), - Some(1), - "the front-door checkpoint is durable on the sealed chain" - ); -} diff --git a/crates/busbar-voice/src/tests/billing_ledger_tests.rs b/crates/busbar-voice/src/tests/billing_ledger_tests.rs deleted file mode 100644 index 7a60da9abf..0000000000 --- a/crates/busbar-voice/src/tests/billing_ledger_tests.rs +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE BEHAVIORAL BILLING PROOF for the voice plane — the ledger-level twin of the LLM plane's -//! crossproto_delivery_billing oracle. The `plane_meter_seam_reachability` gate proves voice *calls* -//! the core Meter seam; THIS test proves the spend actually *lands on the one ledger*, attributed to -//! the presenting key, exactly as marketing states: "the token counts land on the ledger when the -//! response stream completes." -//! -//! It drives a voice turn's usage through the SHIPPED path — `SessionCore`'s per-turn metering, which -//! reports the turn to the kernel's session account (`host.meter_ledger`) — over a GOVERNED host, then -//! reads the key's usage back off the host's ledger. A voice session that "bills nobody" (the pre-fix -//! state) reads back zero tokens; a fixed one reads the turn. -//! -//! The host is the substrate's in-memory fixture host with governance ON: its `meter_ledger` seam is -//! the one ledger this test reads back (`ledger_usage(key)`), keyed by the presenting key exactly as -//! the engine's `usage_for(key)` is. Whether the engine's own ledger accrues what its seam is handed -//! is the engine's money-path suite to prove; this plane's tests do not link the engine. - -use crate::ir::usage::IrDuplexUsage; -use crate::runtime::metering::TurnMeter; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::{ - config, - config_validate::validate, - cost::CostModel, - governance::PLANE_LANE_SEP, - plane::registry::{PlaneDecl, TestRegistryIsolation}, - plane_host::EngineHost, -}; -use busbar_plane_streaming::session::TurnCounters; -use std::sync::Arc; - -/// A governed fixture: a host with governance ON and a virtual key presenting to it — so the host -/// `meter_ledger` seam the voice session drives writes to the very ledger this test reads back. -fn governed_fixture() -> (Arc, busbar_contract::records::VirtualKey) { - let host = Arc::new(FixtureHost::new().governed()); - let key = busbar_contract::records::VirtualKey { - id: "vk-voice-caller".to_string(), - name: "voice-caller".to_string(), - ..Default::default() - }; - (host, key) -} - -/// A voice turn's usage report: `input` audio tokens in, `output` audio tokens out. -fn turn_usage(input: u64, output: u64) -> IrDuplexUsage { - IrDuplexUsage { - audio_in: input, - audio_out: output, - ..IrDuplexUsage::default() - } -} - -#[test] -fn a_voice_turn_lands_spend_on_the_presenting_keys_ledger() { - let (host, key) = governed_fixture(); - - // Before: the key has metered nothing. - let before = host - .ledger_usage(&key.id) - .map(|u| (u.tokens, u.requests)) - .unwrap_or((0, 0)); - assert_eq!(before, (0, 0), "a fresh key has no ledgered usage"); - - // Drive ONE voice turn's usage through the SHIPPED Meter seam — the exact call `SessionCore` - // makes per turn (the kernel session account → `host.meter_ledger`). - let meter = TurnMeter::new( - Arc::clone(&host) as Arc, - key.clone(), - "streaming-server", - crate::OPENAI_REALTIME, - ); - let session = meter - .open("voice-model") - .expect("an uncapped chain opens") - .expect("a governed host opens an account"); - let _ = session.report_turn(Some(&turn_usage(300, 120)), TurnCounters::default()); - - // After: the turn's tokens (300 + 120) landed on the presenting key's ledger — voice now bills - // the caller through the ONE ledger, exactly like a model call or a tool call. THIS is the proof - // that the Meter step works end-to-end at the ledger, not just at the wiring. - let after = host - .ledger_usage(&key.id) - .expect("the key now has a materialised bucket"); - assert_eq!( - after.tokens, 420, - "the voice turn's 420 tokens must land on the one ledger (input 300 + output 120)" - ); - // `requests` is the billable-REQUEST count, incremented by the ADMIT step's per-request fee - // (`govern_admit`), NOT by the Meter step. Voice does not yet run Admit's fee charge — that is the - // tracked next increment — so this reads 0 today. Pinned so that when Admit lands for voice, this - // assertion flips RED and this test is the reminder to update it (and confirm the fee is charged). - assert_eq!( - after.requests, 0, - "voice does not yet run the Admit per-request fee (tracked increment); flip this when it does" - ); -} - -#[test] -fn an_ungoverned_voice_turn_meters_nobody_without_panicking() { - // No governance configured: the host mints no meter pin, so no account opens (a voice session on - // an ungoverned deployment opens and runs, it simply attributes to no ledger). - let host = FixtureHost::new().into_host(); - let key = busbar_contract::records::VirtualKey { - id: "anon".to_string(), - ..Default::default() - }; - let meter = TurnMeter::new(host, key, "streaming-server", crate::OPENAI_REALTIME); - // Must not panic even though there is no ledger to write to. - assert!(matches!(meter.open("voice-model"), Ok(None))); - let _ = turn_usage(10, 5); -} - -/// **EVERY ROW A VOICE SESSION'S METERING WRITES IS THE STREAMS PLANE'S** (#47, OWNER RULING Q32). -/// -/// `GET /admin/usage` knows a metering row as a plane's row by its `provider` column naming a -/// registered plane key; any other row is a pools row, priced at the pools plane's flat -/// `per_request_fee:` per request. Each turn used to write a series row with the UPSTREAM dialect -/// label (`openai_realtime`) as its provider — a pools row — so, with a flat fee of 5, a three-turn -/// session read 15 on `/admin/usage` while the budget book (which charges a turn no per-request fee) -/// read 0. The turn's counts now reach the metering row only through the kernel's one accrual -/// (`meter_ledger`), on the plane-qualified lane the kernel keys by this plane: `/admin/usage` reads -/// 0, as the book does, and the session's own fee (`streams.fees.per_session`) is the fee lane's. -#[test] -fn a_voice_sessions_metering_writes_no_row_keyed_by_the_upstream_provider() { - let (host, key) = governed_fixture(); - let meter = TurnMeter::new( - Arc::clone(&host) as Arc, - key.clone(), - "streaming-server", - crate::OPENAI_REALTIME, - ); - let session = meter - .open("gpt-realtime") - .expect("an uncapped chain opens") - .expect("a governed host opens an account"); - for _ in 0..3 { - let _ = session.report_turn(Some(&turn_usage(30, 4)), TurnCounters::default()); - } - - assert_eq!( - host.series_rows(&key.id), - Vec::<(String, String)>::new(), - "a turn is not a request: no per-turn series row (one keyed by the upstream provider \ - reads as a pools row, charged the flat per_request_fee per turn on /admin/usage)" - ); - let plane_lane = format!("{}{}", crate::PLANE_KEY, PLANE_LANE_SEP); - let rows = host.ledger_rows(&key.id); - assert!( - rows.keys().all(|(lane, _)| lane.starts_with(&plane_lane)), - "every count lands on a lane qualified by this plane's key: {rows:?}" - ); - assert_eq!( - rows.get(&( - format!("{plane_lane}gpt-realtime"), - "audio_tokens_in".to_string() - )), - Some(&90), - "the three turns' counts, on the plane-qualified model lane" - ); -} - -/// The node's resolved config from `yaml` (beside empty providers/models), under whatever plane -/// declarations the registry holds. -fn resolved(yaml: &str) -> config::RootCfg { - let text = format!("store: {{module: memory}}\nproviders: {{}}\nmodels: {{}}\n{yaml}"); - let deploy = config::deploy_from_yaml_str(&text).expect("the config parses"); - config::resolve(&deploy, &Default::default()).expect("it resolves") -} - -/// **THE STREAMING PLANE COUNTS `per_session` ONLY** (ARCHITECT ruling, fees): a turn never passes -/// per-request admission, so `streams.fees.per_request` would charge nothing — boot and `--validate` -/// refuse it, naming the key and the counted list. `streams.fees.per_session: 40` boots, and a -/// session opened over the plane's shipped metering counts one session on the plane's fee lane, -/// which the card the config resolves to prices at 40. -#[test] -fn streams_fees_per_request_refuses_and_per_session_boots_and_charges() { - let _reg = TestRegistryIsolation::seeded(&[&PLANE_ROW]); - assert_eq!( - validate(&resolved("streams:\n fees: { per_request: 2 }\n")), - Err(vec![ - "streams.fees.per_request is not counted by this plane (counted: per_session); \ - remove it" - .to_string() - ]) - ); - let root = resolved("streams:\n fees: { per_session: 40 }\n"); - assert_eq!(validate(&root), Ok(()), "a counted fee boots"); - - let (host, key) = governed_fixture(); - let meter = TurnMeter::new( - Arc::clone(&host) as Arc, - key.clone(), - "streaming-server", - crate::OPENAI_REALTIME, - ); - let _session = meter - .open("gpt-realtime") - .expect("opens") - .expect("governed"); - let sessions = host.ledger_usage(&key.id).expect("a ledger").sessions; - assert_eq!( - sessions, 1, - "one opened session, one count on the plane's fee lane" - ); - let fee_lane = format!("{}{PLANE_LANE_SEP}", crate::PLANE_KEY); - let price = |fees: &config::PlaneFeesMap| { - let cost = CostModel::resolve_parts(None, 0, &Default::default()).with_plane_fees(fees); - let usage = busbar_contract::billing::Usage { - usage_units: [("per_session".to_string(), sessions)].into(), - }; - cost.price_usage_nanos(&fee_lane, &usage) - .expect("the fee lane prices") - }; - let one = resolved("streams:\n fees: { per_session: 1 }\n").plane_fees; - assert!(price(&one) > 0); - assert_eq!( - price(&root.plane_fees), - 40 * price(&one), - "the session is charged 40" - ); -} - -/// This plane's registry row, assembled kernel-side from its contract declaration -/// and its behaviour table. -static PLANE_ROW: PlaneDecl = PlaneDecl::assemble(crate::PLANE_DECLARATION, crate::PLANE_HOOKS); diff --git a/crates/busbar-voice/src/tests/breaker_tests.rs b/crates/busbar-voice/src/tests/breaker_tests.rs deleted file mode 100644 index 1157b1ca02..0000000000 --- a/crates/busbar-voice/src/tests/breaker_tests.rs +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! KILL-THE-PROVIDER, the voice-client dial leg — the `breaker-trip` / `breaker-fastfail` cells, -//! DIRECT-DRIVEN. No live serving path calls [`dial_provider`] yet, so these prove the cells by -//! driving the wired dial path directly against the substrate's in-memory fixture host, whose -//! `(pool, lane)` breaker cells sit behind the same `breaker_admit` / `breaker_record_*` seam the -//! production dispatch admits/records through (and are read back off the same host). -//! -//! 1. TRIP: a provider dial busbar's own net-guard refuses is a DEFINITIVE (hard-down) failure, so it -//! records into the ONE host cell and OPENS it on the first failure. -//! 2. FAST-FAIL: with that cell OPEN, a fresh dial is refused at admission BEFORE any socket — the -//! fast-fail leg, with the cell's own cooldown as `Retry-After`. -//! -//! RED before the wiring: `dial_provider` never touched the breaker at all, so a dead provider left -//! no cell to trip and a fresh dial waited out the full dial timeout against a target already known -//! down. - -use crate::testkit::fixture_host::FixtureHost; -use crate::topology::{dial_provider, stream_breaker_key, DialProviderError}; -use busbar_contract::upstream::{CanonicalSignal, StatusClass}; -use busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy; -use busbar_kernel::store::BreakerState; - -/// A canonical hard-down signal — the disposition a definitive provider failure (auth/billing, or a -/// busbar-side guard refusal) folds to, which OPENS the cell on the first record. -fn hard_down() -> CanonicalSignal { - CanonicalSignal { - class: StatusClass::Auth, - provider_signal: None, - retry_after: None, - } -} - -#[tokio::test] -async fn a_hard_down_provider_records_into_the_core_cell_and_opens_it() { - let host = FixtureHost::new(); - let pool = stream_breaker_key("openai-realtime"); - - // The cell starts Closed — no failure recorded yet. - assert_eq!( - host.breaker_state(&pool, 0), - BreakerState::Closed, - "the provider cell is Closed before any dial" - ); - - // A dial busbar's OWN net-guard refuses (a public loopback under the fail-closed default is an - // internal target) is a DEFINITIVE failure: `dial_provider` records the hard-down signal into the - // ONE host cell through the host seam, and the cell OPENS on this first failure. - let refused = dial_provider( - &host, - &pool, - 0, - "wss://127.0.0.1/", - GuardPolicy::default(), - None, - ) - .await - .err(); - assert!( - matches!(refused, Some(DialProviderError::Dial(_))), - "the guard-refused dial fails: {refused:?}" - ); - assert!( - matches!(host.breaker_state(&pool, 0), BreakerState::Open { .. }), - "the provider's host cell must be Open after a definitive dial failure — the record wired \ - into dial_provider fired" - ); -} - -#[tokio::test] -async fn a_tripped_provider_cell_refuses_before_the_dial() { - let host = FixtureHost::new(); - let pool = stream_breaker_key("openai-realtime"); - - // TRIP the cell through the same host seam a dial-leg failure records through. - busbar_kernel::plane_host::breaker::record_signal(&host, &pool, 0, &hard_down()); - assert!( - matches!(host.breaker_state(&pool, 0), BreakerState::Open { .. }), - "the cell is Open (tripped) before the fresh dial" - ); - - // A fresh dial against the tripped cell is refused at ADMISSION — `dial_provider` returns - // `BreakerOpen` with the cell's own cooldown, and NEVER dials (the target below is unreachable; a - // `Connect`/`Dial` error here would prove the admit did NOT fast-fail). - let refused = dial_provider( - &host, - &pool, - 0, - "ws://127.0.0.1:9/", - GuardPolicy { - allow_private: true, - allow_plaintext: true, - ..GuardPolicy::default() - }, - None, - ) - .await - .err(); - match refused { - Some(DialProviderError::BreakerOpen { retry_after_secs }) => { - assert!( - retry_after_secs >= 1, - "the fast-fail carries the cell's own Retry-After" - ); - } - other => panic!("a tripped cell must fast-fail BreakerOpen before the dial, got {other:?}"), - } -} diff --git a/crates/busbar-voice/src/tests/config_tests.rs b/crates/busbar-voice/src/tests/config_tests.rs deleted file mode 100644 index 1a16781235..0000000000 --- a/crates/busbar-voice/src/tests/config_tests.rs +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! Tests for `crates/busbar-voice/src/config.rs`. - -use super::*; -use crate::ir::control::IrVad; - -/// An ABSENT section (`default_section`) and an empty `streams: {}` must decode to the SAME value, -/// or a present-but-empty block would silently differ from an omitted one. -#[test] -fn empty_section_equals_default() { - let empty: StreamsCfg = - serde_yaml::from_value(serde_yaml::Value::Mapping(Default::default())).unwrap(); - assert_eq!(empty, StreamsCfg::default()); -} - -/// The plane defaults are the DoD ceilings, and the synthesized VAD carries the `streams:`-level -/// 500ms silence (not the IR wire default of 200ms). -#[test] -fn defaults_are_the_dod_values() { - let c = StreamsCfg::default(); - assert_eq!( - c.session_max_secs, None, - "no ceiling unless configured (Q21a)" - ); - assert_eq!(c.context_window_tokens, 32_768); - assert_eq!(c.max_output_tokens, 4096); - match c.session.turn_detection { - Some(Some(IrVad::ServerVad { - silence_duration_ms, - .. - })) => assert_eq!(silence_duration_ms, 500), - other => panic!("expected synthesized server_vad, got {other:?}"), - } -} - -/// A valid `streams:` block — session/VAD knobs plus the three ceilings — parses through the -/// owned `parse_section` hook (the boot-validate leg's (a) assertion). -#[test] -fn a_valid_streams_block_parses_through_parse_section() { - let y: serde_yaml::Value = serde_yaml::from_str( - "session:\n \ - voice: alloy\n \ - turn_detection:\n \ - type: server_vad\n \ - silence_duration_ms: 700\n\ - session_max_secs: 1800\n\ - context_window_tokens: 8192\n\ - max_output_tokens: 2048\n", - ) - .unwrap(); - let boxed = streams_parse_section(&y).expect("a valid streams: block must parse"); - assert!( - boxed.is_present(), - "an operator-written streams: block is present" - ); -} - -/// `deny_unknown_fields` refuses a typo'd key at parse — the boot-validate leg's (b) assertion. -#[test] -fn unknown_key_is_refused() { - let mut m = serde_yaml::Mapping::new(); - m.insert("nonsense_key".into(), 1.into()); - let err = streams_parse_section(&serde_yaml::Value::Mapping(m)).unwrap_err(); - assert!(err.contains("nonsense_key"), "{err}"); -} - -/// `default_section` is the empty section, and it is NOT `is_present` (an omitted block names no -/// plane and is not refused). -#[test] -fn default_section_is_absent() { - let s = streams_default_section(); - assert!(!s.is_present()); -} - -/// OWNER RULING Q21a: the session wall-clock ceiling is optional and plane-owned. Absent reads as no -/// ceiling; a positive figure is kept; zero and a negative figure are refused at parse. -#[test] -fn the_session_ceiling_is_optional_and_refuses_zero_and_negative() { - let parse = |yaml: &str| { - streams_parse_section(&serde_yaml::from_str::(yaml).unwrap()).map(|b| { - b.as_any() - .downcast_ref::() - .expect("the plane's own section") - .session_max_secs - }) - }; - assert_eq!(parse("{}").unwrap(), None, "absent: no ceiling"); - assert_eq!( - parse("session_max_secs: 1800") - .unwrap() - .map(std::num::NonZeroU32::get), - Some(1800) - ); - assert!(parse("session_max_secs: 0").is_err(), "zero is refused"); - assert!( - parse("session_max_secs: -5").is_err(), - "a negative figure is refused" - ); -} diff --git a/crates/busbar-voice/src/tests/egress_tests.rs b/crates/busbar-voice/src/tests/egress_tests.rs deleted file mode 100644 index 31c41bc57a..0000000000 --- a/crates/busbar-voice/src/tests/egress_tests.rs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE EGRESS CREDENTIAL CELL — `egress-auth × voice-client`. The voice plane plans its provider -//! credential through the ONE egress mechanism (`ProtocolDecl::egress_auth_headers`), and the value it -//! plans is the PROVIDER credential (the WebRTC `ek_` / the real provider key / the telephony carrier -//! secret), NEVER a caller governance token forwarded through. -//! -//! RED before the wiring: `DECLS.egress_auth_headers` was `None`, so the plane planned no egress -//! credential at all and the decl could not state that the dial carries busbar's own authority. - -use crate::DECLS; -use busbar_contract::config::UpstreamCreds; -use busbar_kernel::proto::SigningContext; - -#[test] -fn the_provider_dial_carries_the_planned_credential_and_never_a_caller_token() { - let builder = DECLS - .egress_auth_headers - .expect("the voice plane declares an egress credential builder (the ONE egress mechanism)"); - - // The resolved per-lane PROVIDER credential — what the composition root leases for the dial. A - // distinct token the caller might have presented at the front door, which must NEVER be the value - // planned onto the provider hop. - const PROVIDER_CREDENTIAL: &str = "ek_provider_scoped_secret_abc"; - const CALLER_TOKEN: &str = "caller-inbound-governance-bearer-xyz"; - - let ctx = SigningContext { - host: "api.openai.com", - canonical_uri: "/v1/realtime/calls", - body: b"", - timestamp_epoch: 0, - upstream_creds: UpstreamCreds::Own, - }; - let headers = builder(PROVIDER_CREDENTIAL, &ctx); - - // Exactly one header: the provider-facing `Authorization: Bearer `. - assert_eq!( - headers.len(), - 1, - "the plan is one provider Authorization header" - ); - let (name, value) = &headers[0]; - assert_eq!(name.as_str(), "authorization"); - assert_eq!( - value.to_str().unwrap(), - format!("Bearer {PROVIDER_CREDENTIAL}"), - "the dial carries the PLANNED provider credential" - ); - - // The provider hop carries ONLY the leased provider credential — the caller's inbound governance - // token appears nowhere in the plan (the builder reads the resolved credential string and nothing - // else, so a caller token could reach the wire only by being the leased credential, which the - // egress gate is what forbids). - assert!( - !value.to_str().unwrap().contains(CALLER_TOKEN), - "the caller's inbound governance token is never the value planned onto the provider dial" - ); - - // And the lane-constant declaration holds: the builder read nothing off the SigningContext (a pure - // function of the credential string), so the boot path may prebuild it once per lane. - assert!( - DECLS.egress_auth_lane_constant, - "the bearer credential is a pure function of the resolved string (lane-constant)" - ); -} diff --git a/crates/busbar-voice/src/tests/governance_budget_tests.rs b/crates/busbar-voice/src/tests/governance_budget_tests.rs deleted file mode 100644 index c17146fb6b..0000000000 --- a/crates/busbar-voice/src/tests/governance_budget_tests.rs +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! GOVERNANCE-BUDGET capability cells (voice-client + voice-server) — GATE-VALID location. -//! -//! OWNER RULING Q21b: a voice session is governed by the kernel's budget view like every other plane. -//! Each closed turn's raw counts per streaming-plane class go to the kernel's session account, which -//! ledgers them and answers `MustClose` (a hard close) the moment the caller's chain reads dry; a -//! caller whose chain is already dry is refused at the open. These cells replace the D2 lease's -//! (whose names they keep, because `qa/capability-equality.json` cites them): the host here is the -//! fixture host with a COUNT cap — each count one unit of the ceiling — since what a count is worth is -//! the read-time view's, proven over the real engine in the composition root's tests. - -use crate::ir::usage::IrDuplexUsage; -use crate::runtime::metering::{TurnMeter, TurnVerdict}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_plane_streaming::session::TurnCounters; -use std::sync::Arc; - -fn caller() -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: "vk-budget".to_string(), - ..Default::default() - } -} - -fn meter(host: &Arc) -> TurnMeter { - TurnMeter::new( - Arc::clone(host) as Arc, - caller(), - "streaming-server", - crate::OPENAI_REALTIME, - ) -} - -/// One turn of `n` emitted audio tokens. -fn turn(n: u64) -> IrDuplexUsage { - IrDuplexUsage { - audio_out: n, - ..IrDuplexUsage::default() - } -} - -/// voice-client cell: a session whose turns dry the caller's chain hard-closes, and stays closed. -#[test] -fn a_session_lease_settled_past_its_cap_hard_closes_and_refuses_further_spend() { - let host = Arc::new(FixtureHost::new().governed().with_count_cap(5)); - let session = meter(&host).open("m").expect("room").expect("governed"); - let quiet = TurnCounters::default(); - assert_eq!( - session.report_turn(Some(&turn(3)), quiet), - TurnVerdict::Live, - "3 of 5" - ); - assert_eq!( - session.report_turn(Some(&turn(3)), quiet), - TurnVerdict::MustClose, - "6 of 5: the chain is dry — a hard close" - ); - assert_eq!( - session.report_turn(Some(&turn(3)), quiet), - TurnVerdict::MustClose, - "past the cap the verdict stays a hard close" - ); - assert_eq!( - host.ledger_usage("vk-budget").map(|u| u.tokens), - Some(9), - "every delivered count is ledgered exactly, none dropped at the close" - ); -} - -/// voice-server cell: the session's counts are ledgered on the PRESENTING key, the turn that dries its -/// chain closes the carrier, and that key's next session is refused at the open. -#[test] -fn the_session_lease_bills_the_presenting_key_and_refuses_past_the_cap_with_a_hard_close() { - let host = Arc::new(FixtureHost::new().governed().with_count_cap(50)); - let session = meter(&host).open("m").expect("room").expect("governed"); - let quiet = TurnCounters::default(); - assert_eq!( - session.report_turn(Some(&turn(20)), quiet), - TurnVerdict::Live - ); - assert_eq!( - session.report_turn(Some(&turn(20)), quiet), - TurnVerdict::Live - ); - assert_eq!( - host.ledger_rows("vk-budget") - .get(&( - "streaming\u{1f}m".to_string(), - "audio_tokens_out".to_string() - )) - .copied(), - Some(40), - "the presenting key's row, under the voice lane and the plane's own class" - ); - assert_eq!( - session.report_turn(Some(&turn(20)), quiet), - TurnVerdict::MustClose, - "60 of 50: a hard close" - ); - assert!( - meter(&host).open("m").is_err(), - "the dry key's next session is refused at the open" - ); -} diff --git a/crates/busbar-voice/src/tests/governed_binding_tests.rs b/crates/busbar-voice/src/tests/governed_binding_tests.rs deleted file mode 100644 index 3ec48a6db6..0000000000 --- a/crates/busbar-voice/src/tests/governed_binding_tests.rs +++ /dev/null @@ -1,277 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE BINDING CELLS: a served session and a governed session are the same session. -//! -//! The port and its implementor were both in the tree and no served session had ever been handed one. -//! `SessionCore::with_governed` existed, the composition root's table existed, and between them sat a -//! door that opened every one of its sessions ungoverned — so a client-served tool call's wait was -//! entered by the root and there was nothing on any socket that could wake it or sweep it. These -//! cells judge the join: the composition writes a table, every served open reads it, and what the -//! sweep ends is ended at the deadline the plane itself declares rather than at a second spelling of -//! thirty seconds kept here. -//! -//! The table under test is a stand-in that answers the two questions the port declares, and it -//! answers them with the SAME arithmetic the kernel's own wait table uses — a deadline of -//! `now + TOOL_REPLY_DEADLINE_SECS`, swept when the wall is reached and not one millisecond earlier. -//! The root's own cells judge what the ending then does to a unit; what these judge is that a socket -//! can reach the ending at all, and reach it on time. - -use crate::ir::codec::OpenAiRealtimeCodec; -use crate::runtime::carrier::Carrier; -use crate::runtime::{GovernedCalls, ReplyRefusal, VoiceRuntime}; -use crate::topology::open_admitted_session; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use std::sync::{Arc, Mutex}; - -/// The deadline the plane declares for a tool call the node does not serve, in milliseconds. Read -/// from the plane crate the composition root builds its own reply leg from — one declaration, two -/// readers, so a change to it cannot leave this cell asserting the old number. -fn deadline_ms() -> u64 { - u64::from(busbar_plane_streaming::plane::TOOL_REPLY_DEADLINE_SECS) * 1_000 -} - -/// One open call, as the node's table holds it. -#[derive(Debug)] -struct OpenCall { - session: u64, - call_id: String, - /// The wall this call stops waiting at — entered as `now + the plane's declared seconds`, which - /// is the kernel wait table's own arithmetic rather than a second one written here. - deadline: u64, -} - -/// A stand-in for the composition root's open-call table, answering what the port declares and -/// recording what it was asked. -#[derive(Debug, Default)] -struct TableStandIn { - open: Mutex>, - woken: Mutex>, - swept: Mutex>, -} - -impl TableStandIn { - fn enter(&self, session: u64, call_id: &str, now_ms: u64) { - self.open.lock().unwrap().push(OpenCall { - session, - call_id: call_id.to_string(), - deadline: now_ms + deadline_ms(), - }); - } -} - -impl GovernedCalls for TableStandIn { - fn planned(&self, session: u64, call_id: &str, now_ms: u64) -> bool { - self.enter(session, call_id, now_ms); - true - } - - fn replied(&self, session: u64, call_id: &str) -> Result<(), ReplyRefusal> { - let mut open = self.open.lock().unwrap(); - if !open.iter().any(|c| c.session == session) { - return Err(ReplyRefusal::NoSuchSession); - } - let at = open - .iter() - .position(|c| c.session == session && c.call_id == call_id) - .ok_or(ReplyRefusal::UnknownCall)?; - let call = open.remove(at); - self.woken.lock().unwrap().push((session, call.call_id)); - Ok(()) - } - - fn expired(&self, now_ms: u64) -> usize { - let mut open = self.open.lock().unwrap(); - let (done, still): (Vec, Vec) = std::mem::take(&mut *open) - .into_iter() - .partition(|c| c.deadline <= now_ms); - *open = still; - let mut swept = self.swept.lock().unwrap(); - for c in &done { - swept.push((c.session, c.call_id.clone())); - } - done.len() - } -} - -fn runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(crate::runtime::tools::EchoToolExecutor), - ) -} - -/// **A served open takes the binding it is handed, and the sweep on that session reaches the table.** -/// -/// The post-admit open is the one door every served topology goes through — the dialed proxy, the -/// uplink-only fallback and the WebRTC sideband all reach a socket through it — so a binding it -/// carries is a binding all three carry. What is proved here is that the binding survives the open -/// and that the session's own tick lands on the composed table rather than on nothing. -#[test] -fn a_served_open_carries_its_binding_into_the_session_it_opens() { - let rt = runtime(); - let table = Arc::new(TableStandIn::default()); - let governed = crate::runtime::GovernedSession { - session: 4_242, - calls: Arc::clone(&table) as Arc, - }; - table.enter(4_242, "call_x", 0); - - let (core, _handle) = open_admitted_session( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-9", - None, - Carrier::sideband(), - None, - 1, - Some(governed), - ) - .expect("the session opens"); - - assert_eq!( - core.governed_session(), - Some(4_242), - "the session the node knows this pump as is the one the open was handed" - ); - assert_eq!( - core.sweep_expired(deadline_ms()), - 1, - "and the pump's own tick reaches that node's table" - ); - assert_eq!( - table.swept.lock().unwrap().as_slice(), - [(4_242, "call_x".to_string())], - "which is the call this session had open, ended by identifier" - ); -} - -/// **An unanswered call ends at exactly the deadline the plane declares, through the served path.** -/// -/// Not one millisecond early and not on the next tick that happens along. The figure is the plane's -/// own `TOOL_REPLY_DEADLINE_SECS` — the same declaration the composition root builds its reply leg -/// from — and it is read here rather than restated, so the two cannot drift. -#[test] -fn an_unanswered_call_is_swept_at_the_declared_deadline_and_not_before() { - let rt = runtime(); - let table = Arc::new(TableStandIn::default()); - let (core, _handle) = open_admitted_session( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-9", - None, - Carrier::sideband(), - None, - 1, - Some(crate::runtime::GovernedSession { - session: 7, - calls: Arc::clone(&table) as Arc, - }), - ) - .expect("the session opens"); - - table.enter(7, "call_late", 0); - assert_eq!( - core.sweep_expired(deadline_ms() - 1), - 0, - "a call is not ended one millisecond before the deadline its leg declared" - ); - assert_eq!( - core.sweep_expired(deadline_ms()), - 1, - "and it is ended at that deadline, on the tick that reaches it" - ); - assert!( - table.woken.lock().unwrap().is_empty(), - "an ending is not a settlement: nobody answered this call" - ); -} - -/// **Nothing is composed until a composition root composes it.** -/// -/// The plane mounted without the root's own switch keeps what it had: no table, and every session it -/// serves is the ungoverned kind the runtime served before the wait existed. -#[test] -fn a_node_with_no_table_composed_serves_what_it_served_before() { - let rt = runtime(); - let (core, _handle) = open_admitted_session( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-9", - None, - Carrier::sideband(), - None, - 1, - None, - ) - .expect("the session opens"); - assert_eq!(core.governed_session(), None); - assert_eq!(core.sweep_expired(u64::MAX), 0, "and it sweeps nothing"); -} - -/// **The composed table is what a served session-open reads, and each open reads a session of its -/// own.** -/// -/// Two conversations may carry identical call identifiers — a provider mints them per conversation — -/// so the number the root keys its table by has to be unique per session on this node. It is minted -/// here, once per open. -#[test] -fn the_composition_hands_each_served_open_its_own_session() { - assert!( - crate::mount::install_governed_calls( - Arc::new(TableStandIn::default()) as Arc - ), - "the first composition stands" - ); - assert!( - crate::mount::governed_calls_composed(), - "and the door now serves governed sessions" - ); - assert!( - !crate::mount::install_governed_calls( - Arc::new(TableStandIn::default()) as Arc - ), - "a second compose is a no-op, never a silent swap of the table live sessions are keyed into" - ); - - let first = - crate::mount::served_governed_session().expect("a composed node binds every session"); - let second = - crate::mount::served_governed_session().expect("a composed node binds every session"); - assert_ne!( - first.session, second.session, - "two sessions on one node are told apart before either has a call open" - ); -} - -/// **THE ROOT IDENTITY, from this side: no served session-open reaches the ungoverned path.** -/// -/// The cells above prove a binding works when one is handed over. This one proves there is no served -/// call site that declines to hand one — the property that makes "served" and "governed" the same set -/// rather than two sets that happen to overlap. It reads the door's own source because that is where -/// the property lives: the three legs are three call sites in one function, and a fourth added -/// without the binding would be a session nobody could sweep, discovered by a customer. -#[test] -fn every_served_session_open_asks_the_composition_for_its_binding() { - let door = include_str!("../mount.rs"); - let accept = door - .split_once("pub(crate) async fn ws_accept") - .expect("the door is where the served legs are") - .1; - - let opens = accept.matches("open_admitted_session(").count() - + accept.matches("open_admitted_telephony(").count(); - assert_eq!( - opens, 3, - "the served legs are the dialed proxy, the uplink-only fallback and the WebRTC sideband; \ - a fourth needs its own binding and this cell counted {opens}" - ); - let bound = accept.matches("served_governed_session()").count(); - assert_eq!( - bound, opens, - "every served session-open asks the composition for its binding: {bound} of {opens} do" - ); -} diff --git a/crates/busbar-voice/src/tests/governed_table_tests.rs b/crates/busbar-voice/src/tests/governed_table_tests.rs deleted file mode 100644 index f657635aa7..0000000000 --- a/crates/busbar-voice/src/tests/governed_table_tests.rs +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The node's open-call table as a served session pump reaches it: the pump plans the client-served -//! leg into the table, the client's reply is taken, and a session's teardown frees its rows. - -use std::sync::Arc; - -use busbar_contract::ids::UnitKey; -use busbar_plane_streaming::governed::GovernedCalls; -use busbar_plane_streaming::open_calls::{NodeCalls, OpenToolCalls}; - -use crate::ir::codec::{OpenAiRealtimeCodec, WireEvent}; -use crate::runtime::{Carrier, GovernedSession, SessionCore, ToolExecutor}; - -/// A tool executor that serves no tool: every call the session sees is the client's to answer. -#[derive(Debug)] -struct ServesNoTool; - -#[async_trait::async_trait] -impl ToolExecutor for ServesNoTool { - fn executes_here(&self, _name: &str) -> bool { - false - } - async fn execute(&self, _name: &str, _arguments: &[u8]) -> Vec { - b"{\"executed\":\"in-process\"}".to_vec() - } -} - -/// A real session pump bound to `port`'s table as `session`, serving no tool. -fn pump_serving_no_tool(port: &Arc, session: u64) -> SessionCore { - SessionCore::new( - OpenAiRealtimeCodec, - None, - Arc::new(ServesNoTool), - Carrier::sideband(), - None, - ) - .with_governed(GovernedSession { - session, - calls: Arc::clone(port) as Arc, - }) -} - -/// One OpenAI Realtime wire frame. -fn realtime_frame(json: &serde_json::Value) -> WireEvent { - WireEvent(bytes::Bytes::from( - serde_json::to_vec(json).expect("serializes"), - )) -} - -/// The upstream frames a plan writes, as one string. -fn upstream_text(plan: &crate::runtime::Outbound) -> String { - plan.upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect::>() - .join("\n") -} - -/// **Q72(1) / R4: the served session plans the client-served leg, so the client's answer is taken.** -/// -/// The call is announced to a real session pump bound to the node's real table, and the pump is the -/// only thing that can have entered the wait. The tool is one the node does not serve, so nothing is -/// executed in-process; the client's own `function_call_output` for that call is then accepted and -/// carried to the model. Before the writer was wired the table stayed empty and this reply was -/// refused (`refused_reply`). The RED arm: a reply naming a call nobody planned is still refused and -/// reaches the model on no wire. -#[tokio::test] -async fn a_served_session_plans_the_client_leg_and_accepts_its_reply() { - let port = Arc::new(NodeCalls::new(Arc::new(OpenToolCalls::new()), keys())); - let core = pump_serving_no_tool(&port, 5_151); - - for f in [ - serde_json::json!({"type":"response.output_item.added", - "item":{"type":"function_call","call_id":"call_cli","name":"client_tool"}}), - serde_json::json!({"type":"response.function_call_arguments.delta", - "call_id":"call_cli","delta":"{}"}), - serde_json::json!({"type":"response.function_call_arguments.done","call_id":"call_cli"}), - ] { - let plan = core.on_server_frame(realtime_frame(&f)).await; - assert!( - plan.upstream.is_empty(), - "a tool the node does not serve is never executed in-process: {}", - upstream_text(&plan) - ); - } - assert_eq!( - port.table().open(), - 1, - "the pump planned the client-served leg into the node's own table" - ); - - let forged = core.on_client_frame(realtime_frame(&serde_json::json!({ - "type":"conversation.item.create", - "item":{"type":"function_call_output","call_id":"call_unplanned","output":"1"}}))); - assert!( - forged.refused_reply, - "a reply for an unplanned call is refused" - ); - assert!( - forged.upstream.is_empty(), - "and it reaches the model on no wire at all: {}", - upstream_text(&forged) - ); - - let answered = core.on_client_frame(realtime_frame(&serde_json::json!({ - "type":"conversation.item.create", - "item":{"type":"function_call_output","call_id":"call_cli","output":"42"}}))); - assert!( - !answered.refused_reply, - "the client's reply for a planned client-served call is accepted" - ); - let up = upstream_text(&answered); - assert!( - up.contains("function_call_output") - && up.contains("call_cli") - && up.contains("response.create"), - "the accepted reply reaches the model and asks it to continue: {up}" - ); - assert_eq!( - port.table().rows(), - 0, - "and the wait left the table, ending and all" - ); -} - -/// **A served session's teardown forgets its calls.** A session that ends with a client-served call -/// still open leaves nothing behind in the node's table. -#[tokio::test] -async fn a_served_sessions_teardown_forgets_the_calls_it_had_open() { - let port = Arc::new(NodeCalls::new(Arc::new(OpenToolCalls::new()), keys())); - let core = Arc::new(pump_serving_no_tool(&port, 6_161)); - for f in [ - serde_json::json!({"type":"response.output_item.added", - "item":{"type":"function_call","call_id":"call_left","name":"client_tool"}}), - serde_json::json!({"type":"response.function_call_arguments.done","call_id":"call_left"}), - ] { - let _ = core.on_server_frame(realtime_frame(&f)).await; - } - assert_eq!( - port.table().open(), - 1, - "the call is waiting when the session ends" - ); - - let rt = crate::runtime::build_runtime(&(), None) - .downcast::() - .expect("the plane's own runtime"); - let handle = rt.bind_session("acct", "call-teardown"); - handle.open(1).expect("the session's row opens"); - crate::runtime::serve_to_teardown(Arc::clone(&core), handle, async {}, || 2).await; - assert_eq!( - port.table().rows(), - 0, - "the ended session's calls left the table at its teardown" - ); -} - -/// A node allocator for the port under test: unique keys from 1, as the kernel's mint hands them. -fn keys() -> Box UnitKey + Send + Sync> { - let next = std::sync::atomic::AtomicU64::new(1); - Box::new(move || UnitKey::new(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed))) -} diff --git a/crates/busbar-voice/src/tests/hook_gate_tests.rs b/crates/busbar-voice/src/tests/hook_gate_tests.rs deleted file mode 100644 index 80086993c6..0000000000 --- a/crates/busbar-voice/src/tests/hook_gate_tests.rs +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE HOOKS-GATE CELL — `hooks-gate × {voice-client, voice-server}` (one wiring, both directions). -//! `streams.hooks: [reject-all]` is attached, a session-open is driven through the governed choke -//! point, and it is REFUSED before any lease / mint / dial. The gate fires through the kernel's -//! `plane_host::admission_gates_decide` over the hooks seam (the Seam-B inversion — this plane names -//! no core hook symbol): the host is the plane's in-memory fixture host carrying a scripted gate, -//! filed as a resolved hook policy under the plane's own decl key and `streams` container, so the -//! kernel's real gate decision fires it exactly as it fires a configured deployment's. (The same -//! verdict through the kernel's hook port is the engine's own hook battery to prove; this plane's -//! tests do not link the engine.) -//! -//! The control makes the refusal falsifiable: the identical open with NOTHING attached proceeds past -//! the gate (the byte-identical-when-unconfigured guarantee, exercised). -//! -//! RED before the wiring: `open_governed` never consulted the gate, so `reject-all` served the open. - -use crate::mount::{open_governed, GovernedOpen, Ingress}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::{FixtureHost, GateScript}; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use busbar_kernel::plane_host::{EngineHost, GateOutcome}; -use std::sync::Arc; - -/// The `streams:` container the voice plane files its operator hooks under. -const GATE_CONTAINER: &str = "streams"; - -/// A gate whose `raw_decide_reply` drives its verdict verbatim — the same reply shape the kernel's -/// hook double reads off its settings: `{"reject": {"status", "message"}}` refuses, anything -/// else proceeds. -fn gate(settings: serde_json::Value) -> GateScript { - let reply = settings["raw_decide_reply"].clone(); - Arc::new(move |_args_json: &[u8]| match reply.get("reject") { - Some(reject) => GateOutcome::Reject { - status: reject["status"].as_u64().unwrap_or(403) as u16, - message: reject["message"].as_str().unwrap_or_default().to_string(), - hook: "test-hook".to_string(), - }, - None => GateOutcome::Proceed, - }) -} - -fn runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ) -} - -/// A host with nothing attached: the open proceeds past the gate untouched. -fn ungated_host() -> Arc { - FixtureHost::new().into_host() -} - -/// Build a host whose `voice`/`streams` container carries the attached gate, filed under the plane's -/// own decl key exactly where production's resolved gate map puts it, so `admission_gates_attached` -/// answers true and `admission_gates_decide` runs the gate. `hook_name` is the operator's name for the hook (it only labels -/// the attachment here). -fn gated_host(_hook_name: &'static str, script: GateScript) -> Arc { - FixtureHost::new() - .attach_gate(crate::PLANE_DECLARATION.key, GATE_CONTAINER, script) - .into_host() -} - -fn an_open<'a>(rt: &'a VoiceRuntime, host: Arc) -> GovernedOpen<'a> { - GovernedOpen { - rt, - host, - provider: None, - // The `ek_` MINT one-shot pass — a LIVE `open_governed` production ingress (browser WebRTC), - // so this cell proves the gate on a path production actually takes. The WS-accept front door - // (sideband + telephony) runs the SAME `hook_gate` before the upgrade; that production path is - // proven by `a_reject_all_operator_gate_refuses_a_ws_accept_before_the_upgrade` below. - ingress: Ingress::Mint, - owner: "acct".to_string(), - call_id: "call-gate".to_string(), - vkey: None, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 1, - } -} - -#[tokio::test] -async fn streams_hooks_reject_all_refuses_a_session_open() { - let rt = runtime(); - - // ── THE CONTROL: nothing attached ⇒ the open proceeds PAST the gate (byte-identical, no gate hop). - let control = open_governed(an_open(&rt, ungated_host())).await; - assert_eq!( - control.status(), - axum::http::StatusCode::NOT_IMPLEMENTED, - "with no gate attached the session-open proceeds past the gate to the governed open" - ); - - // ── THE TEST: `streams.hooks: [reject-all]`, same open, REFUSED before any lease/mint/dial. ────── - let host = gated_host( - "reject-all", - gate(serde_json::json!({ - "raw_decide_reply": {"reject": {"status": 403, "message": "no voice session today"}} - })), - ); - - let refused = open_governed(an_open(&rt, host)).await; - assert_eq!( - refused.status(), - axum::http::StatusCode::FORBIDDEN, - "`streams.hooks: [reject-all]` must REFUSE the session-open before any lease/mint/dial" - ); - let body = axum::body::to_bytes(refused.into_body(), usize::MAX) - .await - .expect("the refusal body reads"); - assert_eq!( - String::from_utf8_lossy(&body), - "no voice session today", - "the hook's OWN message reaches the caller, so an operator can tell WHICH control refused" - ); -} - -/// The plane's real dispatch slot, built the way `appbuild` does — a `BuildCtx` over a `public_url`. -fn a_slot() -> Arc { - let unit = (); - let ctx = busbar_kernel::plane::registry::BuildCtx { - endpoint_slot: None, - agent_defs: &unit, - tool_defs: &unit, - public_url: Some("https://voice.example"), - prior: None, - providers: None, - }; - crate::mount::voice_build(&ctx).expect("voice_build yields a slot for a public_url") -} - -/// Drive the REAL `ws_accept` over a loopback WS server with `host`, and return the client handshake -/// outcome: `Ok` on a 101 upgrade (the gate proceeded), `Err` on a pre-upgrade refusal (the gate -/// rejected — a 403, no socket bound). A WS upgrade cannot be forged off a live connection -/// (`ConnectionNotUpgradable`), so the accept fn must be driven through a real server + client. -async fn ws_accept_handshake(host: Arc) -> Result<(), ()> { - #[derive(Clone)] - struct S { - host: Arc, - slot: Arc, - } - async fn route( - axum::extract::State(s): axum::extract::State, - upgrade: axum::extract::ws::WebSocketUpgrade, - ) -> axum::response::Response { - let arrival = busbar_kernel::ingress::duplex_ws::WsArrival { - upgrade, - gov: None, - principal: None, - caller_principal: Some("acct".to_string()), - path: "/telephony/call-ws".to_string(), - uri: axum::http::Uri::from_static("/telephony/call-ws"), - headers: axum::http::HeaderMap::new(), - path_params: vec![("call_id".to_string(), "call-ws".to_string())], - host: s.host, - slot: s.slot, - }; - crate::mount::ws_accept( - arrival, - Ingress::Telephony, - crate::ir::codec::OpenAiRealtimeCodec, - ) - .await - } - let app = axum::Router::new() - .route("/telephony/call-ws", axum::routing::get(route)) - .with_state(S { - host, - slot: a_slot(), - }); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - let policy = busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy { - allow_private: true, - allow_plaintext: true, - ..busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy::default() - }; - match busbar_kernel::egress::duplex_ws::dial(&format!("ws://{addr}/telephony/call-ws"), policy) - .await - { - Ok(_) => Ok(()), - Err(_) => Err(()), - } -} - -/// THE WS-ACCEPT FRONT DOOR honors the operator gate — telephony is the sharp case (it has NO preceding -/// `ek_` mint pass, so before this wiring it reached the media leg screened by nothing but the -/// destination gauntlet). A `reject-all` gate REFUSES the session-open BEFORE the socket upgrades, so -/// the client handshake FAILS (a pre-upgrade 403, no socket, no session). The control (no gate) -/// upgrades (101), making the refusal falsifiable. -#[tokio::test] -async fn a_reject_all_operator_gate_refuses_a_ws_accept_before_the_upgrade() { - assert!( - ws_accept_handshake(ungated_host()).await.is_ok(), - "with no gate attached the telephony WS-accept proceeds past the gate and upgrades (101)" - ); - - let host = gated_host( - "reject-all", - gate(serde_json::json!({ - "raw_decide_reply": {"reject": {"status": 403, "message": "no voice session today"}} - })), - ); - assert!( - ws_accept_handshake(host).await.is_err(), - "a reject-all operator gate REFUSES a telephony WS-accept BEFORE the socket upgrades — telephony \ - has no `ek_` mint pass, so the operator gate on this front door is its ONLY request screening" - ); -} diff --git a/crates/busbar-voice/src/tests/hook_tap_tests.rs b/crates/busbar-voice/src/tests/hook_tap_tests.rs deleted file mode 100644 index e867757d9f..0000000000 --- a/crates/busbar-voice/src/tests/hook_tap_tests.rs +++ /dev/null @@ -1,409 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE HOOKS-TAP CELL — `hooks-tap × {voice-client, voice-server}` (one wiring, both directions). -//! `streams.hooks: [rewrite]` with a `prompt: rw` gate is attached, a session-open is driven through -//! the governed choke point, and the params THE PROVIDER RECEIVES are the ones the hook rewrote them -//! to — asserted on the ACTUAL mint request the loopback provider saw (a rewrite is only real if the -//! thing downstream saw the rewritten value). The tap fires through the kernel's -//! `plane_host::admission_transform`, after the gate and before the credential is leased. The host is -//! the substrate's in-memory fixture host carrying a scripted rewrite under the plane's own decl key and -//! `streams` container, so the kernel's `admission_tap_attached` / `admission_transform` run the rewrite -//! exactly as they do over a configured -//! deployment. (The same rewrite over the real loaded hook plugin is the engine's own hook battery to -//! prove; this plane's tests do not link the engine.) -//! -//! ## The control is the byte-identical guarantee, exercised -//! -//! The identical open with the rewrite hook REMOVED must reach the provider carrying the plane's -//! ORIGINAL locked params, byte for byte — the no-op-absent-hooks guarantee. -//! -//! RED before the wiring: `open_governed` never ran the transform, so the provider always saw the -//! plane's own params and no rewrite could change a byte. - -use crate::ir::config::SessionConfig; -use crate::mount::{open_governed, GovernedOpen, Ingress, ProviderEndpoint}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::{FixtureHost, RewriteScript}; -use crate::testkit::loopback_http::{MockResponse, MockServer, MockServerState}; -use busbar_contract::hooks::{RewriteReply, TransformOutcome}; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use busbar_kernel::plane_host::EngineHost; -use std::sync::Arc; - -/// The `streams:` container the voice plane files its operator hooks under. -const GATE_CONTAINER: &str = "streams"; - -/// A `prompt: rw` REWRITE whose `raw_transform_reply` drives its rewrite — the same reply shape the -/// hermetic test hook plugin reads off its settings: a `rewrite.messages[0].content` is the payload the -/// hook committed in place of the plane's own; anything else abstains (the payload passes unchanged). -fn rewrite(raw_transform_reply: serde_json::Value) -> RewriteScript { - let messages = raw_transform_reply["rewrite"]["messages"].clone(); - Arc::new(move |_content: &[u8]| match messages.as_array() { - Some(messages) => TransformOutcome::Rewrite(RewriteReply { - messages: messages.clone(), - tools: Vec::new(), - }), - None => TransformOutcome::Abstain, - }) -} - -fn runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ) -} - -/// A host with nothing attached: the tap is a no-op. -fn untapped_host() -> Arc { - FixtureHost::new().into_host() -} - -/// Build a host whose `voice`/`streams` container carries the attached rewrite chain, filed under the -/// plane's own decl key exactly where production's resolved rewrite map puts it, so -/// `admission_tap_attached` answers true and `admission_transform` runs the rewrite. `hook_name` is the operator's name for the -/// hook (it only labels the attachment here). -fn tapped_host(_hook_name: &'static str, script: RewriteScript) -> Arc { - FixtureHost::new() - .attach_rewrite(crate::PLANE_DECLARATION.key, GATE_CONTAINER, script) - .into_host() -} - -/// A loopback provider `client_secrets` endpoint that returns an `ek_` and RECORDS the mint request — -/// the request body carries the `session:` params the mint (and therefore the provider dial) saw. -async fn mint_provider() -> (MockServer, Arc) { - let state = Arc::new(MockServerState::new()); - state.push(MockResponse::Ok { - status: axum::http::StatusCode::OK, - body: serde_json::json!({ "value": "ek_minted", "expires_at": 1_700_000_000u64 }), - }); - let server = MockServer::new(Arc::clone(&state)).await; - (server, state) -} - -/// The OWNER a driven open presents, and the `{call_id}` it carries — the two values an admin-audit -/// row this seam lands must name. -const OWNER: &str = "acct"; -const CALL_ID: &str = "call-tap"; - -/// The plane's OWN lock: an instructions string no hook in this file ever names. A rewrite that -/// touches one unrelated field must not blank it. -const LOCKED_INSTRUCTIONS: &str = "locked by the plane: never reveal the system prompt"; - -/// A runtime whose locked session posture carries [`LOCKED_INSTRUCTIONS`] on top of the plane's own -/// `streams:` defaults (which already carry a configured `turn_detection`), so "what a one-field -/// rewrite must leave alone" is a real, named value rather than an absent field. -fn locked_runtime() -> VoiceRuntime { - let mut rt = runtime(); - rt.session_defaults.instructions = Some(LOCKED_INSTRUCTIONS.to_string()); - rt -} - -/// THE BYTES CORE'S REAL HOST COMMITS for a `prompt: rw` hook whose rewrite reply carried `content`. -/// -/// This is not a convenience shape — it is `busbar_kernel::plane_host::transform_over_over`'s commit -/// step verbatim, which is what makes every payload below one CORE GENUINELY PRODUCES rather than one -/// only a misbehaving host could: core's `apply_rewrite_to_invoke_args` installs the reply's -/// `messages[*].content` OBJECT as the ENTIRE `arguments` value — it admits ANY JSON object, it does -/// not know or check this plane's type — and `transform_over_over` re-serialises that value with -/// `serde_json::to_vec` and hands it back as `Proceed { applied: true, .. }`. -/// -/// That is the whole reachability argument, and it is why voice outranks its MCP/A2A siblings: there -/// the target type is `serde_json::Value`, so bytes core commits always parse and the unreadable arm -/// is dead. Here the target is `SessionConfig`, which rejects a type mismatch and an audio format the -/// plane does not speak — so core can, and does, commit bytes the apply site cannot read. -/// `plane_host/tests/mod_tests.rs::a_committed_invoke_rewrite_installs_any_json_object_verbatim` pins -/// core's half of that sentence at core's own function. -fn commits(content: serde_json::Value) -> RewriteScript { - rewrite(serde_json::json!({ - "rewrite": { "messages": [ { "role": "user", "content": content } ] } - })) -} - -/// A fixture host carrying the attached rewrite chain, kept as the CONCRETE type so the cell can read -/// the admin-audit log back off the same host the plane audited through. -fn tapped_fixture(script: RewriteScript) -> Arc { - Arc::new(FixtureHost::new().attach_rewrite( - crate::PLANE_DECLARATION.key, - GATE_CONTAINER, - script, - )) -} - -/// Drive one `Mint` open through the governed choke point against the loopback provider and return -/// the RAW answer. The refusal cells assert on its STATUS; the rewrite cells read the provider's -/// recorded request instead (a rewrite is only real if the thing downstream saw it). -async fn open_mint( - rt: &VoiceRuntime, - host: Arc, - base_url: &str, -) -> axum::response::Response { - let provider = ProviderEndpoint { - base_url: base_url.to_string(), - api_key: busbar_contract::Redacted::new("sk-real-key".to_string()), - }; - open_governed(GovernedOpen { - rt, - host, - provider: Some(&provider), - ingress: Ingress::Mint, - owner: OWNER.to_string(), - call_id: CALL_ID.to_string(), - vkey: None, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 1, - }) - .await -} - -/// Drive one `Mint` open through the governed choke point against the loopback provider, and return -/// the `session:` object the mint request carried. -async fn minted_session( - host: Arc, - base_url: &str, - state: &MockServerState, -) -> serde_json::Value { - let rt = runtime(); - let resp = open_mint(&rt, host, base_url).await; - assert_eq!( - resp.status(), - axum::http::StatusCode::OK, - "the mint pass serves" - ); - let raw = state - .get_last_request_body() - .expect("the mint sent a request body"); - let sent: serde_json::Value = serde_json::from_slice(&raw).expect("the mint body is json"); - sent["session"].clone() -} - -#[tokio::test] -async fn no_hook_leaves_the_params_byte_identical() { - let (server, state) = mint_provider().await; - // Nothing attached ⇒ the tap is a no-op and the provider sees the plane's ORIGINAL locked params. - let session = minted_session(untapped_host(), &server.base_url(), &state).await; - assert_eq!( - session, - serde_json::to_value(runtime().session_defaults).unwrap(), - "with no rewrite hook the provider must receive the plane's locked params BYTE FOR BYTE — the \ - no-op-absent-hooks guarantee" - ); - server.shutdown().await; -} - -#[tokio::test] -async fn a_rewrite_hook_edits_the_session_open_params_before_the_provider_dial() { - let (server, state) = mint_provider().await; - - // The rewrite REPLACES the session-open params with a hook-authored posture — a distinct - // instructions string the plane default never carries, so its presence at the provider is proof - // the rewrite reached the wire. - let rewritten: SessionConfig = SessionConfig { - instructions: Some("rewritten-by-hook".to_string()), - voice: Some("marin".to_string()), - ..SessionConfig::default() - }; - let host = tapped_host( - "rewrite", - rewrite(serde_json::json!({ - "rewrite": { - "messages": [ - { "role": "user", "content": serde_json::to_value(&rewritten).unwrap() } - ] - } - })), - ); - - let session = minted_session(host, &server.base_url(), &state).await; - assert_eq!( - session["instructions"], "rewritten-by-hook", - "the provider mint must carry the params THE HOOK REWROTE THEM TO. The plane's own \ - instructions here would be the whole finding: the rewrite fired in memory but never reached \ - the wire. Session the provider saw: {session}" - ); - server.shutdown().await; -} - -/// THE FAIL-OPEN, DRIVEN END TO END. A `prompt: rw` hook COMMITS a rewrite (`applied: true`) whose -/// output busbar cannot read back as a [`SessionConfig`]. The apply site read it with -/// `if let Ok(v) = serde_json::from_slice(…)`, so the committed rewrite fell through to `Ok(None)` — -/// which both callers read as "the hook made no rewrite" — and the session opened carrying the -/// plane's OWN params. -/// -/// RED BEFORE THE FIX, on exactly this text: the mint pass answered `200` and the loopback provider -/// received the plane's original locked session params. A redaction hook's whole purpose is that the -/// thing it removed does not reach the backend; here it reached the backend and nothing said so. -/// -/// Both payloads are ones CORE GENUINELY COMMITS (see [`commits`]): a type mismatch, and an audio -/// format the plane does not speak. Neither is a misbehaving host — `apply_rewrite_to_invoke_args` -/// admits any JSON object, and this plane's target type is not `serde_json::Value`. -/// -/// The assertions are all three halves of the refusal: the caller is answered `500`, THE PROVIDER IS -/// NEVER DIALED (the un-rewritten params reach nothing), and the refusal is on the admin-audit trail -/// under the plane's own session-open action with outcome `rejected`. -#[tokio::test] -async fn a_committed_rewrite_busbar_cannot_read_back_refuses_the_session_open() { - for (why, content) in [ - ( - "a type mismatch: `voice` is a string on this type", - serde_json::json!({ "voice": 7 }), - ), - ( - "an audio format the plane does not speak", - serde_json::json!({ "input_audio_format": "flac" }), - ), - ] { - let (server, state) = mint_provider().await; - let fx = tapped_fixture(commits(content.clone())); - let rt = locked_runtime(); - let resp = open_mint( - &rt, - Arc::clone(&fx) as Arc, - &server.base_url(), - ) - .await; - - assert_eq!( - resp.status(), - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "a committed rewrite busbar cannot read back must REFUSE the session-open. A 200 here is \ - the whole finding: the hook said it replaced these params and busbar opened the session \ - with the plane's own instead. Payload ({why}): {content}" - ); - assert!( - state.get_last_request_body().is_none(), - "the refusal must land BEFORE the provider is dialed — the params the hook said it had \ - replaced must reach nothing. Payload ({why}): {content}" - ); - assert!( - fx.audit_log() - .iter() - .any(|e| e.action == crate::mount::SESSION_AUDIT_ACTION - && e.resource == format!("streaming:{CALL_ID}") - && e.outcome == "rejected" - && e.principal == OWNER), - "the refusal must be ON THE AUDIT TRAIL under the plane's own session-open action — a \ - hook that ran, reported applied, and was refused is an operator-visible event, not a \ - silent one. Payload ({why}): {content}. Log: {:?}", - fx.audit_log() - ); - server.shutdown().await; - } -} - -/// THE SECOND FAIL-OPEN ON THE SAME SEAM, pointed the other way. A committed rewrite that DOES -/// deserialize still went through as a WHOLESALE REPLACEMENT, so `{"voice": "marin"}` parsed into a -/// config whose every other field was the serde default — and the plane's locked `instructions` (and -/// its configured `turn_detection`) were blanked by a rewrite that never named them. -/// -/// RED BEFORE THE FIX, on exactly this text: the provider received a session carrying `voice: marin` -/// and NO `instructions` at all. -/// -/// The assertion is the exact whole object, not just the two interesting keys: a rewrite that names -/// one field must move EXACTLY that field. `SessionConfig`'s own wire contract is patch semantics -/// ("a partial `session.update` patches only what it names"), and this is that contract honored at -/// the one place a hook can write the config. -#[tokio::test] -async fn a_rewrite_touching_one_field_leaves_the_rest_of_the_locked_params_intact() { - let (server, state) = mint_provider().await; - let fx = tapped_fixture(commits(serde_json::json!({ "voice": "marin" }))); - let rt = locked_runtime(); - let resp = open_mint( - &rt, - Arc::clone(&fx) as Arc, - &server.base_url(), - ) - .await; - assert_eq!( - resp.status(), - axum::http::StatusCode::OK, - "a readable one-field rewrite still serves" - ); - - let raw = state - .get_last_request_body() - .expect("the mint sent a request body"); - let sent: serde_json::Value = serde_json::from_slice(&raw).expect("the mint body is json"); - let session = sent["session"].clone(); - - // The locked posture with EXACTLY the one field the hook named moved. - let mut expected = locked_runtime().session_defaults; - expected.voice = Some("marin".to_string()); - assert_eq!( - session, - serde_json::to_value(&expected).unwrap(), - "a rewrite that names ONE field must move exactly that field. The plane's locked \ - instructions ({LOCKED_INSTRUCTIONS:?}) and its configured turn_detection are not the hook's \ - to blank by omission — an absent key is the wire's \"say nothing\", not \"clear it\". \ - Session the provider saw: {session}" - ); - server.shutdown().await; -} - -/// THE RULE ITSELF, driven directly: the shapes a behavioural cell cannot cheaply arrange, plus the -/// two identities the merge has to preserve to be safe to put in front of EVERY committed rewrite. -/// -/// `committed_session_config` is its own function precisely so this is possible — the plane cannot -/// make a host misbehave, and the rule the plane owns is what it does when one does. -#[test] -fn committed_session_config_refuses_what_it_cannot_use_and_patches_what_it_can() { - use crate::mount::committed_session_config as read_back; - let locked = locked_runtime().session_defaults; - - // (1) NOT JSON AT ALL. `Ok(None)` here meant "the hook made no rewrite", which is the lie. - assert!( - read_back(&locked, b"not json").is_err(), - "bytes that are not JSON are not session params" - ); - - // (2) WELL-FORMED JSON THAT IS NOT AN OBJECT. The value is SUBSTITUTED for the plane's locked - // config, and these params are an object on every wire the plane speaks; admitting `7` would - // mean telling the hook its rewrite landed and then opening a session with a number. - for scalar in [&b"7"[..], br#""marin""#, b"null", b"[]", b"true"] { - assert!( - read_back(&locked, scalar).is_err(), - "a JSON scalar is not a session-params object: {}", - String::from_utf8_lossy(scalar) - ); - } - - // (3) AN OBJECT THAT PARSES AS JSON BUT NOT AS THIS TYPE — the merge happens BEFORE the decode, - // so a patch naming one bad field refuses the open instead of being quietly dropped. - for bad in [ - &br#"{"output_audio_format":"flac"}"#[..], - br#"{"voice":7}"#, - br#"{"modalities":"audio"}"#, - ] { - assert!( - read_back(&locked, bad).is_err(), - "an object this type rejects must refuse, not fall through: {}", - String::from_utf8_lossy(bad) - ); - } - - // (4) THE NO-OP IDENTITY. A patch that names nothing moves nothing. This is what makes the merge - // safe in front of every committed rewrite: the locked params must survive a round trip - // through their own JSON projection UNCHANGED, three-state `turn_detection`, wire-named audio - // formats and `max_output_tokens`' `"inf"` sentinel included. - assert_eq!( - read_back(&locked, b"{}").expect("an empty patch is readable"), - locked, - "an empty patch must be the identity — if the projection round trip loses a field, every \ - one-field rewrite silently drops it too" - ); - - // (5) CLEARING STAYS EXPRESSIBLE. A patch's "say nothing" is ABSENCE, not `null`, so a hook that - // genuinely means to remove the plane's instructions can still say so by NAMING the key. The - // merge narrows silence, not intent. - let cleared = read_back(&locked, br#"{"instructions":null}"#).expect("an explicit null reads"); - assert_eq!( - cleared.instructions, None, - "naming a key with an explicit null CLEARS it — the merge must not become a lock-in" - ); - assert_eq!( - cleared.turn_detection, locked.turn_detection, - "...while the keys the patch did not name keep the plane's locked values" - ); -} diff --git a/crates/busbar-voice/src/tests/metering_lease_tests.rs b/crates/busbar-voice/src/tests/metering_lease_tests.rs deleted file mode 100644 index ff6c748de1..0000000000 --- a/crates/busbar-voice/src/tests/metering_lease_tests.rs +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SERVED ROUTE ASKS THE KERNEL'S BUDGET VIEW, AND A SERVED SESSION LEDGERS THE PLANE'S COUNTS. -//! -//! OWNER RULING Q21b replaced the per-key D2 lease with the kernel's session account. Two facts, -//! proven through the mounted open (`open_governed`) rather than the runtime alone: -//! -//! 1. THE GATE IS THE KERNEL'S. A presenting key whose chain the kernel's budget view reads dry is -//! refused `402` before any session opens; the same key with room is served. -//! 2. THE COUNTS ARE THE PLANE'S. A turn of the session the route opened lands on the presenting key's -//! ledger as the streaming plane's classes, under the voice lane. - -use crate::ir::codec::WireEvent; -use crate::mount::{open_governed, GovernedOpen, Ingress}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use busbar_kernel::plane_host::EngineHost; -use std::sync::Arc; - -fn key() -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: "vk-voice-session".to_string(), - name: "streaming-session".to_string(), - ..Default::default() - } -} - -/// The runtime every test here opens through, over `engine`. -fn runtime(engine: Arc) -> VoiceRuntime { - VoiceRuntime::new(engine, Arc::new(EchoToolExecutor)) -} - -async fn open(host: &Arc, rt: &VoiceRuntime, call_id: &str) -> axum::http::StatusCode { - open_governed(GovernedOpen { - rt, - host: Arc::clone(host) as Arc, - provider: None, - ingress: Ingress::Mint, - owner: "acct-meter".to_string(), - call_id: call_id.to_string(), - vkey: Some(key()), - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 5, - }) - .await - .status() -} - -#[tokio::test] -async fn a_served_open_is_refused_when_the_kernel_reads_the_chain_dry() { - let rt = runtime(Arc::new(DurableHandleEngine::new())); - let dry = Arc::new(FixtureHost::new().governed().with_count_cap(0)); - assert_eq!( - open(&dry, &rt, "call-dry").await, - axum::http::StatusCode::PAYMENT_REQUIRED, - "a dry chain is refused before any session opens" - ); - let room = Arc::new(FixtureHost::new().governed().with_count_cap(1_000)); - assert_eq!( - open(&room, &rt, "call-room").await, - axum::http::StatusCode::NOT_IMPLEMENTED, - "the same key with room is served (nothing to dial here)" - ); -} - -#[tokio::test] -async fn a_served_sessions_turn_lands_the_planes_counts_on_the_presenting_key() { - let host = Arc::new(FixtureHost::new().governed()); - let rt = crate::runtime::build_runtime_hosted( - &runtime(Arc::new(DurableHandleEngine::new())), - Arc::clone(&host) as Arc, - ); - let meter = crate::runtime::TurnMeter::new( - Arc::clone(&host) as Arc, - key(), - "streaming-server", - crate::OPENAI_REALTIME, - ); - let (core, _handle) = crate::topology::begin_session( - &rt, - crate::ir::codec::OpenAiRealtimeCodec, - "acct-meter", - "call-turn", - None, - crate::runtime::Carrier::sideband(), - Some(meter), - 5, - ) - .expect("the session opens"); - let done = serde_json::json!({ - "type": "response.done", - "response": { "usage": { - "input_token_details": { "audio_tokens": 120 }, - "output_token_details": { "audio_tokens": 80 }, - }}, - }); - let _ = core - .on_server_frame(WireEvent(bytes::Bytes::from( - serde_json::to_vec(&done).unwrap(), - ))) - .await; - let rows = host.ledger_rows(&key().id); - let lane = "streaming\u{1f}openai_realtime".to_string(); - assert_eq!( - rows.get(&(lane.clone(), "audio_tokens_in".to_string())), - Some(&120) - ); - assert_eq!(rows.get(&(lane, "audio_tokens_out".to_string())), Some(&80)); - assert_eq!(rows.len(), 2, "only the classes the turn carried"); -} - -/// #47 `streams.fees.per_session` (OWNER RULING Q32): each session the kernel's account opens and -/// serves keeps ONE session on the presenting key's budget book — through the one metering path, a -/// count and never a figure — and a refused open counts none. An open the route could not serve (no -/// provider composed here, so the mint answers `501`) is a failed open: its fee is given back (TODO -/// 17(b)), so the book keeps none for it. What a session costs is the kernel's read of the plane's -/// fees. -#[tokio::test] -async fn each_served_session_keeps_one_session_and_a_failed_or_refused_open_keeps_none() { - let rt = runtime(Arc::new(DurableHandleEngine::new())); - let sessions = |host: &FixtureHost| host.ledger_usage(&key().id).map_or(0, |u| u.sessions); - let room = Arc::new(FixtureHost::new().governed().with_count_cap(1_000)); - open(&room, &rt, "call-one").await; - assert_eq!( - sessions(&room), - 0, - "a mint with no provider served nothing, so its fee is back" - ); - let hosted = - crate::runtime::build_runtime_hosted(&rt, Arc::clone(&room) as Arc); - for call in ["call-two", "call-three"] { - let meter = crate::runtime::TurnMeter::new( - Arc::clone(&room) as Arc, - key(), - "streaming-server", - crate::OPENAI_REALTIME, - ); - let (core, _handle) = crate::topology::begin_session( - &hosted, - crate::ir::codec::OpenAiRealtimeCodec, - "acct-meter", - call, - None, - crate::runtime::Carrier::sideband(), - Some(meter), - 5, - ) - .expect("the session opens"); - crate::runtime::session::serve_with_sweep(core, async {}).await; - } - assert_eq!(sessions(&room), 2, "two served sessions, two counts"); - let dry = Arc::new(FixtureHost::new().governed().with_count_cap(0)); - open(&dry, &rt, "call-dry").await; - assert_eq!(sessions(&dry), 0, "a refused open counts nothing"); -} - -/// TODO 17(b) (ARCHITECT R4): a session whose durable open fails never opened, so it keeps no -/// `fees.per_session`. The kernel's account counted the fee AT THE OPEN, under the same dry check and -/// before the durable open; the failed durable open gives that count back on the budget book, exactly -/// once, and the metering row keeps it. A kept session served beforehand on the same key proves the -/// refund takes back one fee and not two. RED with the fee counted at `served()`: the open counted -/// nothing, so the metering row read the kept session alone. -#[tokio::test] -async fn a_failed_durable_open_gives_back_its_session_fee() { - let host = Arc::new(FixtureHost::new().governed().with_count_cap(1_000)); - let healthy = crate::runtime::build_runtime_hosted( - &runtime(Arc::new(DurableHandleEngine::new())), - Arc::clone(&host) as Arc, - ); - let meter = crate::runtime::TurnMeter::new( - Arc::clone(&host) as Arc, - key(), - "streaming-server", - crate::OPENAI_REALTIME, - ); - let (core, handle) = crate::topology::begin_session( - &healthy, - crate::ir::codec::OpenAiRealtimeCodec, - "acct-meter", - "call-kept", - None, - crate::runtime::Carrier::sideband(), - Some(meter), - 5, - ) - .expect("the kept session opens"); - crate::runtime::session::serve_with_sweep(core, async {}).await; - handle.finish(5); - - let engine = Arc::new(DurableHandleEngine::new()); - engine.set_sink(Arc::new(super::mount_tests::MemStore::down())); - let rt = runtime(engine); - let status = open(&host, &rt, "call-down").await; - assert!( - status.is_server_error(), - "a durable open that cannot land is refused, got {status}" - ); - assert_eq!( - host.session_rows(&key().id), - 2, - "the failed open counted its session fee at the open, under the dry check" - ); - assert_eq!( - host.ledger_usage(&key().id).map_or(0, |u| u.sessions), - 1, - "the failed durable open's fee is given back exactly once; the kept session's stays" - ); -} diff --git a/crates/busbar-voice/src/tests/metrics_tests.rs b/crates/busbar-voice/src/tests/metrics_tests.rs deleted file mode 100644 index 071e76be62..0000000000 --- a/crates/busbar-voice/src/tests/metrics_tests.rs +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE FRONT-DOOR SESSION-OPEN REPORT — the `metrics × voice-server` cell (item 566). A governed -//! session-open is reported through the host's `request_finished` seam with the plane's own key, its -//! ingress dialect, the front-door pool and its outcome — AND the elapsed duration. On the engine's host -//! that call lands the `busbar_plane_requests_total{plane="voice"}` count (the same family and labels -//! this door used to emit by hand) and the `busbar_plane_request_duration_seconds{plane="voice"}` sample -//! the hand emit never recorded; the fixture host records the call itself. Revert `finish` to the -//! hand-emitted counter and the host is told nothing — the assertion fails. (The name is the one -//! `qa/capability-equality.json` cites for this cell; the report carries the count and the duration.) - -use crate::mount::{open_governed, GovernedOpen, Ingress}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use std::sync::Arc; - -#[tokio::test] -async fn a_voice_session_open_increments_the_plane_labelled_counter() { - let host = Arc::new(FixtureHost::new()); - let rt = VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ); - - // A governed session-open through the front door (no provider configured ⇒ the WS-accept leg - // answers 501, but the front-door request is REPORTED all the same). - let resp = open_governed(GovernedOpen { - rt: &rt, - host: Arc::clone(&host) as Arc, - provider: None, - ingress: Ingress::Sideband, - owner: "acct".to_string(), - call_id: "call-metrics".to_string(), - vkey: None, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 1, - }) - .await; - assert_eq!(resp.status(), axum::http::StatusCode::NOT_IMPLEMENTED); - - let reported = host.finished_requests(); - assert_eq!(reported.len(), 1, "one front-door request, reported once"); - let r = &reported[0]; - assert_eq!( - ( - r.plane.as_str(), - r.ingress_protocol.as_str(), - r.pool.as_str() - ), - (crate::PLANE_KEY, crate::OPENAI_REALTIME, "streaming-server"), - "the labels the hand-emitted counter carried" - ); - assert_eq!( - r.outcome, - busbar_kernel::telemetry::outcome_of(501), - "the outcome of the answer the caller got" - ); - assert!( - r.seconds.is_finite() && r.seconds >= 0.0, - "a duration sample rides the same report: {}", - r.seconds - ); -} diff --git a/crates/busbar-voice/src/tests/mint_tests.rs b/crates/busbar-voice/src/tests/mint_tests.rs deleted file mode 100644 index 385359c98f..0000000000 --- a/crates/busbar-voice/src/tests/mint_tests.rs +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE EPHEMERAL-SECRET MINT, SERVED — the browser `ek_` pass with a composed provider credential. -//! -//! Two halves, both load-bearing: -//! -//! 1. THE COMPOSITION SEAM. The composition root hands the plane a provider ORIGIN and the secret -//! REFERENCE the deployment already declared for that provider, plus the deployment's own secret -//! resolver. The plane resolves the reference through that seam and composes the endpoint — so a -//! deployment's realtime credential arrives the same way every other provider key does, and the -//! `streams:` grammar gains no credential field. -//! 2. THE SERVED MINT. With a provider composed, the mint pass no longer answers "no provider -//! composed": it dials the provider's client-secrets endpoint under busbar's OWN key and answers -//! `200` with the browser-facing ephemeral token shape (`value` + `expires_at_unix`). -//! -//! RED before the wiring: nothing composed a provider, so the mint route answered `501` on every -//! deployment and the real key never had a way in. - -use crate::mount::{ - compose_provider, composed_provider_base_url, open_governed, provider_composed, GovernedOpen, - Ingress, ProviderEndpoint, -}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use std::sync::{Arc, Mutex}; - -/// The provider key busbar holds server-side — the value the loopback provider must be dialed with. -const PROVIDER_KEY: &str = "sk-realtime-key-held-server-side"; -/// The browser-facing ephemeral secret the provider mints back. -const EK_VALUE: &str = "ek_browser_secret_2f4c"; -/// The absolute expiry the provider stamps on that secret. -const EK_EXPIRES_AT: u64 = 1_780_000_600; - -/// A loopback "provider" for `POST /v1/realtime/client_secrets`: it RECORDS the `Authorization` it was -/// dialed with and answers the provider's own client-secret document. -async fn spawn_client_secrets_provider(seen: Arc>>) -> std::net::SocketAddr { - async fn client_secrets( - axum::extract::State(seen): axum::extract::State>>>, - headers: axum::http::HeaderMap, - ) -> axum::response::Response { - *seen.lock().unwrap() = headers - .get(http::header::AUTHORIZATION) - .and_then(|v| v.to_str().ok()) - .map(str::to_owned); - axum::response::Response::builder() - .status(axum::http::StatusCode::OK) - .header(http::header::CONTENT_TYPE, "application/json") - .body(axum::body::Body::from( - serde_json::json!({ "value": EK_VALUE, "expires_at": EK_EXPIRES_AT }).to_string(), - )) - .unwrap() - } - let app = axum::Router::new() - .route( - "/v1/realtime/client_secrets", - axum::routing::post(client_secrets), - ) - .with_state(seen); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - addr -} - -/// A stand-in for the deployment's secret resolver: it answers one reference with one credential and -/// refuses everything else, so the test can tell "resolved through the seam" from "guessed". -struct OneSecretResolver { - expect: busbar_contract::secret_ref::SecretRef, - value: String, -} - -impl busbar_contract::secret::SecretResolve for OneSecretResolver { - fn resolve(&self, secret: &busbar_contract::secret_ref::SecretRef) -> Result, String> { - self.resolve_string(secret).map(String::into_bytes) - } - fn resolve_string( - &self, - secret: &busbar_contract::secret_ref::SecretRef, - ) -> Result { - if secret == &self.expect { - Ok(self.value.clone()) - } else { - Err("no such secret reference in this deployment".to_string()) - } - } -} - -fn runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ) -} - -#[tokio::test] -async fn a_composed_provider_credential_makes_the_mint_pass_serve_the_browser_token() { - let seen_auth: Arc>> = Arc::new(Mutex::new(None)); - let addr = spawn_client_secrets_provider(Arc::clone(&seen_auth)).await; - let host = FixtureHost::new().into_host(); - let rt = runtime(); - let provider = ProviderEndpoint { - base_url: format!("http://{addr}"), - api_key: busbar_contract::Redacted::new(PROVIDER_KEY.to_string()), - }; - - let resp = open_governed(GovernedOpen { - rt: &rt, - host, - provider: Some(&provider), - ingress: Ingress::Mint, - owner: "acct-mint".to_string(), - call_id: "call-mint".to_string(), - vkey: None, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 7, - }) - .await; - - assert_eq!( - resp.status(), - axum::http::StatusCode::OK, - "with a provider credential composed, the mint pass serves rather than reporting no provider" - ); - // The browser-facing payload: the ephemeral secret and its absolute expiry, and NOT the real key. - let body = axum::body::to_bytes(resp.into_body(), 64 * 1024) - .await - .unwrap(); - let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); - assert_eq!(json["value"], EK_VALUE, "the browser gets the ek_ secret"); - assert_eq!( - json["expires_at_unix"], EK_EXPIRES_AT, - "the browser gets the secret's absolute expiry" - ); - assert!( - !String::from_utf8_lossy(&body).contains(PROVIDER_KEY), - "the real provider key must never appear in the browser payload" - ); - // The provider hop authenticated with busbar's OWN key, held server-side. - assert_eq!( - seen_auth.lock().unwrap().as_deref(), - Some(format!("Bearer {PROVIDER_KEY}").as_str()), - "the mint dials the provider under busbar's own credential" - ); -} - -#[test] -fn the_composition_root_composes_the_provider_through_the_deployments_secret_resolver() { - let reference = busbar_contract::secret_ref::SecretRef::env("REALTIME_PROVIDER_KEY"); - let resolver = OneSecretResolver { - expect: reference.clone(), - value: PROVIDER_KEY.to_string(), - }; - - // A reference this deployment does NOT declare fails closed with the resolver's own message, and - // composes nothing — an unresolvable credential must never become an empty one. - let unknown = busbar_contract::secret_ref::SecretRef::env("NOT_DECLARED_HERE"); - assert!( - compose_provider("https://api.example.com", &unknown, &resolver).is_err(), - "an unresolvable reference composes nothing" - ); - assert!( - !provider_composed(), - "a failed resolve leaves the plane with no provider" - ); - - // The declared reference resolves through the seam and composes the endpoint the mint / SDP - // passes read. - assert_eq!( - compose_provider("https://api.example.com", &reference, &resolver), - Ok(true), - "the declared provider credential composes" - ); - assert!( - provider_composed(), - "the mint / SDP passes now have an endpoint" - ); - assert_eq!( - composed_provider_base_url(), - Some("https://api.example.com"), - "the composed origin is the one the deployment declared" - ); - - // Set-once: a second compose does not silently swap the deployment's credential. - assert_eq!( - compose_provider("https://other.example.com", &reference, &resolver), - Ok(false), - "the first composed endpoint stands" - ); - assert_eq!( - composed_provider_base_url(), - Some("https://api.example.com"), - "a second compose leaves the first endpoint in place" - ); -} diff --git a/crates/busbar-voice/src/tests/mount_tests.rs b/crates/busbar-voice/src/tests/mount_tests.rs deleted file mode 100644 index 492b584016..0000000000 --- a/crates/busbar-voice/src/tests/mount_tests.rs +++ /dev/null @@ -1,757 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! MOUNT TESTS (behind `runtime`): the voice plane's data-route mount is STRUCTURAL — the four routes -//! MOUNT, the claim + admission BIND the plane's RFC 8707 audience from `public_url`, and a route's -//! arrival runs the governed session-open through `run_gauntlet_session` (verify-before-charge). No -//! live provider is called: a clean open answers `501` -//! (governed, but the live serving leg is the deployment's to compose). - -use super::{ - voice_admission, voice_build, voice_claims, voice_hydrate, voice_routes, voice_start, - MOUNT_PATH, -}; -use crate::ir::codec::OpenAiRealtimeCodec; -// Test-support-only: the governed-open battery (`governed_open` + its gauntlet-open test) drives -// `open_governed` over `Ingress`; both are used ONLY under `#[cfg(feature = "test-support")]`, so gate -// the imports to keep a `runtime`-without-`test-support` build (the workspace clippy default now that -// voice ships default-on) clean. -#[cfg(feature = "test-support")] -use super::Ingress; -#[cfg(feature = "test-support")] -use crate::mount::open_governed; -use crate::runtime::scope::rehydrate_sessions; -use crate::runtime::{EchoToolExecutor, SessionHandle, VoiceRuntime}; -use crate::topology::telephony::{begin_telephony, g711_config}; -use busbar_contract::abi::mechanism::route::{RouteAuth, RouteMethod}; -use busbar_contract::records::{ - PlaneRecord, PlaneRecordRef, PlaneSelector, RecordStoreError, RecordStoreResult, -}; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use busbar_kernel::plane::registry::{BuildCtx, CardIssuer, PlaneBootCtx}; -use busbar_kernel::plane::store::PlaneStore; -use busbar_kernel::plane_host::EngineHost; -use futures::channel::mpsc::unbounded; -use futures::StreamExt; -use std::sync::{Arc, Mutex}; - -const PUBLIC_URL: &str = "https://gw.example.com"; - -/// A minimal in-memory [`PlaneStore`] — the durable sink stand-in a boot rehydrate reads back. Only the -/// row upsert + `All` list are backed; the rest are the neutral no-ops a session restore never reaches. -/// A store built [`MemStore::down`] refuses every row upsert, so a session's durable open fails. -#[derive(Default)] -pub(super) struct MemStore { - rows: Mutex>, - down: bool, -} - -impl MemStore { - /// A store whose every row upsert fails. - pub(super) fn down() -> Self { - MemStore { - down: true, - ..MemStore::default() - } - } -} - -impl PlaneStore for MemStore { - fn upsert_plane_record(&self, record: PlaneRecordRef<'_>) -> RecordStoreResult<()> { - let record = &record.to_record(); - if self.down { - return Err(RecordStoreError("store down".to_string())); - } - let mut rows = self.rows.lock().unwrap(); - if let Some(existing) = rows.iter_mut().find(|r| r.id == record.id) { - *existing = record.clone(); - } else { - rows.push(record.clone()); - } - Ok(()) - } - fn get_plane_record(&self, _kind: &str, id: &str) -> RecordStoreResult>> { - Ok(self - .rows - .lock() - .unwrap() - .iter() - .find(|r| r.id == id) - .map(|r| r.body.clone())) - } - fn append_plane_record(&self, _record: PlaneRecordRef<'_>) -> RecordStoreResult<()> { - Ok(()) - } - fn list_plane_records( - &self, - _kind: &str, - selector: &PlaneSelector, - ) -> RecordStoreResult>> { - match selector { - PlaneSelector::All => Ok(self - .rows - .lock() - .unwrap() - .iter() - .map(|r| r.body.clone()) - .collect()), - PlaneSelector::Parent(_) => Ok(Vec::new()), - } - } - fn list_plane_record_parents(&self, _kind: &str) -> RecordStoreResult> { - Ok(Vec::new()) - } - fn purge_plane_records_before(&self, _kind: &str, _before: u64) -> RecordStoreResult { - Ok(0) - } - fn delete_plane_record(&self, _kind: &str, _id: &str) -> RecordStoreResult<()> { - Ok(()) - } - fn redeem_plane_token( - &self, - _kind: &str, - _token: &str, - _expires_at: u64, - _now: u64, - ) -> RecordStoreResult { - Ok(true) - } - /// The multi-use capability check. `false` — the fail-closed direction the neutral trait - /// defaults to — because this fixture keeps no capability rows to be live. - fn plane_token_live( - &self, - _kind: &str, - _token: &str, - _expires_at: u64, - _now: u64, - ) -> RecordStoreResult { - Ok(false) - } -} - -/// A minimal [`PlaneBootCtx`] carrying only the plane-narrowed store: enough to drive the voice boot -/// hooks, which read `plane_store()` and nothing else. The methods a voice hook never calls -/// (`engine_host`, the MCP call-log surface, `card_issuer`) are inert stand-ins. -struct FakeBootCtx { - store: Option>, -} - -impl PlaneBootCtx for FakeBootCtx { - fn has_store(&self) -> bool { - self.store.is_some() - } - fn attach_durable_sinks(&self) {} - fn plane_store(&self) -> Option> { - self.store.clone() - } - fn card_issuer(&self) -> Option { - None - } - fn engine_host(&self) -> Arc { - unimplemented!("the voice boot hooks never mint the engine host") - } - fn as_any(&self) -> &dyn std::any::Any { - self - } -} - -/// Build the voice dispatch slot the way `appbuild` does — a `BuildCtx` carrying the deployment's -/// `public_url`. The other `BuildCtx` fields are the neutral absences the voice plane never reads. -fn slot_from_public_url(public_url: Option<&str>) -> Option> { - let unit = (); - let ctx = BuildCtx { - endpoint_slot: None, - agent_defs: &unit, - tool_defs: &unit, - public_url, - prior: None, - providers: None, - }; - voice_build(&ctx) -} - -/// A session runtime with no metered caller behind it — used to drive -/// `open_governed` without any provider. `model` seeds the gauntlet destination. -fn runtime_for(model: &str) -> VoiceRuntime { - let mut rt = VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ); - rt.session_defaults.model = Some(model.to_string()); - rt -} - -#[test] -fn build_binds_the_audience_from_public_url_and_none_without() { - // No `public_url` ⇒ no receiving side ⇒ no slot, no claim, no admission (delegation-only asymmetry). - assert!( - slot_from_public_url(None).is_none(), - "no public_url ⇒ the plane fronts nothing and binds no audience" - ); - - let slot = slot_from_public_url(Some(PUBLIC_URL)).expect("a public_url ⇒ a dispatch slot"); - - // The claim is the ONE audience-checked base every voice route sits under, spoken in the first - // dialect — so `/v1/realtime/*` is audience-checked by segment-boundary match (R1's invariant). - // K4: a SECOND claim for the Gemini Live route, under its own dialect label — the A2A precedent of - // more than one `(path, wire)` pair per plane, so the Gemini leg's traffic is never mislabelled - // under the OpenAI constant. - let claims = voice_claims(slot.as_ref()); - assert_eq!( - claims, - vec![ - (MOUNT_PATH.to_string(), crate::OPENAI_REALTIME), - ("/v1/realtime/gemini".to_string(), crate::GEMINI_LIVE), - ], - "the plane claims one audience-checked base per dialect route" - ); - - // The admission BINDS the audience derived from `public_url` — the confused-deputy defence: a token - // minted for another resource is refused here (R2: a claim without an admission refuses boot). - let admission = - voice_admission(slot.as_ref()).expect("a claimed plane must admit (mounted ⇒ admitted)"); - assert_eq!( - admission.audience, - format!("{PUBLIC_URL}/v1/realtime"), - "the audience is one reading of public_url + the voice resource path" - ); - assert_eq!( - admission.resource_metadata, - format!("{PUBLIC_URL}/.well-known/oauth-protected-resource/v1/realtime"), - "the refused-caller metadata URL is the same reading of public_url" - ); -} - -#[test] -fn the_five_ingress_doors_mount_audience_checked_across_the_http_and_ws_seams() { - let slot = slot_from_public_url(Some(PUBLIC_URL)).expect("a public_url ⇒ a dispatch slot"); - - // The TWO one-shot HTTP doors ride the buffered-body `routes` seam: ek_ mint + SDP broker. - let http: Vec<(String, RouteMethod, RouteAuth)> = voice_routes(slot.as_ref()) - .into_iter() - .map(|r| (r.path, r.method, r.auth)) - .collect(); - assert_eq!( - http, - vec![ - ( - "/v1/realtime/client_secrets".to_string(), - RouteMethod::Post, - RouteAuth::Key - ), - ( - "/v1/realtime/calls".to_string(), - RouteMethod::Post, - RouteAuth::Key - ), - ], - "the two one-shot HTTP doors mount, each RouteAuth::Key behind the plane's one audience" - ); - - // The TWO inbound WS-accept doors ride the neutral WS-accept seam instead (an upgrade cannot ride - // the buffered-body adapter): sideband + telephony, SAME RouteAuth::Key under the same audience, - // keyed to the plane's decl slot so the core mount resolves the live runtime. - let ws: Vec<(String, RouteAuth, &'static str)> = crate::mount::voice_ws_arrivals() - .into_iter() - .map(|a| (a.path, a.auth, a.slot_key)) - .collect(); - assert_eq!( - ws, - vec![ - ( - "/v1/realtime/sideband/{call_id}".to_string(), - RouteAuth::Key, - crate::PLANE_DECLARATION.key - ), - ( - "/v1/realtime/telephony/{call_id}".to_string(), - RouteAuth::Key, - crate::PLANE_DECLARATION.key - ), - ( - "/v1/realtime/gemini/{call_id}".to_string(), - RouteAuth::Key, - crate::PLANE_DECLARATION.key - ), - ], - "the THREE WS-accept doors declare through the neutral seam, RouteAuth::Key under the plane's key" - ); - - // No receiving side ⇒ no HTTP routes, exactly as it claims and admits nothing. - assert!( - voice_routes(&()).is_empty(), - "a slot that is not a VoiceMount mounts no routes" - ); -} - -/// One `GovernedOpen` over a real host double, no provider configured — the shape the route handler -/// builds. Gated on `test-support` (the host double needs a bare app). -#[cfg(feature = "test-support")] -fn governed_open<'a>( - rt: &'a VoiceRuntime, - host: Arc, - ingress: Ingress, - call_id: &str, -) -> crate::mount::GovernedOpen<'a> { - crate::mount::GovernedOpen { - rt, - host, - provider: None, - ingress, - owner: "acct".to_string(), - call_id: call_id.to_string(), - vkey: None, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 1, - } -} - -#[cfg(feature = "test-support")] -#[tokio::test] -async fn arrival_passes_the_gauntlet_and_opens_the_governed_session() { - let host = crate::testkit::fixture_host::FixtureHost::new().into_host(); - // A destination proceeds PAST the open-pass gate and opens the governed session; with no provider - // configured the one-shot mint/SDP passes answer 501 (governed, uncomposed). Only Mint/Sdp route - // through `open_governed`; the Sideband/Telephony WS legs route through `ws_accept` (the inbound - // WS-accept seam) — their governed open + operator-gate screening is proven by - // `hook_gate_tests::a_reject_all_operator_gate_refuses_a_ws_accept_before_the_upgrade` and their - // route mounting by `the_five_ingress_doors_mount_audience_checked_across_the_http_and_ws_seams`. - let allowed = runtime_for("allowed-model"); - for ingress in [Ingress::Mint, Ingress::Sdp] { - let opened = open_governed(governed_open( - &allowed, - Arc::clone(&host), - ingress, - "call-ok", - )) - .await; - assert_eq!( - opened.status(), - axum::http::StatusCode::NOT_IMPLEMENTED, - "{ingress:?}: the governed open succeeds; the live provider/media leg is uncomposed here" - ); - } -} - -#[test] -fn hydrate_is_a_noop_under_the_ephemeral_posture_and_start_confirms_ready() { - // No configured store (the in-process posture this mount runs): the boot rehydrate has no durable - // working-set to restore, so it skips cleanly — exactly the first move the A2A/MCP hydrate makes. - let ctx = FakeBootCtx { store: None }; - assert!( - voice_hydrate(&ctx).is_ok(), - "hydrate under no store is a clean no-op" - ); - assert!(voice_start(&ctx).is_ok(), "start confirms readiness"); -} - -#[test] -fn hydrate_rehydrates_the_durable_session_working_set() { - // Persist two sessions through an engine with the store attached as its durable sink: one left - // ACTIVE, one driven TERMINAL. A boot rehydrate must restore the active one and count the terminal. - let store: Arc = Arc::new(MemStore::default()); - let engine = Arc::new(DurableHandleEngine::new()); - engine.set_sink(store.clone()); - - let active = SessionHandle::bind(Arc::clone(&engine), "acct", "sess-active"); - active.open(10).expect("active session opens durably"); - - let terminal = SessionHandle::bind(Arc::clone(&engine), "acct", "sess-terminal"); - terminal.open(10).expect("terminal session opens durably"); - terminal - .settle_terminal(11) - .expect("the session drives terminal"); - - // A FRESH engine restores purely from the durable store — the boot path a restart takes. - let restored = Arc::new(DurableHandleEngine::new()); - let counts = rehydrate_sessions(&restored, store.as_ref()).expect("rehydrate reads the store"); - assert_eq!(counts.active, 1, "the active session is restored"); - assert_eq!( - counts.terminal, 1, - "the terminal session is counted, not restored" - ); - assert_eq!(counts.unreadable, 0, "every durable row decoded"); - - // The restored active handle is readable by its owner — the durable binding survived the restart. - let reattached = SessionHandle::bind(restored, "acct", "sess-active"); - assert_eq!( - reattached.get().map(|r| r.id), - Some("sess-active".to_string()), - "the restored session reattaches by (owner, id)" - ); - - // And the hydrate HOOK drives the same restore off the boot store without error. - assert!( - voice_hydrate(&FakeBootCtx { store: Some(store) }).is_ok(), - "the hydrate hook restores the durable working-set off the boot store" - ); -} - -#[tokio::test] -async fn duplex_session_runs_in_process_through_the_gauntlet_after_hydrate() { - // (1) HYDRATE first, before any listener — the ephemeral no-op gate. - assert!(voice_hydrate(&FakeBootCtx { store: None }).is_ok()); - - // (2) ARRIVAL: begin_telephony opens the session THROUGH `run_gauntlet_session` (verify strictly - // before the kernel account's budget check). g711 carries no model, so the destination is unset and admitted. - let rt = runtime_for(""); - let proxy = begin_telephony( - &rt, - OpenAiRealtimeCodec, - "acct", - "call-x", - g711_config(), - None, - 1, - ) - .expect("the open-pass gauntlet admits and the session opens"); - - // (3) HANDLER: drive the session over the neutral pump with an in-process MOCK PEER — four - // in-memory channels stand in for the provider socket and the client socket. No live provider. - let (prov_in_tx, prov_in_rx) = unbounded::>(); - let (prov_out_tx, mut prov_out_rx) = unbounded::>(); - let (cli_in_tx, cli_in_rx) = unbounded::>(); - let (cli_out_tx, mut cli_out_rx) = unbounded::>(); - - prov_in_tx - .unbounded_send( - serde_json::to_vec(&serde_json::json!({ - "type":"response.output_audio.delta","delta":"AAAA" - })) - .unwrap(), - ) - .unwrap(); - cli_in_tx - .unbounded_send( - serde_json::to_vec(&serde_json::json!({ - "type":"input_audio_buffer.append","audio":"BBBB" - })) - .unwrap(), - ) - .unwrap(); - drop(prov_in_tx); - drop(cli_in_tx); - - proxy - .run(prov_in_rx, prov_out_tx, cli_in_rx, cli_out_tx) - .await; - - // Downlink: the provider's audio reached the client. Uplink: the client's audio reached the provider. - cli_out_rx.close(); - let mut downlink = Vec::new(); - while let Some(f) = cli_out_rx.next().await { - let v: serde_json::Value = serde_json::from_slice(&f).unwrap(); - downlink.push(v["type"].as_str().unwrap().to_string()); - } - assert!( - downlink.contains(&"response.output_audio.delta".to_string()), - "the governed session relayed provider downlink audio to the client: {downlink:?}" - ); - - prov_out_rx.close(); - let mut uplink = Vec::new(); - while let Some(f) = prov_out_rx.next().await { - let v: serde_json::Value = serde_json::from_slice(&f).unwrap(); - uplink.push(v["type"].as_str().unwrap().to_string()); - } - assert!( - uplink.contains(&"input_audio_buffer.append".to_string()), - "the governed session relayed client uplink audio to the provider: {uplink:?}" - ); -} - -/// #71 EXIT TEST (money-model integrity, P2-voicefix, architect ruling): a plane's DECLARED -/// BILLABLE CLASSES must be pairwise DISJOINT, because money = Σ count(class) × rate(class) only -/// holds when no class is a subset of another. The upstream `cached_tokens` figure both duplex -/// dialects report is a SUBSET of the input token classes (nested inside -/// `input_token_details`/`promptTokensDetails`, never a sibling count), so it must not appear in -/// `PLANE_DECLARATION.billable_classes` — a streams card that priced it too would double-bill the same -/// cached input. This asserts the declared list excludes it and stays pairwise distinct. -#[test] -fn billable_classes_are_pairwise_disjoint_and_exclude_cached_tokens() { - let classes: Vec<&str> = crate::PLANE_DECLARATION - .billable_classes - .iter() - .map(|c| c.class) - .collect(); - assert!( - !classes.contains(&"cached_tokens"), - "cached_tokens is a subset of audio_tokens_in/text_tokens_in, not a billable class beside \ - them (#71) — declaring it would double-bill the same cached input; declared classes: \ - {classes:?}" - ); - let mut seen = std::collections::BTreeSet::new(); - for class in &classes { - assert!( - seen.insert(*class), - "declared billable classes must be pairwise disjoint (#71) — {class:?} repeats in \ - {classes:?}" - ); - } -} - -/// The presenting key the session-fee cells below open under. -#[cfg(feature = "test-support")] -fn fee_key() -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: "vk-session-fee".to_string(), - name: "session-fee".to_string(), - ..Default::default() - } -} - -/// The presenting key's meter over `host`, as the governed open builds it. -#[cfg(feature = "test-support")] -fn fee_meter(host: &Arc) -> crate::runtime::TurnMeter { - crate::runtime::TurnMeter::new( - Arc::clone(host) as Arc, - fee_key(), - "streaming-server", - crate::OPENAI_REALTIME, - ) -} - -/// A governed host with room on the key's chain, holding ONE KEPT SESSION: opened and served to its -/// end through the one serving loop, so its session fee is on the budget book and stays there. A -/// failed open's refund that gave back more than its own fee would eat this one. -#[cfg(feature = "test-support")] -async fn host_with_one_kept_session( - rt: &VoiceRuntime, -) -> Arc { - let host = Arc::new( - crate::testkit::fixture_host::FixtureHost::new() - .governed() - .with_count_cap(1_000), - ); - let hosted = crate::runtime::build_runtime_hosted(rt, Arc::clone(&host) as Arc); - let (core, handle) = crate::topology::begin_session( - &hosted, - OpenAiRealtimeCodec, - "acct", - "call-kept", - None, - crate::runtime::Carrier::sideband(), - Some(fee_meter(&host)), - 1, - ) - .expect("the kept session opens"); - crate::runtime::serve_with_sweep(core, async {}).await; - handle.finish(1); - assert_eq!( - host.ledger_usage(&fee_key().id).map_or(0, |u| u.sessions), - 1, - "the premise: the kept session's fee is on the budget book" - ); - host -} - -/// TODO 17(b) (ARCHITECT R4): `failed` opens each counted their session fee AT THE OPEN, under the -/// same dry check, and each gave it back EXACTLY ONCE through the governance refund. The metering -/// row keeps every count (the refund adjusts the budget book only, as v1.5.5's fee refund did); the -/// budget book holds the kept session's fee alone. -#[cfg(feature = "test-support")] -#[track_caller] -fn assert_each_failed_open_refunded_once( - host: &crate::testkit::fixture_host::FixtureHost, - failed: u64, - what: &str, -) { - assert_eq!( - host.session_rows(&fee_key().id), - 1 + failed, - "{what}: each failed open counted its session fee at the open, under the dry check" - ); - assert_eq!( - host.ledger_usage(&fee_key().id).map_or(0, |u| u.sessions), - 1, - "{what}: each failed open's fee is given back exactly once, and the kept session's stays" - ); -} - -/// A loopback provider that FAILS both one-shot passes: `POST /v1/realtime/client_secrets` (the mint) -/// and `POST /v1/realtime/calls` (the SDP broker) answer `500`, and each request reaching it is -/// counted in `hits`, so a cell can prove its failure came from the provider leg and not from an -/// earlier refusal. -#[cfg(feature = "test-support")] -async fn spawn_failing_provider( - hits: Arc, -) -> super::ProviderEndpoint { - async fn fail( - axum::extract::State(hits): axum::extract::State>, - ) -> axum::http::StatusCode { - hits.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - axum::http::StatusCode::INTERNAL_SERVER_ERROR - } - let app = axum::Router::new() - .route("/v1/realtime/client_secrets", axum::routing::post(fail)) - .route("/v1/realtime/calls", axum::routing::post(fail)) - .with_state(hits); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0") - .await - .expect("a loopback port"); - let addr = listener.local_addr().expect("its address"); - tokio::spawn(async move { - axum::serve(listener, app) - .await - .expect("the provider serves"); - }); - super::ProviderEndpoint { - base_url: format!("http://{addr}"), - api_key: "sk-test".to_string().into(), - } -} - -/// One governed one-shot open of `ingress` for the fee key on `host`, through `provider`. -#[cfg(feature = "test-support")] -async fn open_one_shot( - rt: &VoiceRuntime, - host: &Arc, - provider: Option<&super::ProviderEndpoint>, - ingress: Ingress, - call_id: &str, -) -> axum::http::StatusCode { - let mut open = governed_open( - rt, - Arc::clone(host) as Arc, - ingress, - call_id, - ); - open.provider = provider; - open.vkey = Some(fee_key()); - open_governed(open).await.status() -} - -/// A FAILED MINT REFUNDS ITS SESSION FEE EXACTLY ONCE (TODO 17(b)): the provider's mint answers `500`, -/// so the browser is handed no secret and the session never opened. RED with the fee counted at -/// `served()`: the open counted nothing to refund. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn a_failed_mint_refunds_its_session_fee_exactly_once() { - let rt = runtime_for("allowed-model"); - let host = host_with_one_kept_session(&rt).await; - let hits = Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let provider = spawn_failing_provider(Arc::clone(&hits)).await; - let status = open_one_shot( - &rt, - &host, - Some(&provider), - Ingress::Mint, - "call-mint-fails", - ) - .await; - assert_eq!( - status, - axum::http::StatusCode::BAD_GATEWAY, - "the premise: the mint failed at the provider" - ); - assert_eq!( - hits.load(std::sync::atomic::Ordering::SeqCst), - 1, - "the premise: the open reached the provider's mint" - ); - assert_each_failed_open_refunded_once(&host, 1, "a failed mint"); -} - -/// A FAILED SDP BROKER REFUNDS ITS SESSION FEE EXACTLY ONCE (TODO 17(b)): the provider answers the -/// brokered offer `500`, so no call was set up and the session never opened. The cell opens under its -/// own call id and proves the broker was reached, so the failure is the broker's and not a durable -/// refusal of a reused id. RED with the fee counted at `served()`. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn a_failed_sdp_broker_refunds_its_session_fee_exactly_once() { - let rt = runtime_for("allowed-model"); - let host = host_with_one_kept_session(&rt).await; - let hits = Arc::new(std::sync::atomic::AtomicUsize::new(0)); - let provider = spawn_failing_provider(Arc::clone(&hits)).await; - let status = open_one_shot(&rt, &host, Some(&provider), Ingress::Sdp, "call-sdp-fails").await; - assert_eq!( - status, - axum::http::StatusCode::INTERNAL_SERVER_ERROR, - "the premise: the provider refused the brokered offer, and the answer carries its status" - ); - assert_eq!( - hits.load(std::sync::atomic::Ordering::SeqCst), - 1, - "the premise: the open reached the provider's SDP broker" - ); - assert_each_failed_open_refunded_once(&host, 1, "a failed SDP broker"); -} - -/// AN UNREACHABLE PROVIDER REFUNDS THE SESSION FEE ON EACH ONE-SHOT PASS (TODO 17(b)): nothing listens -/// on the discard port, so the mint and the SDP broker each fail at connect and answer `502`. Each pass -/// opens under its own call id, so neither failure is a durable refusal of a reused id. RED with the -/// fee counted at `served()`. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn an_unreachable_provider_refunds_the_session_fee_on_each_one_shot_pass() { - let rt = runtime_for("allowed-model"); - let host = host_with_one_kept_session(&rt).await; - let down = super::ProviderEndpoint { - base_url: "http://127.0.0.1:9".to_string(), - api_key: "sk-test".to_string().into(), - }; - for (ingress, call) in [ - (Ingress::Mint, "call-mint-down"), - (Ingress::Sdp, "call-sdp-down"), - ] { - let status = open_one_shot(&rt, &host, Some(&down), ingress, call).await; - assert_eq!( - status, - axum::http::StatusCode::BAD_GATEWAY, - "{ingress:?}: the premise: the provider leg failed at connect" - ); - } - assert_each_failed_open_refunded_once(&host, 2, "an unreachable provider"); -} - -/// AN OPEN ANSWERED `501` REFUNDS ITS SESSION FEE EXACTLY ONCE (TODO 17(b)): with no provider composed, -/// every HTTP open the governed door answers `501` serves nothing, so it is a failed open: the mint, -/// the SDP broker, the HTTP telephony open and the HTTP sideband open each give their fee back. RED -/// with the fee counted at `served()`. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn an_open_answered_501_refunds_its_session_fee_exactly_once() { - let rt = runtime_for("allowed-model"); - let host = host_with_one_kept_session(&rt).await; - let opens = [ - (Ingress::Mint, "call-mint-501"), - (Ingress::Sdp, "call-sdp-501"), - (Ingress::Telephony, "call-telephony-501"), - (Ingress::Sideband, "call-sideband-501"), - ]; - for (ingress, call) in opens { - let status = open_one_shot(&rt, &host, None, ingress, call).await; - assert_eq!( - status, - axum::http::StatusCode::NOT_IMPLEMENTED, - "{ingress:?}: the premise: the open is governed and nothing serves it" - ); - } - assert_each_failed_open_refunded_once(&host, 4, "an open answered 501"); -} - -/// A FAILED PROVIDER DIAL REFUNDS ITS SESSION FEE EXACTLY ONCE (TODO 17(b)): the WS telephony / Gemini -/// leg opens the session, then dials the provider; a failed dial settles the session through -/// [`super::settle_undialed`], the arm `ws_accept` takes, and never serves it. RED with the fee -/// counted at `served()`. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn a_failed_provider_dial_refunds_its_session_fee_exactly_once() { - let base = runtime_for("allowed-model"); - let host = host_with_one_kept_session(&base).await; - let rt = crate::runtime::build_runtime_hosted(&base, Arc::clone(&host) as Arc); - let proxy = crate::topology::telephony::open_admitted_telephony( - &rt, - OpenAiRealtimeCodec, - "acct", - "call-dial-fails", - g711_config(), - Some(fee_meter(&host)), - 1, - None, - ) - .expect("the session opens"); - super::settle_undialed(proxy, 2); - assert_each_failed_open_refunded_once(&host, 1, "a failed provider dial"); -} diff --git a/crates/busbar-voice/src/tests/scope_gate_tests.rs b/crates/busbar-voice/src/tests/scope_gate_tests.rs deleted file mode 100644 index eebd9cd2ff..0000000000 --- a/crates/busbar-voice/src/tests/scope_gate_tests.rs +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SESSION-SCOPE AUTHORIZATION GATE — the plane's declared `session` scope kind, enforced. -//! -//! The plane declares one scope kind (`PLANE_DECLARATION.scope_kinds`), which is the vocabulary an -//! `allowed_scopes: [{ kind: session, value: … }]` entry validates against. Holding a key that is -//! valid for this plane's AUDIENCE is not the same as being GRANTED a session on it — the audience -//! check answers "is this token for this door", the grant answers "may this caller walk through it", -//! exactly as MCP double-gates a tool and A2A gates an agent. -//! -//! The refusal lands FIRST: before the operator's hook gate fires, before a metering lease is -//! reserved, before a durable session row exists, and before any provider is dialed. So a caller -//! without the grant costs zero bytes and zero charge. -//! -//! RED before the wiring: any key valid for the voice audience opened a session, and the declared -//! scope kind was vocabulary nothing consulted. - -use crate::mount::{open_governed, session_scope_allowed, GovernedOpen, Ingress}; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use std::sync::Arc; - -/// The pool a voice session is served on — the value the `session` grant names. -const VOICE_POOL: &str = "streaming-server"; - -/// A key carrying an EXPLICIT scope list. An explicit list is exhaustive across kinds: whatever is -/// not in it is not granted. -fn key_scoped( - id: &str, - scopes: Vec, -) -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: id.to_string(), - name: id.to_string(), - allowed_scopes: Some(scopes), - ..Default::default() - } -} - -fn session_scope(value: &str) -> busbar_contract::records::ScopeRef { - busbar_contract::records::ScopeRef { - kind: "session".to_string(), - value: value.to_string(), - } -} - -fn runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(EchoToolExecutor), - ) -} - -async fn open_as(key: Option) -> axum::http::StatusCode { - let rt = runtime(); - open_governed(GovernedOpen { - rt: &rt, - host: FixtureHost::new().into_host(), - provider: None, - ingress: Ingress::Mint, - owner: "acct-scope".to_string(), - call_id: "call-scope".to_string(), - vkey: key, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 11, - }) - .await - .status() -} - -#[test] -fn the_grant_is_read_off_the_keys_own_scope_list() { - // A wildcard principal (no scope list at all) is granted every kind — the store's own semantic, - // and the most common key shape in a small deployment. - let wildcard = busbar_contract::records::VirtualKey { - id: "vk-wildcard".to_string(), - ..Default::default() - }; - assert!( - session_scope_allowed(&wildcard), - "a key with no scope list is granted every kind, voice included" - ); - - // An explicit list that names the session scope on the voice pool grants it. - assert!( - session_scope_allowed(&key_scoped("vk-granted", vec![session_scope(VOICE_POOL)])), - "an explicit session grant on the voice pool admits" - ); - - // An explicit list WITHOUT it does not — including one that carries pool scopes (a model-plane - // key) or a session scope for some other pool. - assert!( - !session_scope_allowed(&key_scoped( - "vk-pool-only", - vec![busbar_contract::records::ScopeRef::pool("fast")] - )), - "a model-plane key is not thereby granted a voice session" - ); - assert!( - !session_scope_allowed(&key_scoped( - "vk-other-pool", - vec![session_scope("some-other-pool")] - )), - "a session grant for another pool does not reach the voice pool" - ); - assert!( - !session_scope_allowed(&key_scoped("vk-empty", Vec::new())), - "an empty explicit list grants nothing" - ); -} - -#[tokio::test] -async fn a_key_without_session_scope_is_refused_at_the_door() { - let refused = open_as(Some(key_scoped( - "vk-no-session", - vec![busbar_contract::records::ScopeRef::pool("fast")], - ))) - .await; - assert_eq!( - refused, - axum::http::StatusCode::FORBIDDEN, - "a key holding no session scope is refused with the plane's own fail-closed answer" - ); -} - -#[tokio::test] -async fn a_granted_key_and_an_ungoverned_caller_still_open() { - // The granted key proceeds past the gate into the governed open (which, with no provider - // composed here, reports that there is nothing to dial) — the point is that it got there. - let granted = open_as(Some(key_scoped( - "vk-granted", - vec![session_scope(VOICE_POOL)], - ))) - .await; - assert_eq!( - granted, - axum::http::StatusCode::NOT_IMPLEMENTED, - "a granted key opens the governed session" - ); - - // An ungoverned deployment resolves no key and has no grant to consult, so it is unaffected — - // this gate narrows governed callers only. - assert_eq!( - open_as(None).await, - axum::http::StatusCode::NOT_IMPLEMENTED, - "an ungoverned caller is unchanged by the grant check" - ); -} diff --git a/crates/busbar-voice/src/tests/sdp_tests.rs b/crates/busbar-voice/src/tests/sdp_tests.rs deleted file mode 100644 index a8e25cdc1a..0000000000 --- a/crates/busbar-voice/src/tests/sdp_tests.rs +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SDP BROKER + `rtc_` CORRELATION, end-to-end over a loopback. The browser's SDP offer -//! is brokered to the provider's `POST /v1/realtime/calls`; the provider's `Location: -//! …/rtc_` is PRESERVED on the answer AND stamped onto the durable session row — so -//! busbar's governance (this session) and the brokered media call name the SAME session. A mismatch -//! silently attaches the sideband to the wrong call, so the correlation is asserted broker → row. -//! -//! RED before the wiring: the SDP route answered `501` (no broker), so there was no `Location` to -//! preserve and no `rtc_` on the row. - -use crate::mount::{open_governed, GovernedOpen, Ingress, ProviderEndpoint}; -use crate::runtime::scope::SessionHandle; -use crate::runtime::{EchoToolExecutor, VoiceRuntime}; -use crate::testkit::fixture_host::FixtureHost; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use std::sync::{Arc, Mutex}; - -const RTC_CALL_ID: &str = "rtc_correlated_call_9f8e7d"; -const SDP_ANSWER: &str = "v=0\r\no=- 0 0 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\n"; -const PROVIDER_KEY: &str = "sk-real-key-held-server-side"; -/// The caller's inbound governance bearer — a sentinel that MUST NEVER reach the provider hop. -const INBOUND_GOVERNANCE_BEARER: &str = "Bearer inbound-governance-token-MUST-NOT-LEAK"; - -/// A loopback "provider" for `POST /v1/realtime/calls`: it RECORDS the `Authorization` it was dialed -/// with (into `seen`) and answers `201` carrying the SDP answer and a `Location: -/// /v1/realtime/calls/rtc_` header — the exact broker contract. -async fn spawn_calls_broker(seen: Arc>>) -> std::net::SocketAddr { - async fn calls( - axum::extract::State(seen): axum::extract::State>>>, - headers: axum::http::HeaderMap, - ) -> axum::response::Response { - *seen.lock().unwrap() = headers - .get(http::header::AUTHORIZATION) - .and_then(|v| v.to_str().ok()) - .map(str::to_owned); - axum::response::Response::builder() - .status(axum::http::StatusCode::CREATED) - .header(http::header::CONTENT_TYPE, "application/sdp") - .header( - http::header::LOCATION, - format!("/v1/realtime/calls/{RTC_CALL_ID}"), - ) - .body(axum::body::Body::from(SDP_ANSWER)) - .unwrap() - } - let app = axum::Router::new() - .route("/v1/realtime/calls", axum::routing::post(calls)) - .with_state(seen); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - addr -} - -#[tokio::test] -async fn the_sdp_broker_correlates_the_rtc_call_id_from_the_location_header_onto_the_row() { - let seen_auth: Arc>> = Arc::new(Mutex::new(None)); - let addr = spawn_calls_broker(Arc::clone(&seen_auth)).await; - let host = FixtureHost::new().into_host(); - - let engine = Arc::new(DurableHandleEngine::new()); - let rt = VoiceRuntime::new(Arc::clone(&engine), Arc::new(EchoToolExecutor)); - let provider = ProviderEndpoint { - base_url: format!("http://{addr}"), - api_key: busbar_contract::Redacted::new(PROVIDER_KEY.to_string()), - }; - - // The caller reached the `RouteAuth::Key` SDP route with a GOVERNANCE bearer (a sentinel) + an SDP - // offer. The broker hop must authenticate to the provider with busbar's OWN provider key and NEVER - // forward this inbound token upstream. - let mut headers = axum::http::HeaderMap::new(); - headers.insert( - http::header::AUTHORIZATION, - http::HeaderValue::from_static(INBOUND_GOVERNANCE_BEARER), - ); - - let resp = open_governed(GovernedOpen { - rt: &rt, - host: Arc::clone(&host), - provider: Some(&provider), - ingress: Ingress::Sdp, - owner: "acct-sdp".to_string(), - call_id: "call-sdp".to_string(), - vkey: None, - body: axum::body::Bytes::from_static(b"v=0\r\no=browser 0 0 IN IP4 0.0.0.0\r\n"), - headers, - now: 42, - }) - .await; - - // The answer preserved the provider's `Location` header verbatim. - assert_eq!(resp.status(), axum::http::StatusCode::CREATED); - assert_eq!( - resp.headers() - .get(http::header::LOCATION) - .and_then(|v| v.to_str().ok()), - Some(format!("/v1/realtime/calls/{RTC_CALL_ID}").as_str()), - "the SDP broker preserves the Location header verbatim" - ); - - // CREDENTIAL ISOLATION (regression guard for the forward-the-inbound-bearer leak): the provider hop - // saw busbar's OWN provider key, and the caller's inbound governance bearer NEVER left the boundary. - let dialed_with = seen_auth.lock().unwrap().clone(); - assert_eq!( - dialed_with.as_deref(), - Some(format!("Bearer {PROVIDER_KEY}").as_str()), - "the SDP broker authenticates upstream with busbar's OWN provider credential" - ); - assert_ne!( - dialed_with.as_deref(), - Some(INBOUND_GOVERNANCE_BEARER), - "the caller's inbound governance bearer must NEVER be forwarded to the provider" - ); - - // BROKER → ROW: the `rtc_` derived from that Location is stamped onto the durable session - // row, read back by the session's own (owner, id). - let row = SessionHandle::bind(engine, "acct-sdp", "call-sdp") - .get() - .expect("the durable session row exists after the governed open"); - assert_eq!( - row.rtc_call_id.as_deref(), - Some(RTC_CALL_ID), - "the rtc_ from the broker's Location header correlates onto the session row — the \ - single key tying governance here to the media that flows there" - ); -} diff --git a/crates/busbar-voice/src/tests/served_witness.rs b/crates/busbar-voice/src/tests/served_witness.rs deleted file mode 100644 index 728e5e00c0..0000000000 --- a/crates/busbar-voice/src/tests/served_witness.rs +++ /dev/null @@ -1,674 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THIS PLANE'S SERVED-LEG WITNESSES — a live session opened at the plane's served door, through -//! whatever session runner the host has registered for this plane's capability key, each witness -//! asserting ONE capability or ONE loop step on what came out. -//! -//! ## Why they live here and run elsewhere -//! -//! In a deployment a session is opened by the composition root's kernel-loop session rider: the root -//! registers it under [`crate::PLANE_KEY`] at boot, and every governed open (`begin_session`, the -//! choke point every served door funnels through) asks `run_gauntlet_session`, which hands the open to -//! it. That rider lives in the binary crate, which this crate cannot link, so in THIS crate's own test -//! binary the same open runs `run_gauntlet_session`'s inline fallback. The witnesses are therefore -//! compiled under `test-support` and exported through [`crate::testkit::SERVED`]: the binary crate's -//! test build links this crate, installs its real rider, runs every witness, and checks that this -//! plane's session opens actually ran inside the loop the number of times the witness says they -//! should. The composition root names no plane to do it — it reads the table its build script -//! generated from the manifest. -//! -//! ## What the session leg is, and what it is not -//! -//! The rider carries a session's OPEN: the plane's verify and the door, before any account, durable -//! row or socket exists. Everything after it — the turns, their counts on the kernel's session -//! account, the audit row, the durable chain, the provider dial — is the plane's own served session -//! over the live host (OWNER RULING Q21b: a served session is metered by the kernel's session account, -//! per turn). So each witness opens its session at the served door and then drives the part of that -//! session its capability lives in. -//! -//! ## What a witness returns -//! -//! The number of session opens it expects the served path to have carried through the rider: every -//! open the door handed to the governed open. A caller the door refuses before it (a key with no -//! session grant) is not an open and is not counted — which is exactly what the caller checks. -//! -//! They drive the same host double and the same door the plane's own batteries drive -//! (`FixtureHost`, `open_governed`, `begin_session`), so the served witnesses and the plane's tests -//! cannot drift onto two deployments. - -use crate::ir::codec::{OpenAiRealtimeCodec, WireEvent}; -use crate::ir::config::SessionConfig; -use crate::mount::{open_governed, GovernedOpen, Ingress}; -use crate::runtime::{Carrier, SessionCore, TurnMeter, VoiceRuntime}; -use crate::testkit::fixture_host::FixtureHost; -use crate::topology::telephony::{begin_telephony, g711_config}; -use crate::topology::{begin_session, dial_provider, stream_breaker_key, DialProviderError}; -use futures::channel::mpsc::unbounded; -use futures::StreamExt; -use std::future::Future; -use std::pin::Pin; -use std::sync::Arc; - -/// One served-leg witness: drive the served path, assert, and return the number of session opens the -/// served path must have carried through this plane's registered session runner. -pub type Witness = fn() -> Pin>>; - -/// THE WITNESS TABLE, keyed by the loop step (`qa/teller-steps.json`) or the core capability -/// (`qa/capability-equality.json`) each one proves on the served path. The `session-*` keys are the -/// session plane's own shape of a step or capability whose one-shot witness asks something a session -/// does not do: its caller is attributed rather than re-authenticated per call, its verify admits the -/// destination the session is then metered under, its route is the two-way relay a turn rides, it -/// meters per turn and per declared class, and what it reports is the session it opened. -pub(crate) const WITNESSES: &[(&str, Witness)] = &[ - ("arrival", || { - Box::pin(each_served_open_is_one_unit_reported_once()) - }), - ("decode", || { - Box::pin(a_frame_the_plane_cannot_read_is_carried_nowhere()) - }), - ("session-authenticate", || { - Box::pin(the_session_answers_for_the_key_the_door_resolved()) - }), - ("session-verify", || { - Box::pin(the_destination_the_gate_judged_is_the_one_metered()) - }), - ("approve", || { - Box::pin(a_key_granted_no_session_never_reaches_the_gate()) - }), - ("admit", || { - Box::pin(a_dry_key_is_refused_at_the_open_past_the_gate()) - }), - ("session-route", || { - Box::pin(a_served_session_relays_both_directions()) - }), - ("session-meter", || { - Box::pin(a_served_turn_is_ledgered_per_declared_class()) - }), - ("audit", || Box::pin(a_served_open_lands_one_audit_row())), - ("exit", || { - Box::pin(a_served_session_ends_once_and_settles_its_open_turn()) - }), - ("audit-chain", || { - Box::pin(the_served_sessions_row_seals_at_genesis_and_reads_back()) - }), - ("governance-budget", || { - Box::pin(a_served_session_is_charged_to_the_presenting_key_and_closed_dry()) - }), - ("session-metrics", || { - Box::pin(a_served_open_is_reported_under_the_planes_own_labels()) - }), - ("breaker-fastfail", || { - Box::pin(a_served_sessions_dial_fast_fails_on_a_tripped_cell()) - }), -]; - -// ── FIXTURES ──────────────────────────────────────────────────────────────────────────────────── - -/// The pool a voice session is served on — the value a `session` grant names. -const VOICE_POOL: &str = "streaming-server"; - -/// The model a witness session targets, and so the ledger lane its turns land under. -const MODEL: &str = "witness-realtime"; - -/// The plane's own per-generation runtime (its build hook over no `streams:` section), bound to `host` -/// the way every served door binds it. -fn hosted(host: &Arc) -> VoiceRuntime { - let base = crate::runtime::build_runtime(&(), None) - .downcast::() - .expect("the plane's build hook builds its own runtime"); - crate::runtime::build_runtime_hosted(&base, host.clone()) -} - -/// A caller whose key holds no scope list — granted every kind, a voice session included. -fn caller(id: &str) -> busbar_contract::records::VirtualKey { - busbar_contract::records::VirtualKey { - id: id.to_string(), - name: id.to_string(), - ..Default::default() - } -} - -/// One open at the served door, over `host`, as `vkey` (or ungoverned). -fn served_open<'a>( - rt: &'a VoiceRuntime, - host: &Arc, - call_id: &str, - vkey: Option, -) -> GovernedOpen<'a> { - GovernedOpen { - rt, - host: host.clone(), - provider: None, - ingress: Ingress::Sideband, - owner: "acct-witness".to_string(), - call_id: call_id.to_string(), - vkey, - body: axum::body::Bytes::new(), - headers: axum::http::HeaderMap::new(), - now: 1, - } -} - -/// The governed open the served door runs, keeping the session core and its durable handle so the -/// witness can drive the session the door opened. `key` is the caller the door resolved (the turn -/// meter is that key's), or `None` for an ungoverned caller. -fn open_session( - rt: &VoiceRuntime, - host: &Arc, - call_id: &str, - key: Option<&busbar_contract::records::VirtualKey>, -) -> ( - Arc>, - crate::runtime::SessionHandle, -) { - let meter = - key.map(|k| TurnMeter::new(host.clone(), k.clone(), VOICE_POOL, crate::OPENAI_REALTIME)); - begin_session( - rt, - OpenAiRealtimeCodec, - "acct-witness", - call_id, - Some(SessionConfig { - model: Some(MODEL.to_string()), - ..SessionConfig::default() - }), - Carrier::sideband(), - meter, - 1, - ) - .unwrap_or_else(|e| panic!("the served door opens the session: {e}")) -} - -/// One OpenAI Realtime wire frame. -fn frame(json: serde_json::Value) -> WireEvent { - WireEvent(bytes::Bytes::from( - serde_json::to_vec(&json).expect("a frame serializes"), - )) -} - -/// The upstream's usage report closing one turn: `a_in`/`t_in` audio/text in, `a_out`/`t_out` out. -fn usage(a_in: u64, t_in: u64, a_out: u64, t_out: u64) -> WireEvent { - frame(serde_json::json!({ - "type": "response.done", - "response": { "usage": { - "input_token_details": { "audio_tokens": a_in, "text_tokens": t_in }, - "output_token_details": { "audio_tokens": a_out, "text_tokens": t_out }, - }}, - })) -} - -/// The upstream frames a plan writes, as one string. -fn upstream_text(plan: &crate::runtime::Outbound) -> String { - plan.upstream - .iter() - .map(|w| String::from_utf8_lossy(&w.0).to_string()) - .collect::>() - .join("\n") -} - -/// The ledger lane a witness session's turns land under: this plane's key, the separator, the model. -fn lane() -> String { - format!("streaming\u{1f}{MODEL}") -} - -/// What `key` has ledgered under the witness lane for `class`, or `None`. -fn ledgered(host: &FixtureHost, key: &str, class: &str) -> Option { - host.ledger_rows(key) - .get(&(lane(), class.to_string())) - .copied() -} - -// ── THE WITNESSES ─────────────────────────────────────────────────────────────────────────────── - -/// ARRIVAL: each call at the served door is one unit, and each is reported once. -async fn each_served_open_is_one_unit_reported_once() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - for call in ["call-arrival-1", "call-arrival-2"] { - let resp = open_governed(served_open(&rt, &host, call, None)).await; - assert_eq!( - resp.status(), - axum::http::StatusCode::NOT_IMPLEMENTED, - "{call}: the served open succeeds; the provider leg is uncomposed here" - ); - } - assert_eq!( - host.finished_requests().len(), - 2, - "two calls at the door are two units, each reported exactly once" - ); - 2 -} - -/// DECODE: the plane reads what a served session's client sends, and a frame it cannot read is -/// carried to no upstream at all. -async fn a_frame_the_plane_cannot_read_is_carried_nowhere() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let (core, _handle) = open_session(&rt, &host, "call-decode", None); - - let read = core.on_client_frame(frame(serde_json::json!({ - "type": "input_audio_buffer.append", "audio": "AAAA" - }))); - assert!( - upstream_text(&read).contains("input_audio_buffer.append"), - "a client event the plane reads is carried to the upstream: {}", - upstream_text(&read) - ); - - let unread = core.on_client_frame(WireEvent(bytes::Bytes::from_static(b"not a frame {"))); - assert!( - unread.upstream.is_empty(), - "a frame the plane cannot read reaches the upstream on no wire: {}", - upstream_text(&unread) - ); - 1 -} - -/// AUTHENTICATE: the session answers for the key the door resolved and for nobody else — its turns -/// land on that key's own ledger, and a caller the door resolved no key for is attributed nothing. -async fn the_session_answers_for_the_key_the_door_resolved() -> u64 { - let host = Arc::new(FixtureHost::new().governed()); - let rt = hosted(&host); - let presenting = caller("vk-presenting"); - - let (keyed, _h1) = open_session(&rt, &host, "call-auth-keyed", Some(&presenting)); - let _ = keyed.on_server_frame(usage(0, 0, 7, 0)).await; - assert_eq!( - ledgered(&host, "vk-presenting", "audio_tokens_out"), - Some(7), - "the turn lands on the key the door resolved" - ); - - let (anonymous, _h2) = open_session(&rt, &host, "call-auth-anon", None); - let _ = anonymous.on_server_frame(usage(0, 0, 9, 0)).await; - assert_eq!( - ledgered(&host, "vk-presenting", "audio_tokens_out"), - Some(7), - "a session the door resolved no key for is attributed to nobody, the presenting key included" - ); - assert!( - host.ledger_usage("").is_none() && host.ledger_usage("anonymous").is_none(), - "and no anonymous principal is invented to carry it" - ); - 2 -} - -/// VERIFY (the session plane's shape): the destination the open-pass gate admitted is the destination -/// the session is metered under — the gate and the ledger read one model. -async fn the_destination_the_gate_judged_is_the_one_metered() -> u64 { - let host = Arc::new(FixtureHost::new().governed()); - let rt = hosted(&host); - let key = caller("vk-verify"); - let (core, _handle) = open_session(&rt, &host, "call-verify", Some(&key)); - let _ = core.on_server_frame(usage(0, 0, 5, 0)).await; - let rows = host.ledger_rows("vk-verify"); - assert_eq!( - rows.keys() - .map(|(lane, _)| lane.as_str()) - .collect::>(), - vec![lane().as_str()], - "every count lands under the lane of the one destination the gate judged: {rows:?}" - ); - 1 -} - -/// APPROVE: a key valid for the door but granted no session scope is refused before the gate; a -/// granted key reaches it. -async fn a_key_granted_no_session_never_reaches_the_gate() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let ungranted = busbar_contract::records::VirtualKey { - allowed_scopes: Some(vec![busbar_contract::records::ScopeRef::pool("fast")]), - ..caller("vk-no-session") - }; - let refused = open_governed(served_open(&rt, &host, "call-approve-no", Some(ungranted))).await; - assert_eq!( - refused.status(), - axum::http::StatusCode::FORBIDDEN, - "a key holding no session scope is refused with the plane's own answer" - ); - assert!(host.audit_log().is_empty(), "and nothing was opened for it"); - - let granted = busbar_contract::records::VirtualKey { - allowed_scopes: Some(vec![busbar_contract::records::ScopeRef { - kind: "session".to_string(), - value: VOICE_POOL.to_string(), - }]), - ..caller("vk-session") - }; - let opened = open_governed(served_open(&rt, &host, "call-approve-yes", Some(granted))).await; - assert_eq!( - opened.status(), - axum::http::StatusCode::NOT_IMPLEMENTED, - "a key granted the session reaches the governed open" - ); - 1 -} - -/// ADMIT: a caller whose chain already reads dry is refused at the open — after the gate, and before -/// any durable row or account exists — while a caller with room opens. -async fn a_dry_key_is_refused_at_the_open_past_the_gate() -> u64 { - let host = Arc::new(FixtureHost::new().governed().with_count_cap(10)); - let rt = hosted(&host); - - // Spend the key's chain dry on a first session. - let key = caller("vk-admit"); - let (core, _handle) = open_session(&rt, &host, "call-admit-spend", Some(&key)); - let plan = core.on_server_frame(usage(0, 0, 12, 0)).await; - assert!( - plan.close, - "the turn that dried the chain closes its carrier" - ); - - let refused = open_governed(served_open(&rt, &host, "call-admit-dry", Some(key))).await; - assert_eq!( - refused.status(), - axum::http::StatusCode::PAYMENT_REQUIRED, - "a dry key is refused at the open" - ); - let rows_before = host.audit_log().len(); - let room = open_governed(served_open( - &rt, - &host, - "call-admit-room", - Some(caller("vk-room")), - )) - .await; - assert_eq!( - room.status(), - axum::http::StatusCode::NOT_IMPLEMENTED, - "a key with room opens" - ); - assert_eq!( - host.audit_log().len(), - rows_before + 1, - "only the open that was admitted wrote a row" - ); - 3 -} - -/// ROUTE (the session plane's shape): a served session relays the provider's audio down to the client -/// and the client's audio up to the provider. -async fn a_served_session_relays_both_directions() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let proxy = begin_telephony( - &rt, - OpenAiRealtimeCodec, - "acct-witness", - "call-route", - g711_config(), - None, - 1, - ) - .unwrap_or_else(|e| panic!("the served door opens the telephony session: {e}")); - - let (prov_in_tx, prov_in_rx) = unbounded::>(); - let (prov_out_tx, mut prov_out_rx) = unbounded::>(); - let (cli_in_tx, cli_in_rx) = unbounded::>(); - let (cli_out_tx, mut cli_out_rx) = unbounded::>(); - prov_in_tx - .unbounded_send( - serde_json::to_vec(&serde_json::json!({ - "type": "response.output_audio.delta", "delta": "AAAA" - })) - .expect("serializes"), - ) - .expect("queued"); - cli_in_tx - .unbounded_send( - serde_json::to_vec(&serde_json::json!({ - "type": "input_audio_buffer.append", "audio": "BBBB" - })) - .expect("serializes"), - ) - .expect("queued"); - drop(prov_in_tx); - drop(cli_in_tx); - proxy - .run(prov_in_rx, prov_out_tx, cli_in_rx, cli_out_tx) - .await; - - let kinds = |frames: Vec>| -> Vec { - frames - .iter() - .filter_map(|f| serde_json::from_slice::(f).ok()) - .filter_map(|v| v["type"].as_str().map(str::to_string)) - .collect() - }; - cli_out_rx.close(); - let down = kinds(cli_out_rx.by_ref().collect().await); - prov_out_rx.close(); - let up = kinds(prov_out_rx.by_ref().collect().await); - assert!( - down.contains(&"response.output_audio.delta".to_string()), - "the provider's audio reached the client: {down:?}" - ); - assert!( - up.contains(&"input_audio_buffer.append".to_string()), - "the client's audio reached the provider: {up:?}" - ); - 1 -} - -/// METER: one served turn is ledgered on the presenting key per class the plane declares, the two -/// text halves on their own classes rather than summed. -async fn a_served_turn_is_ledgered_per_declared_class() -> u64 { - let host = Arc::new(FixtureHost::new().governed()); - let rt = hosted(&host); - let key = caller("vk-meter"); - let (core, _handle) = open_session(&rt, &host, "call-meter", Some(&key)); - let _ = core.on_server_frame(usage(10, 3, 20, 4)).await; - for (class, count) in [ - ("audio_tokens_in", 10), - ("text_tokens_in", 3), - ("audio_tokens_out", 20), - ("text_tokens_out", 4), - ] { - assert_eq!( - ledgered(&host, "vk-meter", class), - Some(count), - "the served turn's `{class}` lands on the presenting key, exactly" - ); - } - 1 -} - -/// AUDIT: a served open lands exactly one admin-audit row, naming the session and its principal. -async fn a_served_open_lands_one_audit_row() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let resp = open_governed(served_open(&rt, &host, "call-audit", None)).await; - assert_eq!(resp.status(), axum::http::StatusCode::NOT_IMPLEMENTED); - let log = host.audit_log(); - assert_eq!(log.len(), 1, "one served open, one row: {log:?}"); - let row = &log[0]; - assert_eq!( - ( - row.action.as_str(), - row.resource.as_str(), - row.outcome.as_str(), - row.principal.as_str() - ), - ( - crate::mount::SESSION_AUDIT_ACTION, - "streaming:call-audit", - "applied", - "acct-witness" - ), - "the row names the mutation, the session, its outcome and its principal" - ); - 1 -} - -/// EXIT: a served session ends once — the turn still open when the carrier ends is ledgered with the -/// session, and the durable row is settled and evicted. -async fn a_served_session_ends_once_and_settles_its_open_turn() -> u64 { - let host = Arc::new(FixtureHost::new().governed()); - let rt = hosted(&host); - let key = caller("vk-exit"); - let (core, handle) = open_session(&rt, &host, "call-exit", Some(&key)); - - // Two seconds of PCM16 at 24 kHz (96 000 zero bytes, which base64 spells as 128 000 `A`s), - // admitted uplink, with no usage report to close the turn. - let audio = "A".repeat(128_000); - let _ = core.on_client_frame(frame(serde_json::json!({ - "type": "input_audio_buffer.append", "audio": audio - }))); - assert_eq!( - ledgered(&host, "vk-exit", "audio_seconds_in"), - None, - "nothing is ledgered while the turn is open" - ); - - crate::runtime::serve_to_teardown(Arc::clone(&core), handle, async {}, || 9).await; - assert_eq!( - ledgered(&host, "vk-exit", "audio_seconds_in"), - Some(2), - "the turn open at the end is ledgered with the session, in the class's own seconds" - ); - assert!( - rt.bind_session("acct-witness", "call-exit").get().is_none(), - "the durable row is settled and evicted: the session ended" - ); - 1 -} - -/// AUDIT-CHAIN: the served session's durable row seals at genesis, a turn checkpoint bumps it, and it -/// reads back to its owner and to nobody else. -async fn the_served_sessions_row_seals_at_genesis_and_reads_back() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let (_core, handle) = open_session(&rt, &host, "call-chain", None); - let row = handle - .get() - .expect("the genesis row the served open sealed reads back"); - assert_eq!( - (row.turns, row.owner.as_str()), - (0, "acct-witness"), - "a fresh genesis row, attributed to the session's principal" - ); - assert_eq!(handle.bump_turn(2).expect("the owner checkpoints"), 1); - assert_eq!( - rt.bind_session("acct-witness", "call-chain") - .get() - .map(|r| r.turns), - Some(1), - "a reattach reads the checkpoint back off the durable chain" - ); - assert!( - rt.bind_session("mallory", "call-chain").get().is_none(), - "a foreign owner reads nothing" - ); - 1 -} - -/// GOVERNANCE-BUDGET: a served session's turns are charged to the presenting key, the turn that dries -/// its chain closes the carrier, and that key's next open is refused. -async fn a_served_session_is_charged_to_the_presenting_key_and_closed_dry() -> u64 { - let host = Arc::new(FixtureHost::new().governed().with_count_cap(50)); - let rt = hosted(&host); - let key = caller("vk-budget"); - let (core, _handle) = open_session(&rt, &host, "call-budget", Some(&key)); - assert!( - !core.on_server_frame(usage(0, 0, 30, 0)).await.close, - "30 of 50: the session stays open" - ); - assert!( - core.on_server_frame(usage(0, 0, 30, 0)).await.close, - "60 of 50: the chain is dry and the carrier closes" - ); - assert!(core.carrier().is_closed(), "the carrier is hard-closed"); - assert_eq!( - ledgered(&host, "vk-budget", "audio_tokens_out"), - Some(60), - "every delivered count is charged to the presenting key, the drying turn included" - ); - let next = open_governed(served_open(&rt, &host, "call-budget-next", Some(key))).await; - assert_eq!( - next.status(), - axum::http::StatusCode::PAYMENT_REQUIRED, - "the dry key's next session is refused at the open" - ); - 2 -} - -/// METRICS: a served open is reported under the plane's own key, its dialect and its pool, with a -/// duration sample. -async fn a_served_open_is_reported_under_the_planes_own_labels() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let _ = open_governed(served_open(&rt, &host, "call-metrics", None)).await; - let reported = host.finished_requests(); - assert_eq!(reported.len(), 1, "one served open, reported once"); - let r = &reported[0]; - assert_eq!( - ( - r.plane.as_str(), - r.ingress_protocol.as_str(), - r.pool.as_str() - ), - (crate::PLANE_KEY, crate::OPENAI_REALTIME, VOICE_POOL), - "the plane's key, the dialect the door speaks and the pool it serves" - ); - assert!( - r.seconds.is_finite() && r.seconds >= 0.0, - "a duration sample rides the same report: {}", - r.seconds - ); - 1 -} - -/// BREAKER-FASTFAIL: a served session's provider dial is refused before any socket once the -/// provider's cell has tripped, with the cell's own `Retry-After`. -async fn a_served_sessions_dial_fast_fails_on_a_tripped_cell() -> u64 { - let host = Arc::new(FixtureHost::new()); - let rt = hosted(&host); - let (_core, _handle) = open_session(&rt, &host, "call-breaker", None); - let pool = stream_breaker_key("witness-provider"); - - // A target busbar's own guard refuses is a definitive failure: it trips the provider's cell. - let first = dial_provider( - &*host, - &pool, - 0, - "wss://127.0.0.1/", - Default::default(), - None, - ) - .await - .err(); - assert!( - matches!(first, Some(DialProviderError::Dial(_))), - "the guard-refused dial fails: {first:?}" - ); - assert_ne!( - host.breaker_state(&pool, 0), - host.breaker_state("never-dialled", 0), - "the provider's cell is no longer Closed" - ); - - // The next dial is refused at admission, before the guard or any socket is reached. - match dial_provider( - &*host, - &pool, - 0, - "wss://127.0.0.1/", - Default::default(), - None, - ) - .await - { - Err(DialProviderError::BreakerOpen { retry_after_secs }) => assert!( - retry_after_secs >= 1, - "the fast-fail carries the cell's own Retry-After" - ), - other => panic!( - "a tripped cell must fast-fail before the dial, got {:?}", - other.err() - ), - } - 1 -} diff --git a/crates/busbar-voice/src/topology/minter_https.rs b/crates/busbar-voice/src/topology/minter_https.rs deleted file mode 100644 index 205a0f085e..0000000000 --- a/crates/busbar-voice/src/topology/minter_https.rs +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CONCRETE HTTPS EPHEMERAL-SECRET MINTER for the browser WebRTC sideband. -//! -//! Implements the [`TokenMinter`] port over the substrate egress engine: a one-shot -//! `POST /v1/realtime/client_secrets` carrying the REAL provider key, from which the browser-facing -//! `ek_` client secret is returned. The real key stays server-side — it authenticates only the -//! busbar↔provider hop and never appears in the returned [`EphemeralToken`]. -//! -//! The mint stamps two guards the raw token carries no policy for on its own: the requested secret -//! lifetime is clamped to the provider's accepted window, and an `OpenAI-Safety-Identifier` header -//! binds the minted secret to the calling identity so it is attributable and rate-limitable to that -//! caller rather than a shared blob. The returned value is asserted to carry the `ek_` prefix before -//! it is handed back. - -use crate::ir::config::SessionConfig; -use crate::topology::webrtc::{EphemeralToken, MintError, TokenMinter}; -use async_trait::async_trait; -use busbar_kernel::egress::engine::{send_bounded, EngineClient}; -use bytes::Bytes; -use http_body_util::{BodyExt, Full}; -use std::time::Duration; - -/// The default requested secret lifetime when the caller pins none. -const DEFAULT_TTL_SECS: u64 = 600; -/// The provider's minimum accepted secret lifetime. -const MIN_TTL_SECS: u64 = 10; -/// The provider's maximum accepted secret lifetime. -const MAX_TTL_SECS: u64 = 7200; -/// The prefix every ephemeral client secret the provider mints carries. -const EK_PREFIX: &str = "ek_"; -/// The header binding a minted secret to the caller identity. -const SAFETY_IDENTIFIER_HEADER: &str = "OpenAI-Safety-Identifier"; -/// The bound on the whole mint exchange up to the response head plus its small body read. -const MINT_DEADLINE: Duration = Duration::from_secs(30); -/// The client-secrets endpoint path on the provider. -const CLIENT_SECRETS_PATH: &str = "/v1/realtime/client_secrets"; - -/// MINTS the browser's ephemeral client secret over a real HTTPS call to the provider's -/// client-secrets endpoint, holding the real key server-side. -/// -/// The `base_url` + owned [`EngineClient`] are constructor inputs so the composition root binds the -/// production provider origin while a test points them at a loopback server; the same request path -/// runs against both. `safety_identifier` is the caller-identity binding stamped on every mint; -/// `requested_ttl_secs` is the desired secret lifetime before clamping (`None` ⇒ the default). -pub struct HttpsTokenMinter { - client: EngineClient, - base_url: String, - /// The real provider key, held `Redacted`; exposed only into the mint's `Authorization` header. - api_key: busbar_contract::Redacted, - safety_identifier: String, - requested_ttl_secs: Option, -} - -impl HttpsTokenMinter { - /// Build a minter over an already-assembled egress client. `base_url` is the provider origin - /// (scheme + authority, e.g. `https://api.openai.com`); `api_key` is the REAL provider key held - /// server-side; `safety_identifier` is the caller-identity binding; `requested_ttl_secs` is the - /// desired secret lifetime (`None` ⇒ [`DEFAULT_TTL_SECS`]), clamped to the accepted window on mint. - pub fn new( - client: EngineClient, - base_url: impl Into, - api_key: impl Into, - safety_identifier: impl Into, - requested_ttl_secs: Option, - ) -> Self { - HttpsTokenMinter { - client, - base_url: base_url.into(), - api_key: busbar_contract::Redacted::new(api_key.into()), - safety_identifier: safety_identifier.into(), - requested_ttl_secs, - } - } - - /// The requested lifetime clamped to the provider's accepted `[MIN, MAX]` window. - fn clamped_ttl_secs(&self) -> u64 { - self.requested_ttl_secs - .unwrap_or(DEFAULT_TTL_SECS) - .clamp(MIN_TTL_SECS, MAX_TTL_SECS) - } -} - -/// The provider's client-secret response: the `ek_` value and its absolute expiry in unix seconds. -#[derive(serde::Deserialize)] -struct ClientSecretResponse { - value: String, - #[serde(default)] - expires_at: u64, -} - -/// A decode failure on the mint answer, described without `serde_json::Error`'s own `Display`: the -/// body carries the minted secret and a data error quotes the offending value, so the decoder's text -/// is withheld (secret-hygiene #53, Check 3). The class and the place of the failure survive. -fn json_failure_shape(e: &serde_json::Error) -> String { - let class = match e.classify() { - serde_json::error::Category::Io => "the body could not be read", - serde_json::error::Category::Syntax => "it is not well-formed JSON", - serde_json::error::Category::Data => "a field is missing or has the wrong type", - serde_json::error::Category::Eof => "it ends before the JSON does", - }; - let (line, column) = (e.line(), e.column()); - format!("{class} (line {line}, column {column}; the decoder's text is withheld)") -} - -#[async_trait] -impl TokenMinter for HttpsTokenMinter { - async fn mint(&self, config: &SessionConfig) -> Result { - let ttl_secs = self.clamped_ttl_secs(); - let body = serde_json::json!({ - "expires_after": { "anchor": "created_at", "seconds": ttl_secs }, - "session": config, - }); - let body_bytes = serde_json::to_vec(&body).map_err(|e| { - MintError::Provider(format!("mint request body did not serialize: {e}")) - })?; - - let uri = format!( - "{}{}", - self.base_url.trim_end_matches('/'), - CLIENT_SECRETS_PATH - ); - let req = http::Request::builder() - .method(http::Method::POST) - .uri(&uri) - .header(http::header::CONTENT_TYPE, "application/json") - .header( - http::header::AUTHORIZATION, - format!("Bearer {}", self.api_key.expose_secret()), - ) - .header(SAFETY_IDENTIFIER_HEADER, &self.safety_identifier) - .body(Full::new(Bytes::from(body_bytes))) - .map_err(|e| MintError::Provider(format!("mint request did not build: {e}")))?; - - let deadline = tokio::time::Instant::now() + MINT_DEADLINE; - let resp = send_bounded(&self.client, req, deadline) - .await - .map_err(|e| MintError::Provider(e.into_cause()))?; - - let status = resp.status(); - let raw = tokio::time::timeout_at(deadline, resp.into_body().collect()) - .await - .map_err(|_| { - MintError::Provider( - "client-secret response was not read before the deadline".into(), - ) - })? - .map_err(|e| MintError::Provider(format!("client-secret response body failed: {e}")))? - .to_bytes(); - - if !status.is_success() { - return Err(MintError::Provider(format!( - "client-secret endpoint returned {status}" - ))); - } - - // Decoded here, in the binary's own crate (door-only: the streaming plane's reader is a plugin - // crate's item). A decode failure is described WITHOUT the decoder's text, which can quote the - // minted secret (secret-hygiene #53, Check 3): what survives is the class and the place. - let parsed: ClientSecretResponse = serde_json::from_slice(&raw).map_err(|e| { - MintError::Provider(format!( - "client-secret response did not parse: {}", - json_failure_shape(&e) - )) - })?; - - // The browser-facing invariant: only an `ek_` secret ever leaves this boundary. A response - // whose value lacks the prefix is refused rather than handed on as if it were a client secret. - if !parsed.value.starts_with(EK_PREFIX) { - return Err(MintError::Provider( - "client-secret response value is not an ek_ ephemeral secret".into(), - )); - } - - Ok(EphemeralToken { - value: parsed.value, - expires_at_unix: parsed.expires_at, - }) - } -} - -#[cfg(all(test, feature = "test-support"))] -#[path = "tests/minter_https_tests.rs"] -mod minter_https_tests; diff --git a/crates/busbar-voice/src/topology/mod.rs b/crates/busbar-voice/src/topology/mod.rs deleted file mode 100644 index 97f960b1ac..0000000000 --- a/crates/busbar-voice/src/topology/mod.rs +++ /dev/null @@ -1,437 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE TWO TOPOLOGIES the voice runtime exposes (design `BUSBAR-1.6.0.md` #18/#45), behind the `runtime` feature. -//! -//! * [`webrtc`] — the BROWSER WebRTC sideband: busbar mints the ephemeral token and holds a persistent -//! sideband control channel owning tools + instructions; the browser's MEDIA path is peer-to-peer, so -//! busbar is mint/guard + control, NOT a media relay. -//! * [`telephony`] — a THIN WS PROXY: `g711_ulaw` end-to-end so 8 kHz passes straight through (no -//! resample), with barge-in truncate driven from the codec's playback marks. -//! -//! Both are assembled from a [`crate::runtime::VoiceRuntime`] via [`begin_session`], which opens the -//! presenting key's kernel account (fail-closed on a chain already dry) and the durable -//! [`SessionHandle`] before a frame flows. - -pub mod minter_https; -pub mod telephony; -/// The Twilio Media Streams messages belong to the plane crate's own `codec` module: they are a -/// GRAMMAR, not a dial — a total function from a frame to an IR event and back — so they live with -/// the IR they map onto. Re-exported here so `crate::topology::twilio::…` resolves to them. -/// The Twilio DIAL (the outbound call the webhook answers) is `telephony`, and it lives here. -pub use busbar_plane_streaming::codec::topology::twilio; -pub mod webrtc; - -#[cfg(test)] -mod tests; - -use crate::ir::codec::{DuplexReader, DuplexWriter}; -use crate::ir::config::SessionConfig; -use crate::runtime::carrier::Carrier; -use crate::runtime::scope::SessionHandle; -use crate::runtime::session::SessionCore; -use crate::runtime::VoiceRuntime; -use busbar_contract::transport::transport::{Transport, UpstreamWireKind}; -use busbar_contract::upstream::{CanonicalSignal, StatusClass}; -use busbar_kernel::egress::duplex_ws::{self, DialCredential, DialError, DialTarget}; -use busbar_kernel::plane::handle_engine::HandleEngineError; -use busbar_kernel::plane_host::{ - breaker, run_gauntlet_session, BreakerHost, DispatchScope, GauntletPlane, GauntletRequest, - VerifyOutcome, -}; -use busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy; -use futures::{Sink, Stream}; -use std::sync::Arc; - -/// THE VOICE PLANE'S BREAKER-CELL KEY for one provider dial target. The `stream:` prefix is the -/// plane-qualified keyspace rule (the `tool:` / `agent:` precedent — a voice cell can never collide -/// with an MCP tool cell, an A2A agent cell, or a bare LLM pool name), and the id is the operator's -/// provider/pool name that a refusal names. Formed HERE (not in the neutral substrate) so the voice -/// plane keys its own cell without a substrate helper and stays strong-form deletable. -#[must_use] -pub fn stream_breaker_key(provider: &str) -> String { - format!("stream:{provider}") -} - -/// Why a governed provider dial did not open a socket. -#[derive(Debug)] -pub enum DialProviderError { - /// The `(pool, lane)` breaker cell was OPEN, so admission was refused BEFORE any socket — the - /// fast-fail leg. Carries the honest `Retry-After` read off the cell's own cooldown. - BreakerOpen { - /// Seconds until the cell's cooldown expires (floored at 1). - retry_after_secs: u64, - }, - /// The dial itself failed (guard-refused / connect / TLS / handshake). The failure has already - /// been recorded into the breaker cell. - Dial(DialError), -} - -impl std::fmt::Display for DialProviderError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DialProviderError::BreakerOpen { retry_after_secs } => write!( - f, - "streaming provider dial refused: breaker open (retry after {retry_after_secs}s)" - ), - DialProviderError::Dial(e) => write!(f, "streaming provider dial failed: {e:?}"), - } - } -} - -impl std::error::Error for DialProviderError {} - -/// The canonical breaker signal one [`DialError`] means. A target busbar's OWN net-guard or URL parse -/// refused is a DEFINITIVE (hard-down) failure — that configured/derived target can never be dialed -/// for this session, so its cell earns the sticky cooldown, exactly as an auth/billing hard-down does. -/// A connect / TLS / handshake failure to a pinned address is a TRANSIENT upstream signal (the cell -/// trips only once the error-rate window crosses its threshold), the same disposition the model -/// plane's own dispatch records a network blip under. -fn dial_signal(e: &DialError) -> CanonicalSignal { - let class = match e { - DialError::Guard(_) | DialError::Url(_) => StatusClass::Auth, - DialError::Connect(_) | DialError::Tls(_) | DialError::Handshake(_) => StatusClass::Network, - }; - CanonicalSignal { - class, - provider_signal: None, - retry_after: None, - } -} - -/// DIAL THE PROVIDER SIDEBAND/UPSTREAM WSS through the neutral, net-guarded WS transport — the plane's -/// one door to an outbound duplex socket, and the close of the egress-audit finding that voice's -/// provider WSS never went through net-guard. -/// -/// The plane SELECTS [`Transport::WebSocket`], resolves the axis to its neutral wire shape -/// ([`UpstreamWireKind::Duplex`]) and lets the SUBSTRATE open the socket: the dialer -/// resolves-then-pins-then-guards `url` and hands back the message `Stream`/`Sink>` pair the -/// session pump (`serve_messages`) consumes. The plane holds no socket, resolver or WS framing of its -/// own — it feeds the returned pair to a topology's provider leg -/// ([`telephony::TelephonyProxy::run`](crate::topology::telephony::TelephonyProxy::run) or the webrtc -/// sideband) and keeps ONLY data/session/media logic. -/// -/// `policy` is the outbound trust posture (a public provider `wss://` takes the fail-closed -/// [`GuardPolicy::default`]); the guard NEVER opens a socket to a target it did not pin. -/// -/// THE BREAKER RIDES BENEATH THE DIAL (the voice-client cell): before any socket, the `(pool, lane)` -/// cell is probed through the kernel's `breaker::admit` — an OPEN cell fast-fails in microseconds with the -/// cell's own `Retry-After` (never waiting out a dial timeout against a target already known down). -/// Past admission the attempt is counted on `busbar_upstream_attempts_total`, and the dial's outcome -/// is FOLDED back into the same cell: a clean open records a success (diluting the error window / -/// closing a recovery probe), a failure records its canonical signal (a guard/URL refusal opens the -/// cell hard-down; a connect/TLS/handshake blip is transient). `pool` is the plane's breaker-cell key -/// (see [`stream_breaker_key`]); `lane` is the member position (0 for a degenerate cell). -/// -/// `url` is the KEYLESS provider target and `credential` the provider key with its placement; the -/// substrate dialer adds the key to the upgrade request only, so no error this returns carries it. -pub async fn dial_provider( - host: &dyn BreakerHost, - pool: &str, - lane: usize, - url: &str, - policy: GuardPolicy, - credential: Option>, -) -> Result< - ( - impl Stream> + Unpin, - impl Sink, Error = ()> + Unpin + Send + 'static, - ), - DialProviderError, -> { - // FAST-FAIL ADMISSION FIRST: probe the cell through the host seam. An OPEN cell refuses here with - // its own cooldown as `Retry-After` — no socket, no dial timeout. The probe is scoped so any - // recovery probe it wins is released the instant this block ends; the in-place record below is the - // authoritative fold, the documented `breaker::record_*` fallback disposition. - { - let scope = DispatchScope::new(); - if breaker::admit(host, &scope, pool.as_bytes(), lane as u32).is_err() { - return Err(DialProviderError::BreakerOpen { - retry_after_secs: breaker::retry_after_secs(host, pool, lane), - }); - } - } - - // ADMITTED: count the dispatch attempt on the voice-client leg (both labels operator-configured - // and bounded — a provider/pool name and a small lane integer — so the series count stays bounded). - busbar_kernel::telemetry::upstream_attempt_on(pool, &lane.to_string()); - - // The axis pins `WebSocket` to the full-duplex wire; the `else` is unreachable by construction (a - // closed axis), expressed as a refusal rather than a panic so a mis-selection fails closed. - let Some(UpstreamWireKind::Duplex) = Transport::WebSocket.upstream_wire() else { - let e = DialError::Url(DialTarget::new(url)); - breaker::record_signal(host, pool, lane, &dial_signal(&e)); - return Err(DialProviderError::Dial(e)); - }; - - match duplex_ws::dial_with_credential(url, policy, credential).await { - Ok((stream, sink)) => { - breaker::record_success(host, pool, lane); - // `sink_map_err(|_| ())`: the substrate dialer's own Sink `Error` associated type is an - // opaque `impl Trait` detail with no `Send` bound of its own (it happens to be `Send` today, - // but nothing in its signature promises that) — and `TelephonyProxy::run` (the one consumer) - // needs `POut::Error: Send` to spawn the drain task. Discarding the write-failure detail into - // a bare `()` is a small, honest price: a write failure already means the socket is going - // away (the pump's own EOF/close handling is what actually tears the session down), so no - // caller of this fn reads the write error's content today. - Ok((stream, futures::SinkExt::sink_map_err(sink, |_| ()))) - } - Err(e) => { - breaker::record_signal(host, pool, lane, &dial_signal(&e)); - Err(DialProviderError::Dial(e)) - } - } -} - -/// Why a session failed to start before any frame flowed. -#[derive(Debug)] -pub enum StartError { - /// The OPEN-PASS gauntlet gate ([`run_gauntlet_session`]) REFUSED the session's destination BEFORE - /// any account/durable open — zero bytes, zero charge. The verify-strictly-before-charge invariant. - DestinationRefused, - /// The kernel's budget view reads the presenting key's chain already dry — fail closed, never open. - BudgetRefused, - /// The durable [`SessionHandle`] could not be opened (the engine rejected the genesis). - Durable(HandleEngineError), -} - -impl std::fmt::Display for StartError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StartError::DestinationRefused => { - write!( - f, - "streaming session destination refused at the open-pass gate (fail closed)" - ) - } - StartError::BudgetRefused => { - write!(f, "streaming session budget refused (fail closed)") - } - StartError::Durable(e) => write!(f, "streaming session durable open failed: {e:?}"), - } - } -} - -impl std::error::Error for StartError {} - -/// THE VOICE PLANE's [`GauntletPlane`] for a SESSION open — its contribution to the shared open-pass -/// gauntlet gate, which runs BEFORE the account/durable open (a refusal there costs zero bytes and -/// zero charge). The plane keeps NO private destination policy of its own (1.5.5 had none; model -/// access is the key/group pool ACL at the scope step), so its `verify_destination` (stage 2) -/// proceeds; a kernel-loop session runner registered under the plane's capability key is what may -/// refuse at this gate. `drive` (the one-shot stages 4+5) is UNREACHABLE on the session path — -/// [`run_gauntlet_session`] only runs the gate, never `drive` — so it fails closed with a neutral 500 -/// if a future refactor ever mis-routed a session opener through the one-shot path. -pub(crate) struct SessionGauntlet; - -#[async_trait::async_trait] -impl GauntletPlane for SessionGauntlet { - fn verify_destination(&self, _req: &GauntletRequest<'_>) -> VerifyOutcome { - VerifyOutcome::Proceed - } - - async fn drive(self: Box, _req: GauntletRequest<'_>) -> axum::response::Response { - // Never reached on the session path (the opener runs only the admission gate). Fail closed. - axum::response::Response::builder() - .status(axum::http::StatusCode::INTERNAL_SERVER_ERROR) - .body(axum::body::Body::from( - "session gauntlet never drives a one-shot response", - )) - .expect("static fault response builds") - } - - /// THE VOICE (STREAMING) PLANE'S CAPABILITY KEY on the composition-tier host-selection seam (loop - /// unification, #28). The plane DECLARES its identity here; the LOOP CHOICE is the composition - /// root's — it registers a kernel-loop SESSION runner under this same key ([`crate::PLANE_KEY`]) or - /// leaves it UNSET. With W2.b the root flips `voice` on, so this reported key routes the session - /// open-pass admit through the unified kernel loop; the admit gate is byte-identical either way, so - /// declaring the key is safe with or without the flip. - fn capability_key(&self) -> Option<&str> { - Some(crate::PLANE_KEY) - } -} - -/// BEGIN a governed session, common to both topologies: open the presenting key's kernel account -/// (fail-closed on a chain already dry), open the durable [`SessionHandle`] at genesis, and assemble -/// the [`SessionCore`] with the plane's locked config, chosen `codec`, and `carrier`. The caller then -/// serves the returned core over the neutral pump. -#[allow(clippy::too_many_arguments)] -pub fn begin_session( - rt: &VoiceRuntime, - codec: C, - owner: impl Into, - call_id: impl Into, - locked_config: Option, - carrier: Carrier, - meter: Option, - now: u64, -) -> Result<(Arc>, SessionHandle), StartError> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - let gate: Box = Box::new(SessionGauntlet); - begin_session_through( - rt, - gate, - codec, - owner, - call_id, - locked_config, - carrier, - meter, - now, - ) -} - -/// [`begin_session`] over a given open-pass `gate`: the plane's own [`SessionGauntlet`] in production; -/// a test hands a refusing gate to witness that the budget is never asked on a refused session. -#[allow(clippy::too_many_arguments)] -pub(crate) fn begin_session_through( - rt: &VoiceRuntime, - gate: Box, - codec: C, - owner: impl Into, - call_id: impl Into, - locked_config: Option, - carrier: Carrier, - meter: Option, - now: u64, -) -> Result<(Arc>, SessionHandle), StartError> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - // OPEN-PASS ADMISSION FIRST (verify STRICTLY before any charge): run the shared gauntlet gate at the - // TOP through `run_gauntlet_session`. On refuse NOTHING is opened — no account, no durable genesis, - // no socket — so a refused session costs ZERO bytes and ZERO charge. The session's budget check - // (the kernel account's open) fires only AFTER the gate clears, matching the LLM plane's real - // verify-before-admission-door order. - let destination = locked_config - .as_ref() - .and_then(|c| c.model.clone()) - .unwrap_or_default(); - let gov = busbar_contract::records::PlaneRequestCtx::default(); - let gauntlet_req = GauntletRequest { - gov: &gov, - destination: &destination, - correlation_id: 0, - charged_at: now, - started: std::time::Instant::now(), - }; - // The call-site the D3 witness pins: begin_session ACTUALLY calls run_gauntlet_session here. - run_gauntlet_session(gauntlet_req, gate).map_err(|_refusal| StartError::DestinationRefused)?; - - // Only past the gate: reserve/bind/open the live carrier. Factored into [`open_admitted_session`] - // so the inbound WS-accept seam — where the gauntlet has ALREADY run inside `accept_gauntlet`, - // strictly before the socket upgrades — reuses the SAME post-admit open without re-running (or - // duplicating) the gate. begin_session's own order is byte-identical: gauntlet, then this. - open_admitted_session( - rt, - codec, - owner, - call_id, - locked_config, - carrier, - meter, - now, - // NO BINDING FROM HERE. This entry point is the one a caller drives directly — a topology - // test, a conformance probe, the one-shot HTTP passes — and none of them is a session a - // composition root's table has an identifier for. The SERVED door mints one per session and - // hands it to the post-admit open below, which is the whole of what makes a served session a - // governed one. - None, - ) -} - -/// THE POST-ADMIT OPEN of a governed session — the reserve/bind/open half of [`begin_session`], -/// called ONLY after the open-pass gauntlet has already admitted the destination (verify strictly -/// before any charge). Opens the presenting key's kernel account (fail-closed on a chain already dry), -/// opens the durable [`SessionHandle`] at genesis, and assembles the [`SessionCore`]. NO gauntlet runs here: the -/// caller (`begin_session`, or the inbound WS-accept `accept_gauntlet` path) is responsible for having -/// run it first. A refused budget or a failed durable open returns before ANY durable row is committed -/// — so an aborted open, like a refused gauntlet, leaves no orphaned live session row. -/// -/// `governed` is the node's open-call table and the identifier this session is known to it by — the -/// composition root's own binding, minted per session at the served door -/// (`crate::mount::served_governed_session`). Every session this door opens over a socket carries -/// one, which is what makes "served" and "governed" the same set rather than two that overlap. -/// `None` is a deployment with no composition root behind it: every tool call is served in-process -/// and a client-authored result is carried upstream verbatim, exactly as before the wait existed. -#[allow(clippy::too_many_arguments)] -pub(crate) fn open_admitted_session( - rt: &VoiceRuntime, - codec: C, - owner: impl Into, - call_id: impl Into, - locked_config: Option, - carrier: Carrier, - meter: Option, - now: u64, - governed: Option, -) -> Result<(Arc>, SessionHandle), StartError> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - // Captured before any move: `bind_session` and the exit-path audit call below both need their own - // copy, and the admin-audit row's principal/resource must name THIS session even though every - // downstream consumer (the durable handle, the audit call) takes owned strings. - let owner = owner.into(); - let call_id = call_id.into(); - - // The kernel's budget gate, AFTER admission: a caller whose chain is already dry never opens a - // session (fail closed). The account it opens is what each turn's counts are reported to. - let model = locked_config - .as_ref() - .and_then(|c| c.model.clone()) - .unwrap_or_default(); - let metering = match meter { - Some(meter) => meter.open(&model).map_err(|_| StartError::BudgetRefused)?, - None => None, - }; - - let handle = rt.bind_session(owner.clone(), call_id.clone()); - // A durable open that fails leaves a session that never opened: its counted session fee is - // given back (TODO 17(b), ARCHITECT R4) before the refusal. - if let Err(e) = handle.open(now) { - if let Some(metering) = metering { - metering.refund_open(); - } - return Err(StartError::Durable(e)); - } - - // ONE ADMIN-AUDIT ROW for this governed mutation — fired exactly once (this is the single `Ok` - // construction point below every early `?` return above), no-op on a runtime with no bound host - // (the pre-host/dev-default runtime). Voice posts no separate "kernel-floor" line at Admit the way - // BUSBAR-1.6.0.md's Admit step describes for an already-dialed provider push: a refused reserve above never - // reaches here, so nothing is owed and nothing is journaled for it. - rt.audit_session( - // The ONE action literal, named where the REJECTED twin names it (`mount::hook_tap` refuses a - // committed rewrite busbar cannot read back under this same action) — so the two outcomes of - // one mutation cannot drift into two spellings. - crate::mount::SESSION_AUDIT_ACTION, - &format!("streaming:{call_id}"), - "applied", - &owner, - ); - - let core = SessionCore::new( - codec, - metering, - Arc::clone(&rt.tools), - carrier, - locked_config, - ); - // The operator's hard wall-clock ceiling, enforced on the sweep tick beside the pump — only when - // the operator configured one (Q21a: no config, no limit). - let core = match rt.session_max_secs { - Some(secs) => core.with_session_ceiling(secs.get()), - None => core, - }; - // The binding, applied before the core is shared: a session that reached a socket ungoverned and - // was governed afterwards would have a window in which its pump could see a tool call it had no - // table to enter. - let core = Arc::new(match governed { - Some(g) => core.with_governed(g), - None => core, - }); - Ok((core, handle)) -} diff --git a/crates/busbar-voice/src/topology/telephony.rs b/crates/busbar-voice/src/topology/telephony.rs deleted file mode 100644 index 5abb2f1b4e..0000000000 --- a/crates/busbar-voice/src/topology/telephony.rs +++ /dev/null @@ -1,253 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! TOPOLOGY B — the THIN TELEPHONY WS PROXY (design `BUSBAR-1.6.0.md` #18/#45). -//! -//! busbar sits between the telephony leg (the phone/carrier media stream) and the provider's Realtime -//! upstream, proxying frames both ways while metering, governing tools, and driving barge-in. It -//! chooses `g711_ulaw` END-TO-END so 8 kHz µ-law passes straight through with NO resample; the barge-in -//! truncate `audio_end_ms` is computed from the codec's playback marks (`DecodeState::flush_playback`) -//! and the queued outbound audio is flushed on `speech_started` — the truncate bookkeeping the codec -//! exposes, driven by [`crate::runtime::session::SessionCore`]. -//! -//! WIRING. Two sockets, four directions. The provider socket's WRITE side is shared by two producers — -//! the downlink-facing [`VoiceSession`] (tool results / barge-in cancel) and the [`UplinkForwarder`] -//! (client audio → upstream) — so it is funnelled through one channel into a single writer. The client -//! socket's write side is the downlink, driven by the session [`Carrier`]. - -use crate::ir::codec::{DuplexReader, DuplexWriter}; -use crate::ir::config::SessionConfig; -use crate::ir::media::AudioFormat; -use crate::runtime::carrier::Carrier; -use crate::runtime::scope::SessionHandle; -use crate::runtime::session::{SessionCore, UplinkForwarder, VoiceSession}; -use crate::runtime::VoiceRuntime; -use crate::topology::{begin_session, StartError}; -use busbar_kernel::ingress::byte_duplex::serve_messages; -use futures::channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender}; -use futures::{Sink, Stream, StreamExt}; -use std::sync::Arc; - -/// THE LOCKED CONFIG for a telephony leg: `g711_ulaw` on BOTH the input and output audio formats so the -/// 8 kHz µ-law carrier passes straight through with no resample. Callers overlay their own -/// instructions/tools onto the returned config before locking it. -#[must_use] -pub fn g711_config() -> SessionConfig { - SessionConfig { - input_audio_format: Some(AudioFormat::G711Ulaw), - output_audio_format: Some(AudioFormat::G711Ulaw), - ..SessionConfig::default() - } -} - -/// A LIVE TELEPHONY PROXY — the two planes to serve (one per socket), the funnels between them, the -/// shared session core, and the durable handle. Build it with [`begin_telephony`]; drive it with -/// [`TelephonyProxy::run`]. -pub struct TelephonyProxy { - /// The shared session core (metering, tools, barge-in). - pub core: Arc>, - /// The durable session binding to close at teardown. - pub handle: SessionHandle, - /// The downlink-facing plane — serve it over the PROVIDER socket. - downlink_plane: Arc>, - /// The uplink-facing plane — serve it over the CLIENT socket. - uplink_plane: Arc>, - /// The provider-write funnel receiver (both producers merge here). - upstream_rx: UnboundedReceiver>, - /// The provider-write funnel sender handed to the downlink plane's serve sink. - upstream_tx: UnboundedSender>, - /// The client-write (downlink) funnel receiver, fed by the session carrier. - downlink_rx: UnboundedReceiver>, -} - -/// BEGIN a telephony proxy: lock the `g711`-based config, open the governed session (kernel account + -/// durable handle), and build the two planes + the funnels between them. The provider/client sockets are bound -/// later by [`TelephonyProxy::run`]. -#[allow(clippy::too_many_arguments)] -pub fn begin_telephony( - rt: &VoiceRuntime, - codec: C, - owner: impl Into, - call_id: impl Into, - locked_config: SessionConfig, - meter: Option, - now: u64, -) -> Result, StartError> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - // The client-write (downlink) funnel: the carrier relays server→client audio here. - let (downlink_tx, downlink_rx) = unbounded::>(); - let carrier = Carrier::with_downlink(downlink_tx); - - let (core, handle) = begin_session( - rt, - codec, - owner, - call_id, - Some(locked_config), - carrier, - meter, - now, - )?; - - // The provider-write funnel: both the downlink plane's `out` and the uplink forwarder merge here. - let (upstream_tx, upstream_rx) = unbounded::>(); - let downlink_plane = Arc::new(VoiceSession::new(Arc::clone(&core))); - let uplink_plane = Arc::new(UplinkForwarder::new(Arc::clone(&core), upstream_tx.clone())); - - Ok(TelephonyProxy { - core, - handle, - downlink_plane, - uplink_plane, - upstream_rx, - upstream_tx, - downlink_rx, - }) -} - -/// POST-ADMIT variant of [`begin_telephony`], for a caller that ALREADY ran the open-pass gauntlet -/// (the inbound WS-accept seam, `accept_gauntlet`) — mirrors [`crate::topology::open_admitted_session`]'s -/// contract exactly: no gauntlet runs here, so a caller that already admitted the destination does not -/// re-run (or double-charge) it. Used for BOTH the g711 telephony leg and the Gemini Live thin-duplex -/// leg — the wiring is identical; only the codec and the locked config differ, and both are the -/// caller's to choose. -/// -/// `governed` is the served door's own binding — the node's open-call table and the identifier this -/// session is known to it by — and it is carried straight through to the post-admit open, so a -/// dialed proxy is governed on exactly the terms the other two served legs are. -#[allow(clippy::too_many_arguments)] -pub(crate) fn open_admitted_telephony( - rt: &VoiceRuntime, - codec: C, - owner: impl Into, - call_id: impl Into, - locked_config: SessionConfig, - meter: Option, - now: u64, - governed: Option, -) -> Result, StartError> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - let (downlink_tx, downlink_rx) = unbounded::>(); - let carrier = Carrier::with_downlink(downlink_tx); - - let (core, handle) = crate::topology::open_admitted_session( - rt, - codec, - owner, - call_id, - Some(locked_config), - carrier, - meter, - now, - governed, - )?; - - let (upstream_tx, upstream_rx) = unbounded::>(); - let downlink_plane = Arc::new(VoiceSession::new(Arc::clone(&core))); - let uplink_plane = Arc::new(UplinkForwarder::new(Arc::clone(&core), upstream_tx.clone())); - - Ok(TelephonyProxy { - core, - handle, - downlink_plane, - uplink_plane, - upstream_rx, - upstream_tx, - downlink_rx, - }) -} - -impl TelephonyProxy -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, -{ - /// The shared session core. - #[must_use] - pub fn core(&self) -> &Arc> { - &self.core - } - - /// RUN the proxy until either socket ends OR the kernel's budget verdict hard-closes the carrier. Binds the - /// four socket halves — the PROVIDER pair is opened THROUGH the neutral guarded WS transport - /// ([`crate::topology::dial_provider`], which resolves-then-pins-then-guards the upstream `wss://`), - /// and the CLIENT pair is the telephony leg; the plane holds no socket plumbing, only these - /// `Stream`/`Sink` halves: - /// * `provider_in` / `provider_out` — the Realtime upstream (server→client in, client→server out). - /// * `client_in` / `client_out` — the telephony leg (client audio in, downlink audio out). - /// - /// The provider-write funnel drains into `provider_out`; the downlink funnel drains into - /// `client_out`. A hard close (budget dry) wins the `select!` and tears the session down. - pub async fn run( - self, - provider_in: PIn, - provider_out: POut, - client_in: DIn, - client_out: DOut, - ) where - PIn: Stream> + Unpin, - POut: Sink> + Unpin + Send + 'static, - POut::Error: Send, - DIn: Stream> + Unpin, - DOut: Sink> + Unpin + Send + 'static, - DOut::Error: Send, - { - let TelephonyProxy { - core, - handle, - downlink_plane, - uplink_plane, - upstream_rx, - upstream_tx, - downlink_rx, - } = self; - - let carrier = core.carrier().clone(); - - // The two writer drains run as detached tasks (their receivers/sinks are `Send + 'static`). - // Each ends when ITS funnel's last sender drops — provider-write when both serve legs finish, - // downlink when the session core (and its carrier) is dropped below. - let up_drain = tokio::spawn( - upstream_rx - .map(Ok::, POut::Error>) - .forward(provider_out), - ); - let down_drain = tokio::spawn( - downlink_rx - .map(Ok::, DOut::Error>) - .forward(client_out), - ); - - // Serve the provider socket: server→client events drive `on_server_frame`; `out` is the shared - // provider-write funnel. Serve the client socket: client→server frames are forwarded to the - // provider funnel; this socket's own write side is the downlink (driven by the carrier), so the - // pump's sink discards — the uplink plane never writes through `out`. - let up_serve = serve_messages(provider_in, upstream_tx, downlink_plane); - let down_serve = serve_messages(client_in, futures::sink::drain(), uplink_plane); - let serves = futures::future::join(up_serve, down_serve); - - // The marquee teardown: either both sockets reach EOF, or a hard close (budget dry) wins the - // race and drops the serve futures (dropping the planes + the provider-write sender). - tokio::select! { - _ = serves => {} - _ = carrier.closed() => {} - } - - // Release the session so the downlink funnel's last sender (the carrier) drops and its drain - // completes; then await both drains so queued frames are flushed to the sockets before return. - drop(carrier); - // A turn the call ended in the middle of was served all the same: its counts are ledgered now. - core.settle_open_turn(); - // And every call it still had open is over with it. - core.forget_governed_calls(); - drop(core); - let _ = up_drain.await; - let _ = down_drain.await; - // The call is over: its durable row goes terminal and leaves the working set. Dropped - // unsettled, it stayed ACTIVE after both sockets were gone. - handle.finish(crate::runtime::scope::unix_now_secs()); - } -} diff --git a/crates/busbar-voice/src/topology/tests/minter_https_tests.rs b/crates/busbar-voice/src/topology/tests/minter_https_tests.rs deleted file mode 100644 index 2e02a829aa..0000000000 --- a/crates/busbar-voice/src/topology/tests/minter_https_tests.rs +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! HTTPS token-minter tests, at LLM-egress parity: the REAL substrate egress client dials a loopback -//! `MockServer` (the exact bar the LLM plane's egress mock-tests hold), so the minter's request -//! preparation and response handling are exercised over the production request path, not a fake. -//! -//! Proven here: the requested secret lifetime is clamped to the accepted window (over the ceiling → -//! max, under the floor → min, unset → default); the `OpenAI-Safety-Identifier` caller binding is -//! stamped; the returned token is the provider's `ek_` value; a response value without the `ek_` -//! prefix is refused; and the real key never appears in the returned token. - -use super::*; -use crate::ir::config::SessionConfig; -use crate::testkit::loopback_http::{MockResponse, MockServer, MockServerState}; -use crate::topology::webrtc::{MintError, TokenMinter}; -use busbar_kernel::egress::engine::EngineSpec; -use busbar_kernel::proxy::build_egress_client; -use std::sync::Arc; - -const REAL_KEY: &str = "sk-real-secret-key-never-leaves-the-server"; -const SAFETY_ID: &str = "caller-identity-binding-abc123"; - -fn locked_config() -> SessionConfig { - SessionConfig { - instructions: Some("locked system instructions".to_string()), - voice: Some("marin".to_string()), - ..SessionConfig::default() - } -} - -async fn mock_returning(body: serde_json::Value) -> (MockServer, Arc) { - let state = Arc::new(MockServerState::new()); - state.push(MockResponse::Ok { - status: axum::http::StatusCode::OK, - body, - }); - let server = MockServer::new(Arc::clone(&state)).await; - (server, state) -} - -fn minter(server: &MockServer, requested_ttl_secs: Option) -> HttpsTokenMinter { - let client = build_egress_client(&EngineSpec::pooled_webpki(4, 300, false, false)); - HttpsTokenMinter::new( - client, - server.base_url(), - REAL_KEY, - SAFETY_ID, - requested_ttl_secs, - ) -} - -/// The seconds the mint request carried under `expires_after`, read off the mock's recorded body. -fn recorded_ttl_secs(state: &MockServerState) -> u64 { - let raw = state.get_last_request_body().expect("mint sent a body"); - let sent: serde_json::Value = serde_json::from_slice(&raw).expect("mint body is json"); - sent["expires_after"]["seconds"] - .as_u64() - .expect("expires_after.seconds is a number") -} - -#[tokio::test] -async fn ttl_over_ceiling_is_clamped_to_max() { - let (server, state) = mock_returning( - serde_json::json!({ "value": "ek_test123", "expires_at": 1_700_000_600u64 }), - ) - .await; - let token = minter(&server, Some(99_999)) - .mint(&locked_config()) - .await - .expect("mint succeeds"); - - assert_eq!( - recorded_ttl_secs(&state), - 7200, - "over-ceiling clamps to 7200" - ); - assert_eq!(token.value, "ek_test123"); - server.shutdown().await; -} - -#[tokio::test] -async fn ttl_under_floor_is_clamped_to_min() { - let (server, state) = mock_returning( - serde_json::json!({ "value": "ek_test123", "expires_at": 1_700_000_600u64 }), - ) - .await; - minter(&server, Some(1)) - .mint(&locked_config()) - .await - .expect("mint succeeds"); - - assert_eq!(recorded_ttl_secs(&state), 10, "under-floor clamps to 10"); - server.shutdown().await; -} - -#[tokio::test] -async fn ttl_unset_defaults_to_600() { - let (server, state) = mock_returning( - serde_json::json!({ "value": "ek_test123", "expires_at": 1_700_000_600u64 }), - ) - .await; - minter(&server, None) - .mint(&locked_config()) - .await - .expect("mint succeeds"); - - assert_eq!(recorded_ttl_secs(&state), 600, "unset defaults to 600"); - server.shutdown().await; -} - -#[tokio::test] -async fn safety_identifier_header_is_stamped() { - let (server, state) = mock_returning( - serde_json::json!({ "value": "ek_test123", "expires_at": 1_700_000_600u64 }), - ) - .await; - minter(&server, None) - .mint(&locked_config()) - .await - .expect("mint succeeds"); - - assert_eq!( - state - .get_last_request_header("OpenAI-Safety-Identifier") - .as_deref(), - Some(SAFETY_ID), - "the caller-identity binding is stamped on the mint request" - ); - server.shutdown().await; -} - -#[tokio::test] -async fn returns_the_ek_value_and_expiry() { - let (server, _state) = mock_returning( - serde_json::json!({ "value": "ek_livetoken789", "expires_at": 1_700_000_600u64 }), - ) - .await; - let token = minter(&server, None) - .mint(&locked_config()) - .await - .expect("mint succeeds"); - - assert_eq!(token.value, "ek_livetoken789"); - assert_eq!(token.expires_at_unix, 1_700_000_600); - server.shutdown().await; -} - -#[tokio::test] -async fn value_without_ek_prefix_is_refused() { - let (server, _state) = mock_returning( - serde_json::json!({ "value": "sk-not-an-ephemeral-secret", "expires_at": 1_700_000_600u64 }), - ) - .await; - let err = minter(&server, None) - .mint(&locked_config()) - .await - .expect_err("a non-ek_ value is refused"); - - match err { - MintError::Provider(m) => assert!( - m.contains("ek_"), - "the refusal names the missing ek_ prefix, got: {m}" - ), - } - server.shutdown().await; -} - -#[tokio::test] -async fn the_real_key_never_appears_in_the_token() { - let (server, _state) = mock_returning( - serde_json::json!({ "value": "ek_test123", "expires_at": 1_700_000_600u64 }), - ) - .await; - let token = minter(&server, None) - .mint(&locked_config()) - .await - .expect("mint succeeds"); - - assert!( - !token.value.contains(REAL_KEY), - "the real provider key is never in the browser-facing token value" - ); - assert_eq!(token.value, "ek_test123"); - server.shutdown().await; -} diff --git a/crates/busbar-voice/src/topology/tests/mod.rs b/crates/busbar-voice/src/topology/tests/mod.rs deleted file mode 100644 index 9689354a4a..0000000000 --- a/crates/busbar-voice/src/topology/tests/mod.rs +++ /dev/null @@ -1,718 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! TOPOLOGY TESTS (behind `runtime`): the telephony proxy relays both ways over in-memory sockets and -//! tears down on hard-close; the WebRTC sideband mints a token, governs the locked config, and relays -//! NO media (media is peer-to-peer). - -use crate::ir::codec::OpenAiRealtimeCodec; -use crate::ir::config::SessionConfig; -use crate::runtime::carrier::Carrier; -use crate::runtime::metering::TurnMeter; -use crate::runtime::VoiceRuntime; -use crate::topology::telephony::{begin_telephony, g711_config}; -use crate::topology::webrtc::{attach, EphemeralToken, MintError, TokenMinter}; -use busbar_kernel::plane::handle_engine::DurableHandleEngine; -use futures::channel::mpsc::unbounded; -use futures::StreamExt; -use std::sync::Arc; -// Test-support-only: the guarded provider-dial battery (dial_provider through the net-guard) and its -// loopback EchoProvider. These symbols are used ONLY by the `#[cfg(feature = "test-support")]` dial -// tests, so gate the imports to match — otherwise a `runtime`-without-`test-support` build (the -// workspace clippy default now that voice ships default-on) sees them as unused. -use crate::testkit::fixture_host::FixtureHost; -#[cfg(feature = "test-support")] -use crate::topology::dial_provider; -#[cfg(feature = "test-support")] -use busbar_kernel::ingress::byte_duplex::{CallRef, DuplexHandle, DuplexPlane}; -#[cfg(feature = "test-support")] -use busbar_kernel::ingress::duplex_ws as ws_ingress; -#[cfg(feature = "test-support")] -use busbar_kernel::proxy::egress_unit::trust::net::GuardPolicy; -#[cfg(feature = "test-support")] -use futures::SinkExt; - -fn runtime() -> VoiceRuntime { - VoiceRuntime::new( - Arc::new(DurableHandleEngine::new()), - Arc::new(crate::runtime::tools::EchoToolExecutor), - ) -} - -/// A presenting key's meter over a governed fixture host whose budget view is capped at `cap` counts. -fn meter_capped(cap: i64) -> TurnMeter { - TurnMeter::new( - Arc::new(FixtureHost::new().governed().with_count_cap(cap)), - busbar_contract::records::VirtualKey { - id: "vk".to_string(), - ..Default::default() - }, - "streaming-server", - crate::OPENAI_REALTIME, - ) -} - -fn json_frame(v: serde_json::Value) -> Vec { - serde_json::to_vec(&v).unwrap() -} - -// ── Topology B: the thin telephony proxy relays both directions ───────────────────────────────── - -#[tokio::test] -async fn telephony_proxy_relays_both_directions() { - let rt = runtime(); - // g711 end-to-end: 8 kHz µ-law passes straight through, no resample. - let cfg = g711_config(); - assert_eq!( - cfg.output_audio_format, - Some(crate::ir::media::AudioFormat::G711Ulaw) - ); - let proxy = begin_telephony(&rt, OpenAiRealtimeCodec, "acct-1", "call-9", cfg, None, 1) - .expect("telephony begins"); - - let (prov_in_tx, prov_in_rx) = unbounded::>(); - let (prov_out_tx, mut prov_out_rx) = unbounded::>(); - let (cli_in_tx, cli_in_rx) = unbounded::>(); - let (cli_out_tx, mut cli_out_rx) = unbounded::>(); - - // The provider emits a downlink audio frame → it must reach the client. - prov_in_tx - .unbounded_send(json_frame(serde_json::json!({ - "type":"response.output_audio.delta","delta":"AAAA" - }))) - .unwrap(); - // The client (phone) sends an uplink audio frame → it must reach the provider. - cli_in_tx - .unbounded_send(json_frame(serde_json::json!({ - "type":"input_audio_buffer.append","audio":"BBBB" - }))) - .unwrap(); - // EOF both sockets so the proxy returns cleanly. - drop(prov_in_tx); - drop(cli_in_tx); - - proxy - .run(prov_in_rx, prov_out_tx, cli_in_rx, cli_out_tx) - .await; - - // Downlink: the client received the provider's audio. - cli_out_rx.close(); - let mut downlink = Vec::new(); - while let Some(f) = cli_out_rx.next().await { - let v: serde_json::Value = serde_json::from_slice(&f).unwrap(); - downlink.push(v["type"].as_str().unwrap().to_string()); - } - assert!( - downlink.contains(&"response.output_audio.delta".to_string()), - "provider downlink audio reached the client: {downlink:?}" - ); - - // Uplink: the provider received the client's forwarded audio append. - prov_out_rx.close(); - let mut uplink = Vec::new(); - while let Some(f) = prov_out_rx.next().await { - let v: serde_json::Value = serde_json::from_slice(&f).unwrap(); - uplink.push(v["type"].as_str().unwrap().to_string()); - } - assert!( - uplink.contains(&"input_audio_buffer.append".to_string()), - "client uplink audio reached the provider: {uplink:?}" - ); -} - -/// Item 137: a telephony call that ends settles its durable row terminal and evicts it. The proxy -/// used to bind its handle as `_handle` and drop it unsettled, so every call's row stayed ACTIVE in -/// the working set after both sockets were gone. -#[tokio::test] -async fn a_finished_telephony_call_settles_and_evicts_its_durable_row() { - let rt = runtime(); - let proxy = begin_telephony( - &rt, - OpenAiRealtimeCodec, - "acct-1", - "call-ended", - g711_config(), - None, - 1, - ) - .expect("telephony begins"); - let (prov_in_tx, prov_in_rx) = unbounded::>(); - let (prov_out_tx, _prov_out_rx) = unbounded::>(); - let (cli_in_tx, cli_in_rx) = unbounded::>(); - let (cli_out_tx, _cli_out_rx) = unbounded::>(); - // Both sockets end at once: the call is over. - drop(prov_in_tx); - drop(cli_in_tx); - proxy - .run(prov_in_rx, prov_out_tx, cli_in_rx, cli_out_tx) - .await; - assert!( - crate::runtime::SessionHandle::bind(Arc::clone(&rt.engine), "acct-1", "call-ended") - .get() - .is_none(), - "the ended call's durable row is settled terminal and evicted" - ); -} - -// ── The provider WSS dials THROUGH the neutral guarded transport (HARD RULE 3) ─────────────────── - -/// A loopback echo "provider" served over the neutral WS ingress acceptor — stands in for the Realtime -/// upstream so the plane's `dial_provider` can be driven end to end without a network. Test-support-only -/// (the guarded-dial battery it feeds is `#[cfg(feature = "test-support")]`). -#[cfg(feature = "test-support")] -struct EchoProvider; - -#[cfg(feature = "test-support")] -#[async_trait::async_trait] -impl DuplexPlane for EchoProvider { - fn classify(&self, _frame: &[u8]) -> Option { - None - } - async fn handle(self: Arc, frame: Vec, out: DuplexHandle) { - // The echo is what these tests observe, so a write that did not land is reported rather - // than read later as a frame the provider never sent. - if let Err(e) = out.emit(frame).await { - eprintln!("echo provider: the frame could not be written: {e}"); - } - } -} - -#[cfg(feature = "test-support")] -async fn spawn_echo_provider() -> std::net::SocketAddr { - async fn route(upgrade: axum::extract::ws::WebSocketUpgrade) -> axum::response::Response { - ws_ingress::serve(upgrade, Arc::new(EchoProvider)) - } - let app = axum::Router::new().route("/", axum::routing::get(route)); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - addr -} - -/// The plane SELECTS `Transport::WebSocket` and lets the substrate open the socket: `dial_provider` -/// dials the loopback provider THROUGH the net-guard and a frame crosses both directions. This is the -/// plane using the neutral transport instead of carrying its own socket plumbing. Gated on -/// `test-support` because the governed dial now rides the breaker beneath it, reached through the -/// substrate's in-memory fixture host. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn dial_provider_routes_through_the_guarded_ws_transport() { - let addr = spawn_echo_provider().await; - let url = format!("ws://{addr}/"); - let policy = GuardPolicy { - allow_private: true, - allow_plaintext: true, - ..GuardPolicy::default() - }; - let host = FixtureHost::new(); - let pool = crate::topology::stream_breaker_key("openai-realtime"); - let (mut stream, mut sink) = dial_provider(&host, &pool, 0, &url, policy, None) - .await - .expect("the plane dials the provider through the guarded transport"); - sink.send(b"realtime-frame".to_vec()).await.ok(); - let got = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next()) - .await - .expect("a reply arrived") - .expect("the stream yielded a frame"); - assert_eq!( - got, b"realtime-frame", - "a frame crossed the guarded WS both ways" - ); -} - -/// The provider dial FAILS CLOSED on a guard-failing target — the plane never opens a socket the -/// net-guard did not pin (the egress-audit finding this closes). -#[cfg(feature = "test-support")] -#[tokio::test] -async fn dial_provider_fails_closed_on_a_guarded_target() { - // A public loopback under the fail-closed default is an internal address — refused, no socket. - let host = FixtureHost::new(); - let pool = crate::topology::stream_breaker_key("openai-realtime"); - assert!( - dial_provider( - &host, - &pool, - 0, - "wss://127.0.0.1/", - GuardPolicy::default(), - None - ) - .await - .is_err(), - "the provider dial must refuse an unpinned/guard-failing target" - ); -} - -// ── The provider key: on the upgrade request only, in no dial error ───────────────────────────── - -/// The endpoint a provider-dial cell dials: `base_url` with the key `SEKRET123`. -#[cfg(feature = "test-support")] -fn sekret_endpoint(base_url: &str) -> crate::mount::ProviderEndpoint { - crate::mount::ProviderEndpoint { - base_url: base_url.to_string(), - api_key: busbar_contract::Redacted::new(SEKRET.to_string()), - } -} - -#[cfg(feature = "test-support")] -const SEKRET: &str = "SEKRET123"; - -/// THE PROVIDER KEY IS IN NO DIAL ERROR. A `base_url` the dialer cannot parse fails the dial at the -/// URL check; the error that comes back, and the line the failed-dial arm logs from it, must not carry -/// the deployment's provider key in either rendering. Before the key moved out of the URL the dialer -/// quoted the keyed target back verbatim. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn a_failed_provider_dial_error_carries_no_provider_key() { - let host = FixtureHost::new(); - for dialect in [crate::GEMINI_LIVE, crate::OPENAI_REALTIME] { - let ep = sekret_endpoint("https://bad host"); - let pool = crate::topology::stream_breaker_key(dialect); - let err = dial_provider( - &host, - &pool, - 0, - &ep.ws_url(dialect), - GuardPolicy::default(), - Some(ep.ws_credential(dialect)), - ) - .await - .err() - .expect("a base_url with a space in its host cannot be dialed"); - let shown = err.to_string(); - let debug = format!("{err:?}"); - assert!( - !shown.contains(SEKRET) && !debug.contains(SEKRET), - "{dialect}: the dial error carries the provider key: {shown} / {debug}" - ); - assert!( - shown.contains("bad host"), - "{dialect}: the error still names the target an operator has to fix: {shown}" - ); - } -} - -/// What a loopback provider saw on the upgrade request: the request target and the -/// `authorization` header. -#[cfg(feature = "test-support")] -type SeenUpgrade = Arc)>>>; - -/// A loopback WebSocket "provider" that records the upgrade request's target and `authorization` -/// header, then serves a socket that answers nothing. -#[cfg(feature = "test-support")] -async fn spawn_recording_provider() -> (std::net::SocketAddr, SeenUpgrade) { - struct Silent; - #[async_trait::async_trait] - impl DuplexPlane for Silent { - fn classify(&self, _frame: &[u8]) -> Option { - None - } - async fn handle(self: Arc, _frame: Vec, _out: DuplexHandle) {} - } - async fn route( - axum::extract::State(seen): axum::extract::State, - uri: axum::http::Uri, - headers: axum::http::HeaderMap, - upgrade: axum::extract::ws::WebSocketUpgrade, - ) -> axum::response::Response { - let auth = headers - .get("authorization") - .and_then(|v| v.to_str().ok()) - .map(str::to_string); - *seen.lock().unwrap() = Some((uri.to_string(), auth)); - ws_ingress::serve(upgrade, Arc::new(Silent)) - } - let seen: SeenUpgrade = Arc::new(std::sync::Mutex::new(None)); - let app = axum::Router::new() - .fallback(route) - .with_state(Arc::clone(&seen)); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - tokio::spawn(async move { - axum::serve(listener, app).await.unwrap(); - }); - (addr, seen) -} - -/// THE WIRE IS UNCHANGED FOR GEMINI LIVE, AND OPENAI REALTIME NOW AUTHENTICATES. The key no longer -/// rides the URL the plane builds, but the upgrade request the provider receives still carries it -/// where each dialect reads it: Gemini Live's `?key=` on the request target (the bytes it always -/// received), OpenAI Realtime's `Authorization: Bearer` header (which its dial never sent before). -/// Loopback is private and plaintext, so the dial needs a guard that admits both. -#[cfg(feature = "test-support")] -#[tokio::test] -async fn the_provider_receives_its_key_where_its_dialect_reads_it() { - let admit_loopback = GuardPolicy { - allow_private: true, - allow_plaintext: true, - ..GuardPolicy::default() - }; - let host = FixtureHost::new(); - for (dialect, want_target, want_auth) in [ - ( - crate::GEMINI_LIVE, - "/ws/google.ai.generativelanguage.v1beta.GenerativeService.BidiGenerateContent\ - ?key=SEKRET123", - None, - ), - ( - crate::OPENAI_REALTIME, - "/v1/realtime", - Some("Bearer SEKRET123".to_string()), - ), - ] { - let (addr, seen) = spawn_recording_provider().await; - let ep = sekret_endpoint(&format!("http://{addr}")); - assert!( - !ep.ws_url(dialect).contains(SEKRET), - "{dialect}: the URL the plane builds is keyless" - ); - let pool = crate::topology::stream_breaker_key(dialect); - let dialed = dial_provider( - &host, - &pool, - 0, - &ep.ws_url(dialect), - admit_loopback, - Some(ep.ws_credential(dialect)), - ) - .await; - assert!(dialed.is_ok(), "{dialect}: the loopback provider upgrades"); - let got = seen.lock().unwrap().clone(); - assert_eq!( - got, - Some((want_target.to_string(), want_auth)), - "{dialect}: the provider's upgrade request" - ); - } -} - -// ── Topology A: the WebRTC sideband mints a token and relays no media ──────────────────────────── - -struct FakeMinter { - fail: bool, -} - -#[async_trait::async_trait] -impl TokenMinter for FakeMinter { - async fn mint(&self, config: &SessionConfig) -> Result { - if self.fail { - return Err(MintError::Provider("endpoint down".into())); - } - // The minted secret is scoped to the SAME locked config busbar governs. - assert_eq!(config.instructions.as_deref(), Some("be helpful")); - Ok(EphemeralToken { - value: "ek_test_secret".into(), - expires_at_unix: 9_999, - }) - } -} - -#[tokio::test] -async fn webrtc_sideband_mints_token_locks_config_and_relays_no_media() { - let rt = runtime(); - let minter = FakeMinter { fail: false }; - let mut locked = SessionConfig { - instructions: Some("be helpful".into()), - ..SessionConfig::default() - }; - locked.tools = vec![serde_json::json!({"type":"function","name":"lookup"})]; - - let attached = attach( - &rt, - &minter, - OpenAiRealtimeCodec, - "acct-2", - "call-42", - locked, - Some(meter_capped(3)), - 1, - ) - .await - .expect("sideband attaches"); - - assert_eq!(attached.token.value, "ek_test_secret"); - // The sideband carrier relays NO media (the browser's media path is peer-to-peer). - assert!( - !attached.core.carrier().send_downlink(vec![1, 2, 3]), - "sideband relays no downlink media" - ); - - // Metering is real over the sideband too: a turn that dries a chain of 3 hard-closes. - let usage = crate::ir::codec::WireEvent(bytes::Bytes::from(json_frame(serde_json::json!({ - "type":"response.done", - "response": { "usage": { "output_token_details": { "audio_tokens": 5 } } }, - })))); - let plan = attached.core.on_server_frame(usage).await; - assert!(plan.close, "sideband session hard-closes on exhaustion"); - assert!(attached.core.carrier().is_closed()); -} - -// ── begin_session: past the open-pass gate, the budget is asked before anything opens ───────────────── - -#[test] -fn begin_session_asks_the_budget_past_the_gate_and_opens_only_with_room() { - // A session proceeds past the open-pass gate to the budget check: a dry chain is refused there, - // and a chain with room opens. - let rt = runtime(); - let ok_cfg = SessionConfig { - model: Some("allowed-model".into()), - ..SessionConfig::default() - }; - let open = |call: &str, cap| { - crate::topology::begin_session( - &rt, - OpenAiRealtimeCodec, - "acct", - call, - Some(ok_cfg.clone()), - Carrier::sideband(), - Some(meter_capped(cap)), - 1, - ) - }; - assert!(matches!( - open("call-dry", 0), - Err(crate::topology::StartError::BudgetRefused) - )); - assert!( - open("call-ok", 10).is_ok(), - "an allowed destination with room opens" - ); -} - -/// A TEST-ONLY open-pass gate that refuses every session. The plane's own [`SessionGauntlet`] keeps no -/// destination policy (1.5.5 had none), so nothing in production can plant a refusal; this stands in -/// for whatever may refuse at the gate (a kernel-loop session runner) without resurrecting a policy. -/// -/// [`SessionGauntlet`]: crate::topology::SessionGauntlet -struct RefusingGate; - -#[async_trait::async_trait] -impl super::GauntletPlane for RefusingGate { - fn verify_destination(&self, _req: &super::GauntletRequest<'_>) -> super::VerifyOutcome { - super::VerifyOutcome::Refuse( - axum::response::Response::builder() - .status(axum::http::StatusCode::FORBIDDEN) - .body(axum::body::Body::empty()) - .expect("static refusal builds"), - ) - } - - async fn drive(self: Box, _req: super::GauntletRequest<'_>) -> axum::response::Response { - unreachable!("the session path only runs the admission gate") - } -} - -/// THE D3 ORDERING WITNESS: `begin_session` runs the open-pass gate at the TOP, so a session the gate -/// refuses is refused BEFORE the kernel account's budget is asked — the caller here has a DRY chain, -/// and the answer is still the gate's refusal, not the budget's. RED if the budget is asked first. -#[test] -fn begin_session_refuses_at_the_gauntlet_before_the_budget_is_asked() { - let r = crate::topology::begin_session_through( - &runtime(), - Box::new(RefusingGate), - OpenAiRealtimeCodec, - "acct", - "call-refused", - Some(SessionConfig { - model: Some("any-model".into()), - ..SessionConfig::default() - }), - Carrier::sideband(), - Some(meter_capped(0)), - 1, - ); - assert!( - matches!(r, Err(crate::topology::StartError::DestinationRefused)), - "a gate-refused session is refused at the open-pass gate, before the budget is asked" - ); -} - -// ── A SESSION HOLDS NOTHING OPEN (the D2 lease-leak witness, retired with the lease) ───────────────── - -/// The lease this replaced held a reserve that only a by-value guard could release when a parked -/// handler pinned the core. A session now holds no reserve at all: each turn's counts reach the ledger -/// as the turn closes, so a pinned core has nothing left to release and nothing waits on its drop. -#[tokio::test] -async fn a_pinned_core_holds_nothing_open() { - let host = Arc::new(FixtureHost::new().governed()); - let meter = TurnMeter::new( - Arc::clone(&host) as Arc, - busbar_contract::records::VirtualKey { - id: "vk-pin".to_string(), - ..Default::default() - }, - "streaming-server", - crate::OPENAI_REALTIME, - ); - let (core, _handle) = crate::topology::begin_session( - &runtime(), - OpenAiRealtimeCodec, - "acct", - "call-pin", - None, - Carrier::sideband(), - Some(meter), - 1, - ) - .expect("session begins"); - let usage = crate::ir::codec::WireEvent(bytes::Bytes::from(json_frame(serde_json::json!({ - "type":"response.done", - "response": { "usage": { "output_token_details": { "audio_tokens": 4 } } }, - })))); - let _ = core.on_server_frame(usage).await; - let pinned = Arc::clone(&core); - drop(core); - assert_eq!( - host.ledger_usage("vk-pin").map(|u| u.tokens), - Some(4), - "the turn is ledgered as it closed — nothing waits on the core's drop" - ); - drop(pinned); - assert_eq!(host.ledger_usage("vk-pin").map(|u| u.tokens), Some(4)); -} - -/// A minter that RECORDS whether it was ever asked to mint — the witness for the ordering fix: on a -/// refused governed open NOTHING may mint. -struct RecordingMinter { - minted: std::sync::atomic::AtomicBool, -} - -#[async_trait::async_trait] -impl TokenMinter for RecordingMinter { - async fn mint(&self, _config: &SessionConfig) -> Result { - self.minted.store(true, std::sync::atomic::Ordering::SeqCst); - Ok(EphemeralToken { - value: "ek_should_never_be_minted".into(), - expires_at_unix: 1, - }) - } -} - -/// THE ORDERING FIX: `attach` runs the gauntlet + the budget check FIRST and mints the ephemeral secret -/// only past a clean open. A session the open refuses (here: a dry budget chain) is refused BEFORE any -/// mint — so the browser is handed NO `ek_` on a refused session (zero bytes, zero charge, zero -/// credential). RED before the reorder: the mint ran before `begin_session`, so a refused session still -/// minted a secret. -#[tokio::test] -async fn the_open_refuses_before_the_mint_on_a_dry_budget() { - let rt = runtime(); - let minter = RecordingMinter { - minted: std::sync::atomic::AtomicBool::new(false), - }; - let locked = SessionConfig { - model: Some("allowed-model".into()), - ..SessionConfig::default() - }; - let r = attach( - &rt, - &minter, - OpenAiRealtimeCodec, - "acct", - "call-dry", - locked, - Some(meter_capped(0)), - 1, - ) - .await; - assert!( - matches!( - r, - Err(crate::topology::webrtc::AttachError::Start( - crate::topology::StartError::BudgetRefused - )) - ), - "a dry chain is refused at the open, before the mint" - ); - assert!( - !minter.minted.load(std::sync::atomic::Ordering::SeqCst), - "NOTHING mints on a refused session — the open runs before the mint" - ); -} - -#[tokio::test] -async fn webrtc_attach_fails_closed_when_mint_fails() { - let rt = runtime(); - let minter = FakeMinter { fail: true }; - let r = attach( - &rt, - &minter, - OpenAiRealtimeCodec, - "acct-3", - "call-x", - SessionConfig::default(), - None, - 1, - ) - .await; - assert!(r.is_err(), "a failed mint refuses the session"); -} - -/// A FAILED WEBRTC MINT REFUNDS ITS SESSION FEE EXACTLY ONCE (TODO 17(b), ARCHITECT R4): `attach` -/// opens the governed session, counting its fee at the open under the same dry check, then mints; a -/// mint that fails hands the browser no secret, so the session never opened and its fee is given back -/// through the governance refund. The metering row keeps the count (the refund adjusts the budget -/// book only); a kept session attached first on the same key proves one fee comes back and not two. -/// RED with the fee counted at `served()`: the failed attach counted nothing. -#[tokio::test] -async fn a_failed_webrtc_mint_refunds_its_session_fee_exactly_once() { - let host = Arc::new(FixtureHost::new().governed().with_count_cap(1_000)); - let key = busbar_contract::records::VirtualKey { - id: "vk-webrtc-fee".to_string(), - ..Default::default() - }; - let meter = || { - let host: Arc = Arc::clone(&host); - TurnMeter::new( - host, - key.clone(), - "streaming-server", - crate::OPENAI_REALTIME, - ) - }; - let rt = runtime(); - let locked = SessionConfig { - instructions: Some("be helpful".into()), - ..SessionConfig::default() - }; - let _kept = attach( - &rt, - &FakeMinter { fail: false }, - OpenAiRealtimeCodec, - "acct", - "call-kept", - locked.clone(), - Some(meter()), - 1, - ) - .await - .expect("the kept session attaches"); - let failed = attach( - &rt, - &FakeMinter { fail: true }, - OpenAiRealtimeCodec, - "acct", - "call-mint-fails", - locked, - Some(meter()), - 1, - ) - .await; - assert!( - matches!(failed, Err(crate::topology::webrtc::AttachError::Mint(_))), - "the premise: the mint failed after the governed open" - ); - assert_eq!( - host.session_rows(&key.id), - 2, - "the failed attach counted its session fee at the open, under the dry check" - ); - assert_eq!( - host.ledger_usage(&key.id).map_or(0, |u| u.sessions), - 1, - "the failed mint's fee is given back exactly once; the kept session's stays" - ); -} diff --git a/crates/busbar-voice/src/topology/webrtc.rs b/crates/busbar-voice/src/topology/webrtc.rs deleted file mode 100644 index a8a5793675..0000000000 --- a/crates/busbar-voice/src/topology/webrtc.rs +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! TOPOLOGY A — the BROWSER WebRTC SIDEBAND (design `BUSBAR-1.6.0.md` #18/#45). -//! -//! busbar attaches to the session over a persistent WSS keyed by `call_id`, OWNING tools + instructions -//! (the locked [`SessionConfig`]). It mints the ephemeral client-secret the browser uses to establish -//! the media path DIRECTLY with the provider: media flows peer-to-peer, and only auth + the SDP -//! handshake + the sideband control transit busbar. busbar's role is MINT/GUARD + sideband control, -//! NOT a media relay — modelled here by a [`Carrier::sideband`] that relays no downlink audio. - -use crate::ir::codec::{DuplexReader, DuplexWriter}; -use crate::ir::config::SessionConfig; -use crate::runtime::carrier::Carrier; -use crate::runtime::scope::SessionHandle; -use crate::runtime::session::{SessionCore, VoiceSession}; -use crate::runtime::VoiceRuntime; -use crate::topology::{begin_session, StartError}; -use async_trait::async_trait; -use std::sync::Arc; - -/// AN EPHEMERAL CLIENT SECRET the browser presents to the provider to open its own media session — the -/// short-lived token busbar mints so the long-lived provider key never reaches the browser. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct EphemeralToken { - /// The opaque secret value handed to the browser. - pub value: String, - /// Unix seconds after which the secret is rejected. - pub expires_at_unix: u64, -} - -/// Why minting an ephemeral client secret failed. -#[derive(Debug)] -pub enum MintError { - /// The provider's client-secret endpoint refused or was unreachable. - Provider(String), -} - -impl std::fmt::Display for MintError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MintError::Provider(m) => write!(f, "ephemeral token mint failed: {m}"), - } - } -} - -impl std::error::Error for MintError {} - -/// MINTS the ephemeral client secret via the provider's client-secret endpoint — dependency-inverted -/// so the composition root binds the real HTTPS call (no network dep leaks into the plane) while tests -/// bind a fake. The minted secret is scoped to the SAME locked config busbar governs. -#[async_trait] -pub trait TokenMinter: Send + Sync { - /// Mint an ephemeral client secret for a browser session governed by `config`. - async fn mint(&self, config: &SessionConfig) -> Result; -} - -/// Why attaching a sideband session failed. -#[derive(Debug)] -pub enum AttachError { - /// The session could not begin (lease refused / durable open failed). - Start(StartError), - /// The ephemeral token could not be minted. - Mint(MintError), -} - -impl std::fmt::Display for AttachError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AttachError::Start(e) => write!(f, "{e}"), - AttachError::Mint(e) => write!(f, "{e}"), - } - } -} - -impl std::error::Error for AttachError {} - -/// AN ATTACHED SIDEBAND SESSION — everything busbar holds after the sideband is bound: the ephemeral -/// token to hand the browser, the [`VoiceSession`] plane to serve over the sideband WSS, its shared -/// [`SessionCore`], and the durable [`SessionHandle`] to close at teardown. -pub struct Attached { - /// The ephemeral client secret to return to the browser. - pub token: EphemeralToken, - /// The sideband control plane — serve it over the persistent WSS the plane opens THROUGH the neutral - /// guarded WS transport ([`crate::topology::dial_provider`] → `serve_messages`); the plane holds no - /// socket of its own. - pub session: VoiceSession, - /// The shared session core (metering, tools, barge-in). - pub core: Arc>, - /// The durable session binding to close at teardown. - pub handle: SessionHandle, -} - -/// ATTACH a browser WebRTC sideband session: lock the plane's `instructions` + `tools` into the -/// [`SessionConfig`], begin the governed session (kernel account + durable handle), and mint the ephemeral token -/// scoped to that locked config. The returned [`Attached::session`] is served over the persistent WSS; -/// media never transits busbar (the sideband carrier relays no audio). -#[allow(clippy::too_many_arguments)] -pub async fn attach( - rt: &VoiceRuntime, - minter: &M, - codec: C, - owner: impl Into, - call_id: impl Into, - locked_config: SessionConfig, - meter: Option, - now: u64, -) -> Result, AttachError> -where - C: DuplexReader + DuplexWriter + Send + Sync + 'static, - M: TokenMinter, -{ - // GOVERN FIRST, MINT SECOND (verify-strictly-before-charge, and before any credential is issued): - // begin_session runs the shared open-pass gauntlet gate + opens the kernel account + the durable - // handle. NOTHING is minted on a gate- or budget-refused session — a refused open costs zero bytes, - // zero charge, and hands the browser NO ephemeral secret. The mint runs ONLY past a clean open. - // A sideband carrier: no downlink media relay — the browser's media path is peer-to-peer. - let carrier = Carrier::sideband(); - let (core, handle) = begin_session( - rt, - codec, - owner, - call_id, - Some(locked_config.clone()), - carrier, - meter, - now, - ) - .map_err(AttachError::Start)?; - - // Only past the governed open: mint the ephemeral secret scoped to the SAME config busbar locked and - // re-applies. A mint failure tears the just-opened session down through the returned handle - // drop rather than leaking a governed-but-unusable session, and gives back the session fee its - // open counted (TODO 17(b), ARCHITECT R4): a session that never opened charges nothing. - let token = match minter.mint(&locked_config).await { - Ok(token) => token, - Err(e) => { - core.refund_open(); - return Err(AttachError::Mint(e)); - } - }; - // The secret is out: the session is served, and the fee its open counted is final. - core.served(); - - let session = VoiceSession::new(Arc::clone(&core)); - Ok(Attached { - token, - session, - core, - handle, - }) -} diff --git a/crates/busbar-voice/tests/compose_governs_every_session.rs b/crates/busbar-voice/tests/compose_governs_every_session.rs deleted file mode 100644 index 4f899fc7e5..0000000000 --- a/crates/busbar-voice/tests/compose_governs_every_session.rs +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! **The plane composes its own open-call table.** Its own binary, because the table behind the -//! served door is set once per process: a cell sharing a test binary with the plane's other -//! composition cells would race them for the first write. - -#![cfg(feature = "runtime")] - -/// After this plane's compose step, the served door binds every session it opens to the node's -/// table, two sessions are told apart before either has a call open, and a second compose is a -/// no-op rather than a silent swap of the table live sessions are keyed into. -#[test] -fn the_planes_compose_step_governs_every_served_session() { - assert!( - busbar_voice::mount::served_governed_session().is_none(), - "before the compose step nothing is bound" - ); - assert!( - busbar_voice::mount::compose_node_calls(), - "the plane's compose step writes the table" - ); - assert!( - !busbar_voice::mount::compose_node_calls(), - "and a second compose never swaps it" - ); - let bound = busbar_voice::mount::served_governed_session() - .expect("after the compose step, every session the door opens is bound to a table"); - let next = busbar_voice::mount::served_governed_session().expect("and so is the next one"); - assert_ne!( - bound.session, next.session, - "two conversations are told apart before either has a call open" - ); - assert!( - bound.calls.planned(bound.session, "call_aaa", 0), - "the table enters a planned client-served leg" - ); - assert_eq!( - next.calls.replied(next.session, "call_aaa"), - Err(busbar_voice::runtime::ReplyRefusal::NoSuchSession), - "and keys it by the session that planned it: the same identifier on another session wakes nothing" - ); - assert_eq!(bound.calls.replied(bound.session, "call_aaa"), Ok(())); -} diff --git a/crates/busbar/Cargo.toml b/crates/busbar/Cargo.toml index dcddd8b2d3..34bd5faa6c 100644 --- a/crates/busbar/Cargo.toml +++ b/crates/busbar/Cargo.toml @@ -68,13 +68,6 @@ busbar-plane-mcp = { path = "../busbar-plane-mcp", optional = true } # — nothing in `main.rs` names it and `plane-a2a` below is not in `default`, so this edge is inert # until the plane/codec code that a later step moves actually lands here. busbar-a2a = { path = "../busbar-a2a", optional = true } -# THE VOICE PLANE-KIND CRATE (Plane 4), the analogue of busbar-a2a above. Optional behind -# `plane-streaming`, now IN `default` (voice ships default-ON + deletable, exactly like plane-mcp/ -# plane-a2a). The LLM money path is unchanged (busbar-core's default carries no voice/duplex-ws -# edge); the shipped binary gains the voice plane + its WS transport, and `--no-default` proves the -# whole edge disappears with the crate (strong-form deletable). The `busbar-voice/runtime` edge is -# carried by the feature (below), not here. -busbar-voice = { path = "../busbar-voice", optional = true } # Boot-time plugin listing (`--plugins`) reads the registry the loader builds. busbar-plugin-loader = { path = "../plugin-loader" } @@ -96,9 +89,10 @@ busbar-kernel-breaker = { path = "../busbar-kernel-breaker" } busbar-kernel-wal = { path = "../busbar-kernel-wal" } busbar-kernel-audit = { path = "../busbar-kernel-audit" } # The pure planes reach the boot seal through their host's linked entry (`PLANE` + `CLAIMS` on the -# `claims` axis), so the root names none of them. The streaming plane's edge stays for the root's own -# streaming unit, which builds its node over the pure plane. -busbar-plane-streaming = { path = "../busbar-plane-streaming" } +# `claims` axis), so the root names none of them. The streaming plane's edge is the crate the +# `plane-streaming` switch's `plane-door` row names: its memory-ABI door, served through the loader's +# one load like every dropped-in plane door (FLIP-STREAMING). +busbar-plane-streaming = { path = "../busbar-plane-streaming", optional = true } # THE DECISIONS PLANE (jev), #48's fifth: the edge the `plane-decisions` switch's `plane-door` row # names. The plane is served through its memory-ABI door (`busbar_plane_decisions::plane_door::door`), # loaded through the loader's one load like every dropped-in plane door (FLIP-DECISIONS), so its @@ -253,16 +247,16 @@ plane-mcp = ["plane-mcp-door", "busbar-kernel/plane-mcp", "busbar-core-admin/pla # section when off, so `--no-default-features` drops the `busbar-a2a` edge and compiles the whole A2A # plane out while core still routes LLM + MCP (the plane-split deletion test). plane-a2a = ["dep:busbar-a2a", "busbar-kernel/plane-a2a", "busbar-core-admin/plane-a2a"] -# THE VOICE PLANE, the busbar-voice crate, behind this switch. The analogue of `plane-a2a` — one -# switch for the crate edge + the core/substrate forwards — and, like it, IN `default` (voice ships -# default-ON + deletable; `--no-default-features` compiles it out). `dep:busbar-voice` pulls the crate; `busbar-voice?/runtime` turns on its live-session -# runtime (the same feature it would be under `busbar-voice/runtime`, since `dep:busbar-voice` here -# enables the crate). The `?/` spelling is deliberate: it makes the forward an OPTIONAL-dep feature -# reference, so the strong-form deletion gate (scripts/plane-delete-test.sh) strips it with the same -# rule it strips `busbar-voice?/openapi-schema`, leaving voice byte-for-byte removable. Compiling the -# runtime here gives the `build_runtime` body; the skeleton build leaves `build_runtime: None`. -plane-streaming = ["dep:busbar-voice", "transport-ws", "busbar-voice?/runtime", "busbar-kernel/duplex-ws"] -# THE WS WIRE, the one optional transport: the voice plane's edge and nothing else's, so `plane-streaming` +# THE STREAMING PLANE, through its memory-ABI door (FLIP-STREAMING; BUSBAR-1.6.0.md Part 3 section 12, +# "The switch": the fold's development-only switch is deleted when the fold completes, and the +# plane's one switch carries its door). IN `default`; `--no-default-features` compiles it out. On, the +# plane's door (`busbar_plane_streaming::door::door`) is a `plane-door` row: the root hands it to the +# loader beside every dropped-in plane door and binds it through the one load on the one dispatcher, +# so the compiled-in plane is called through the same table as its dropped-in cdylib (#2, #30; THE +# DESIGN section 11.4), and the serve row hands it its arrivals. It carries the ws wire its live +# sessions arrive over and leave on. +plane-streaming = ["dep:busbar-plane-streaming", "transport-ws", "busbar-kernel/duplex-ws"] +# THE WS WIRE, the one optional transport: the streaming plane's edge and nothing else's, so `plane-streaming` # carries it and `--no-default-features` drops the crate edge and its linked row together. transport-ws = ["dep:busbar-transport-ws"] # THE WEBRTC WIRE, optional and never in `default`: on, the crate edge and its linked row together. @@ -343,7 +337,7 @@ auth-header = ["dep:busbar-auth-header"] auth-sigv4 = ["dep:busbar-auth-sigv4"] auth-oauth = ["dep:busbar-auth-oauth"] hooks-ranking = ["dep:busbar-hook-ranking", "busbar-kernel/hooks-ranking", "busbar-core-admin/hooks-ranking"] -openapi-schema = ["busbar-kernel/openapi-schema", "busbar-core-admin/openapi-schema", "busbar-llm?/openapi-schema", "busbar-a2a?/openapi-schema", "busbar-voice?/openapi-schema"] +openapi-schema = ["busbar-kernel/openapi-schema", "busbar-core-admin/openapi-schema", "busbar-llm?/openapi-schema", "busbar-a2a?/openapi-schema"] loom-model = ["busbar-kernel/loom-model"] # ── THE LINKED PLUGINS (K1, #2 rule (1)) ───────────────────────────────────────────────────────────── @@ -364,7 +358,6 @@ loom-model = ["busbar-kernel/loom-model"] [package.metadata.busbar.linked] proto-llm = "busbar-llm" plane-a2a = "busbar-a2a" -plane-streaming = "busbar-voice" export-file = "busbar-export-file" export-webhook = "busbar-export-webhook" export-prometheus = "busbar-export-prometheus" @@ -397,10 +390,11 @@ transport-webrtc = "busbar-transport-webrtc" plane-decisions = "busbar-plane-decisions" llm-on-driver = "busbar-plane-llm" plane-mcp-door = "busbar-plane-mcp" +plane-streaming = "busbar-plane-streaming" # What each linked feature's crate registers (keyed by the feature, as the linked row is), axis by axis: `plane` (PLANE_DECLARATION + PLANE_HOOKS, joined # kernel-side), `protocols`, `path-ingress`, `body-ingress`, `protocol-seams`, `diagnostics`, -# `ws-arrivals`, `on-host`, `compose`, `exports` — each an item of the crate's entry +# `on-host`, `compose`, `exports` — each an item of the crate's entry # module — and the root-bound seams it drives: `egress`, `plane-sections`, `admin-envelope`. # `gauntlet-one-shot` / `gauntlet-session`: the plane rides the unified kernel loop (#28) — # `gauntlet_install::install()` flips its `PLANE_DECLARATION.key` onto the one-shot or session runner. @@ -416,7 +410,6 @@ plane-mcp-door = "busbar-plane-mcp" [package.metadata.busbar.linked-axes] proto-llm = "plane claims protocols path-ingress body-ingress protocol-seams diagnostics on-host cli-help node gauntlet-one-shot" plane-a2a = "plane claims diagnostics egress plane-sections admin-envelope gauntlet-one-shot" -plane-streaming = "plane claims diagnostics ws-arrivals compose gauntlet-session" export-file = "export-doors" export-webhook = "export-doors" export-prometheus = "export-doors" @@ -431,9 +424,9 @@ secret-env = "secret-doors" secret-file = "secret-doors" transport-tcp = "transport transport-door" # `connector-door`: the framer's door is opened on the root's one Connector, so an outbound need on -# its schemes is served compiled-in. Every transport row is a door row (`transport-door`): the seal -# folds it from its door's Statement, and no transport names another (the carrier is the -# connector's choice from the target's scheme). +# its schemes is served compiled-in (the streaming door's ws needs bind on the ws row). Every +# transport row is a door row (`transport-door`): the seal folds it from its door's Statement, and no +# transport names another (the carrier is the connector's choice from the target's scheme). transport-http = "transport transport-door connector-door" transport-ws = "transport transport-door connector-door" transport-grpc = "transport transport-door" @@ -442,6 +435,7 @@ transport-webrtc = "transport transport-door" plane-decisions = "plane-door" llm-on-driver = "plane-door" plane-mcp-door = "plane-door" +plane-streaming = "plane-door" # A linked crate whose entry module is not `::linked`. [package.metadata.busbar.linked-entry] @@ -463,6 +457,7 @@ busbar-export-otlp = "busbar_export_otlp::door" # The file and webhook sinks' doors are at their logic crates' roots (`pub use table::door`). busbar-export-file = "busbar_export_file" busbar-export-webhook = "busbar_export_webhook" +plane-streaming = "busbar_plane_streaming::door" # Each linked plane door's declared metadata (its manifest `declares` section, as JSON): the item that # holds it. A door row with no row here declares nothing. @@ -476,6 +471,7 @@ plane-mcp-door = "busbar_plane_mcp::DECLARES" [package.metadata.busbar.linked-section] plane-mcp-door = "tools" plane-decisions = "decisions" +plane-streaming = "streams" # The key an `auths` row answers on the auth axis — the name its signed tarball's manifest states. [package.metadata.busbar.linked-name] @@ -500,7 +496,6 @@ auth-admin-tokens = "busbar-auth-admin-tokens" # `root/tests/gauntlet_kernel.rs` includes); each crate is linked with `test-support` by the dev-deps. [package.metadata.busbar.served-witness] plane-a2a = "busbar-a2a" -plane-streaming = "busbar-voice" # THE DRIVER PROOFS of a development-only fold switch (a `-on-driver` feature, BUSBAR-1.6.0.md # Part 3 section 12 "The switch"): cargo feature -> the plane crate whose `tests/on_driver/` holds the @@ -559,7 +554,6 @@ busbar-a2a = { path = "../busbar-a2a", features = ["test-support"] } # The MCP plane's door, a `door:` row of `test-linked` above: linked in every build's tests, whatever # plane features the build enables (a single-plane build's tests still install the whole roster). busbar-plane-mcp = { path = "../busbar-plane-mcp" } -busbar-voice = { path = "../busbar-voice", features = ["test-support"] } # `root::door_steps` tests: a member bound under an OAuth grant, its credential minted by the real # token-style auth plugin through its own need (TODO row 22). The same pin the `auth-oauth` row links. busbar-auth-oauth = { workspace = true } diff --git a/crates/busbar/build.rs b/crates/busbar/build.rs index af389df585..6d918ae173 100644 --- a/crates/busbar/build.rs +++ b/crates/busbar/build.rs @@ -101,10 +101,19 @@ fn main() { println!("cargo:rerun-if-changed=Cargo.toml"); println!("cargo:rerun-if-changed=src/linked_gen.rs"); let manifest = std::fs::read_to_string("Cargo.toml").expect("read Cargo.toml"); - let enabled = |feature: &str| { + let on = |feature: &str| { let var = format!("CARGO_FEATURE_{}", feature.to_uppercase().replace('-', "_")); env::var_os(var).is_some() }; + // A fold's development-only switch SWAPS its plane's legacy row out (`linked-swaps`): the row is + // not linked while the switch is on, so the folded door is the plane's one claimant. + // A build with no switch left has no table: nothing is swapped. + let swaps = if manifest.contains(&format!("[{LINKED_SWAPS}]")) { + metadata_map(&manifest, LINKED_SWAPS) + } else { + Vec::new() + }; + let enabled = |feature: &str| swapped_in(&swaps, &on, feature); let (source, cfgs) = linked_source(&manifest, &enabled); let out = std::path::PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR")).join("linked.rs"); std::fs::write(out, source).expect("write linked.rs"); diff --git a/crates/busbar/src/linked_gen.rs b/crates/busbar/src/linked_gen.rs index 2103c800f6..382fdfb399 100644 --- a/crates/busbar/src/linked_gen.rs +++ b/crates/busbar/src/linked_gen.rs @@ -48,7 +48,6 @@ pub(crate) const AXES: &[(&str, &str, &str)] = &[ ("body-ingress", "body_ingress", "BODY_INGRESS"), ("protocol-seams", "protocol_seams", "install_protocol_seams"), ("diagnostics", "diagnostics", "DIAGNOSTICS"), - ("ws-arrivals", "ws_arrivals", "install_ws_arrivals"), ("on-host", "on_host", "on_host"), ("compose", "compose", "compose"), ("cli-help", "cli_help", "CLI_HELP"), @@ -140,6 +139,17 @@ pub(crate) const SEAMS: &[(&str, &str)] = &[ ("admin-envelope", "linked_admin_envelope"), ]; +/// THE SWAPS TABLE (BUSBAR-1.6.0.md Part 3 section 12, "The switch"): ` = ""`, +/// a linked row its fold's development-only switch swaps out. While the switch is on, the row is +/// not linked (no claims, no routes, no section of its own), so the plane's folded door is its one +/// claimant: the switch swaps the row, it never adds a second one. +pub(crate) const LINKED_SWAPS: &str = "package.metadata.busbar.linked-swaps"; + +/// Whether `feature`'s row is linked: its feature is `on`, and no switch that swaps it is. +pub(crate) fn swapped_in(swaps: &[(String, String)], on: &dyn Fn(&str) -> bool, feature: &str) -> bool { + on(feature) && !swaps.iter().any(|(row, switch)| row == feature && on(switch)) +} + /// The `key = "value"` rows of the `[table]` header of a Cargo manifest, in file order. Deliberately /// not a TOML parser: one row per line, both sides optionally double-quoted. An absent or empty /// table is a build failure, never an empty list — an empty list would silently register nothing. diff --git a/crates/busbar/src/main.rs b/crates/busbar/src/main.rs index 0385d60dac..c38c197e60 100644 --- a/crates/busbar/src/main.rs +++ b/crates/busbar/src/main.rs @@ -10,7 +10,7 @@ // linked crates' own docs and the README for what a running deployment answers on the wire and the // `--help` output above for the CLI surface this binary exposes. // -// See `register_protocols`/`register_planes`/`register_diagnostics`/`register_ws_arrivals` below +// See `register_protocols`/`register_planes`/`register_diagnostics` below // for the composition root's one write into each axis: each linked crate contributes its own // declarations here, under its own Cargo feature, and nowhere else. @@ -331,14 +331,6 @@ fn register_diagnostics() { root::linked::register_diagnostics(&LINKED); } -/// REGISTER THE LINKED DUPLEX PLANES' INBOUND WS-ACCEPT ARRIVALS — the composition root's one write -/// into the neutral WS-accept registry. Installed BEFORE the router is built (in `run()`), so -/// `take_ws_arrivals` drains a populated set; a build with no duplex plane installs nothing and the -/// router mounts no WS-accept route. -fn register_ws_arrivals() { - root::linked::register_ws_arrivals(&LINKED); -} - /// The data-plane worker count and its warnings, resolved once at the top of `main()`. static WORKERS: std::sync::LazyLock<(usize, Vec)> = std::sync::LazyLock::new(resolve_worker_threads); @@ -379,12 +371,6 @@ fn main() { // any reader. Each linked entry contributes its owned diagnostics; a no-planes build installs // nothing and the catalog is the neutral built-ins alone. register_diagnostics(); - // INBOUND WS-ACCEPT ARRIVAL REGISTRATION, same slot and the same reason as the axes above: the - // core router drains the installed arrivals at build (`take_ws_arrivals`), which happens later in - // `run()` — so the duplex planes' arrivals must be installed here, before the router is built. Each - // duplex entry installs its `WsArrivalSpec`s; a build with no duplex plane installs nothing and - // the router mounts no WS-accept route — strong-form deletable. - register_ws_arrivals(); // THE COMPOSITION ROOT'S OWN SEAL is NOT here, and it is the one boot step that is not: it // composes the transports a switched-over plane would serve through, and the http one carries // the operator's `limits.request_body_max_bytes`, so it cannot run before the configuration it @@ -924,6 +910,7 @@ async fn run(data_workers: usize, late_services: std::sync::Arc { pub conns: Arc, /// Whole seconds. pub stream_ceiling_secs: u64, + /// The top-level models catalog a section's `session.model` names an entry of + /// (`root::serve::catalog_routes`, ARCHITECT Q-L5B-ROUTE); `None` = the section's own table. + pub catalog: Option<&'a std::collections::HashMap>, /// The model-serving pools the plane serving the `pools` map walks /// ([`crate::root::model_egress`]); `None` where none is composed. pub models: Option<&'a crate::root::model_egress::ModelServing>, diff --git a/crates/busbar/src/root/linked.rs b/crates/busbar/src/root/linked.rs index 0a11788b11..43468b2fde 100644 --- a/crates/busbar/src/root/linked.rs +++ b/crates/busbar/src/root/linked.rs @@ -92,9 +92,6 @@ pub struct Linked { pub protocol_seams: &'static [fn()], /// Owned diagnostics, joining the rendered catalog. pub diagnostics: &'static [&'static [&'static busbar_contract::diagnostic::Diagnostic]], - /// Installers of a duplex plane's inbound WS-accept arrivals (the seam is set once: the first - /// duplex entry in table order is the one installed). - pub ws_arrivals: &'static [fn()], /// Background work re-anchored on every generation's engine host (boot, then each swap). pub on_host: &'static [fn(&Arc)], /// Providers composed off the resolved configuration (see [`Compose`]). @@ -1438,14 +1435,6 @@ fn catalogue_entries( Ok(declared) } -/// THE WS-ACCEPT AXIS: each duplex entry installs its inbound arrivals — and none does when no entry -/// is duplex, so the router mounts no WS-accept route. -pub fn register_ws_arrivals(linked: &Linked) { - for install in linked.ws_arrivals { - install(); - } -} - /// THE ROOT-BOUND SEAMS an enabled entry drives, each bound once and only when some entry drives it /// (the manifest's `egress` / `plane-sections` / `admin-envelope` axes, emitted by the build script as /// `linked_*` cfgs): the hostless-egress driver and the egress-trust host and the parse-time section diff --git a/crates/busbar/src/root/serve.rs b/crates/busbar/src/root/serve.rs index 9c18e4da54..31aadb6e60 100644 --- a/crates/busbar/src/root/serve.rs +++ b/crates/busbar/src/root/serve.rs @@ -543,6 +543,8 @@ pub struct ApplyReach { conns: Arc, journal: Arc, stream_ceiling_secs: u64, + /// The top-level models catalog the deployment booted with ([`catalog_routes`]). + catalog: Option>, upgrades: Vec<&'static str>, } @@ -630,7 +632,11 @@ impl DoorApply { &settings, generation, )?; - let pools = DoorPools::of(section); + let table = routes_of( + section, + self.reach.as_ref().and_then(|r| r.catalog.as_ref()), + ); + let pools = DoorPools::of(&table); let egress = match &self.reach { Some(r) => { let reach = crate::root::door_steps::DoorReach { @@ -639,11 +645,12 @@ impl DoorApply { auths: Arc::clone(&r.auths), conns: Arc::clone(&r.conns), stream_ceiling_secs: r.stream_ceiling_secs, + catalog: r.catalog.as_ref(), upgrades: r.upgrades.clone(), models: None, }; let routes = crate::root::door_steps::member_routes( - section, + &table, &pools, &self.served_facts, &reach, @@ -1534,20 +1541,19 @@ pub(crate) fn compose_planes_over( )?; // The door's own requests to its members carry the members' bindings (ARCHITECT round 5 // Q-L3B-DOOR-EXCHANGE), held on the connection table its need is declared on. - if let (Some(egress), Some(table)) = (egress, plugin.conn_table()) { - let pools = DoorPools::of(section); - let routes = crate::root::door_steps::member_routes( - section, - &pools, - &served_facts, - egress.reach, - ) - .map_err(|e| format!("{instance}: {e}"))?; + if let (Some(egress), Some(conn_table)) = (egress, plugin.conn_table()) { + // The route table: the section, with the catalog model its `session.model` names + // folded in. + let table = routes_of(section, egress.reach.catalog); + let pools = DoorPools::of(&table); + let routes = + crate::root::door_steps::member_routes(&table, &pools, &served_facts, egress.reach) + .map_err(|e| format!("{instance}: {e}"))?; crate::root::door_steps::bind_member_fetches( &served_facts, &routes, plugin.instance(), - &*table, + &*conn_table, kernel.units(), ); } @@ -1565,6 +1571,7 @@ pub(crate) fn compose_planes_over( conns: Arc::clone(&e.reach.conns), journal: Arc::clone(&e.journal), stream_ceiling_secs: e.reach.stream_ceiling_secs, + catalog: e.reach.catalog.cloned(), upgrades: e.reach.upgrades.clone(), }); let live = Arc::new(DoorApply { @@ -1592,6 +1599,60 @@ pub(crate) fn compose_planes_over( Ok(served) } +/// The `session:` block of a section, and the `model:` key inside it that names a top-level catalog +/// model. +const SESSION_KEY: &str = "session"; +const MODEL_KEY: &str = "model"; + +/// THE ROUTE TABLE A SECTION'S DOOR STEPS RESOLVE AGAINST (ARCHITECT Q-L5B-ROUTE, 2026-10-03): the +/// section as written, plus, where it states no `models:` of its own and its `session.model` names an +/// entry of the top-level models `catalog`, that one entry folded in as its `models:` map (its +/// provider, upstream model and attempt cap), so the plane's door names it as a DIRECT route and the +/// kernel's door steps resolve it as any entry of the plane's own section. The 1.5.5 shape of both +/// sections is unchanged: only the route table is folded, never the settings the door opens with. +#[must_use] +pub fn catalog_routes( + section: &serde_yaml::Value, + catalog: &std::collections::HashMap, +) -> serde_yaml::Value { + use serde_yaml::{Mapping, Value}; + let mut routes = section.clone(); + let named = section + .get(SESSION_KEY) + .and_then(|s| s.get(MODEL_KEY)) + .and_then(Value::as_str); + let (Some(map), Some(model)) = (routes.as_mapping_mut(), named) else { + return routes; + }; + let models_key = Value::from(busbar_contract::section::RESERVED_MODELS_KEY); + let Some(entry) = catalog + .get(model) + .filter(|_| !map.contains_key(&models_key)) + else { + return routes; + }; + let mut folded = Mapping::new(); + folded.insert("provider".into(), entry.provider.clone().into()); + if let Some(upstream) = &entry.upstream_model { + folded.insert("upstream_model".into(), upstream.clone().into()); + } + if let Some(ms) = entry.attempt_timeout_ms { + folded.insert("attempt_timeout_ms".into(), ms.into()); + } + let mut models = Mapping::new(); + models.insert(model.into(), Value::Mapping(folded)); + map.insert(models_key, Value::Mapping(models)); + routes +} + +/// The route table of `section` under `catalog` ([`catalog_routes`]); the section itself without one. +fn routes_of( + section: &serde_yaml::Value, + catalog: Option<&std::collections::HashMap>, +) -> serde_yaml::Value { + catalog.map_or_else(|| section.clone(), |c| catalog_routes(section, c)) +} + /// THE GENERATION A DOOR PLANE'S UNITS ARE SERVED BY ([`DoorLive`], refresh generation `generation`), sealed over its walked /// `section`: the tail's facts (the flat card's empty plane key for the plane serving the `pools` /// map, which owns 1.5.5's `rate_card:`; each provider's dialect, for the translation counter), the @@ -1653,11 +1714,14 @@ fn seal_live( ), ); } - let pools = DoorPools::of(section); + // The route table: the section, with the catalog model its `session.model` names folded in + // ([`catalog_routes`]). + let table = routes_of(section, egress.and_then(|e| e.reach.catalog)); + let pools = DoorPools::of(&table); let egress = match egress { Some(egress) => { let routes = - crate::root::door_steps::member_routes(section, &pools, served_facts, egress.reach) + crate::root::door_steps::member_routes(&table, &pools, served_facts, egress.reach) .map_err(|e| format!("{instance}: {e}"))?; let sealed = match egress.reach.models { Some(models) if models_plane => crate::root::model_egress::compose( @@ -3842,8 +3906,9 @@ pub(crate) mod planes_tests; // The doors served end to end through this composition: the decisions plane's (the `root-decisions` // leg's own cells, qa/capability-equality.json, under its feature) and the MCP plane's (the -// `root-mcp` leg's loop cells, under the linked plane-door axis), each gated item by item inside, so -// either plane's switch alone still compiles its own. Its MCP cells reach the door_steps helpers +// `root-mcp` leg's loop cells, under the linked plane-door axis), each gated item by item inside, +// so either plane's switch alone still compiles its own, and the session door's root-leg cells, +// which find their door among the linked plane doors. Its MCP cells reach the door_steps helpers // that need the default build's linked auth rows (gated with the node-axis plane that carries that // build), and the capability cells of the door serving the `pools` map. #[cfg(all(test, linked_axis_node))] diff --git a/crates/busbar/src/root/tests/door_steps.rs b/crates/busbar/src/root/tests/door_steps.rs index 675d3401ad..38f6ba87b6 100644 --- a/crates/busbar/src/root/tests/door_steps.rs +++ b/crates/busbar/src/root/tests/door_steps.rs @@ -431,6 +431,7 @@ fn resolve_upgrading( auths: std::sync::Arc::new(auths), conns, stream_ceiling_secs: 1, + catalog: None, models: None, upgrades, }; @@ -885,6 +886,7 @@ async fn a_member_under_an_oauth_grant_presents_its_minted_then_refreshed_bearer auths: std::sync::Arc::new(linked_mint_plugin(&table)), conns: std::sync::Arc::new(busbar_core_connector::Connector::new()), stream_ceiling_secs: 1, + catalog: None, models: None, upgrades: Vec::new(), }; @@ -1022,6 +1024,7 @@ fn jwt_bearer_routes( auths: std::sync::Arc::new(linked_mint_plugin(table)), conns: std::sync::Arc::new(busbar_core_connector::Connector::new()), stream_ceiling_secs: 1, + catalog: None, models: None, upgrades: Vec::new(), }; @@ -1744,6 +1747,7 @@ pub(crate) mod tool_door { conns: Arc::clone(&connector) as Arc, models: None, stream_ceiling_secs: 600, + catalog: None, upgrades: Vec::new(), }; let egress = crate::root::serve::DoorEgress { diff --git a/crates/busbar/src/root/tests/fixtures/claims_per_plane.txt b/crates/busbar/src/root/tests/fixtures/claims_per_plane.txt index 3d2dd819f9..9139d56623 100644 --- a/crates/busbar/src/root/tests/fixtures/claims_per_plane.txt +++ b/crates/busbar/src/root/tests/fixtures/claims_per_plane.txt @@ -1,7 +1,6 @@ # THE CLAIMS EACH PLANE OF THE SHIPPED COMPOSITION DECLARES — ` `, one row per -# plane. Read by root/tests/registry.rs (`the_planes_declare_forty_five_claims`). A plane that gains or +# plane. Read by root/tests/registry.rs (`the_planes_declare_forty_claims`). A plane that gains or # loses a claim has to say so here. llm 25 a2a 14 -streaming 5 admin 1 diff --git a/crates/busbar/src/root/tests/fixtures/sealed_order.txt b/crates/busbar/src/root/tests/fixtures/sealed_order.txt index 82e187fd69..3b48b7fec7 100644 --- a/crates/busbar/src/root/tests/fixtures/sealed_order.txt +++ b/crates/busbar/src/root/tests/fixtures/sealed_order.txt @@ -1,6 +1,6 @@ # THE SEALED CLAIM ORDER of the shipped composition, most specific first — one row per claim: # ` `. Read by root/tests/registry.rs -# (`the_sealed_order_of_the_forty_five_claims_is_pinned`). A change here is a change to which plane answers +# (`the_sealed_order_of_the_forty_claims_is_pinned`). A change here is a change to which plane answers # which request, and has to be made on purpose, with the new order visible in the same diff. a2a ExactPath("/.well-known/oauth-protected-resource/a2a") a2a ExactPath("/.well-known/agent-card.json") @@ -20,25 +20,20 @@ a2a PathPattern([Lit("a2a"), Lit("agents"), Var]) llm PathPattern([Lit("v1"), Lit("models"), Tail]) llm PathPattern([Lit("v1beta"), Lit("models"), Tail]) a2a PathPattern([Lit("lf.a2a.v1.A2AService"), Var]) -streaming PrefixOneLevel("/twilio") llm HeaderPrefix("authorization", "AWS4-HMAC-SHA256") llm HeaderPresent("anthropic-version") llm HeaderPresent("anthropic-beta") llm HeaderPresent("x-goog-api-key") llm HeaderPresent("x-api-key") -streaming PathSuffix("/v1/audio/transcriptions") llm PathSuffix("/v1/audio/translations") llm PathContains(":streamGenerateContent") llm PathSuffix("/v1/chat/completions") llm PathContains(":batchEmbedContents") -streaming PathContains("BidiGenerateContent") -streaming PathSuffix("/v1/audio/speech") llm PathContains(":generateContent") llm PathSuffix("/v1/moderations") llm PathSuffix("/v1/embeddings") llm PathSuffix("/v1/responses") llm PathContains(":embedContent") -streaming PathSuffix("/v1/realtime") llm PathContains("/v1/messages") llm PathContains("/v1/images/") llm PathSuffix("/v2/rerank") diff --git a/crates/busbar/src/root/tests/gauntlet_kernel.rs b/crates/busbar/src/root/tests/gauntlet_kernel.rs index 284dfbbc79..ed5bbebb41 100644 --- a/crates/busbar/src/root/tests/gauntlet_kernel.rs +++ b/crates/busbar/src/root/tests/gauntlet_kernel.rs @@ -675,67 +675,6 @@ async fn served_rider_dials_only_the_judged_address() { run_served("net-guard", "served_rider_dials_only_the_judged_address").await; } -/// VERIFY, on a session's served leg: the destination the door judged is the one the session runs -/// under. -#[tokio::test] -async fn served_rider_opens_the_session_on_the_destination_it_judged() { - run_served( - "session-verify", - "served_rider_opens_the_session_on_the_destination_it_judged", - ) - .await; -} - -/// ROUTE, on a session's served leg: the session the door opened relays both directions. -#[tokio::test] -async fn served_rider_opens_a_session_that_relays_both_ways() { - run_served( - "session-route", - "served_rider_opens_a_session_that_relays_both_ways", - ) - .await; -} - -/// AUTHENTICATE, on a session's served leg: the session answers for the key the door resolved. -#[tokio::test] -async fn served_rider_attributes_the_session_to_the_resolved_key() { - run_served( - "session-authenticate", - "served_rider_attributes_the_session_to_the_resolved_key", - ) - .await; -} - -/// METER, on a session's served leg: each turn is ledgered per class the plane declares. -#[tokio::test] -async fn served_rider_meters_each_turn_per_declared_class() { - run_served( - "session-meter", - "served_rider_meters_each_turn_per_declared_class", - ) - .await; -} - -/// METRICS, on a session's served leg: each session the door opens is reported under its labels. -#[tokio::test] -async fn served_rider_reports_each_session_it_opens() { - run_served( - "session-metrics", - "served_rider_reports_each_session_it_opens", - ) - .await; -} - -/// BREAKER-FASTFAIL, on the served leg: a tripped upstream cell refuses before any socket opens. -#[tokio::test] -async fn served_rider_fast_fails_a_tripped_upstream() { - run_served( - "breaker-fastfail", - "served_rider_fast_fails_a_tripped_upstream", - ) - .await; -} - /// CATALOGUE, on the served leg: what the caller may not see is not served. #[tokio::test] async fn served_rider_serves_only_the_callers_catalogue() { diff --git a/crates/busbar/src/root/tests/registry.rs b/crates/busbar/src/root/tests/registry.rs index b92922c8f4..caaf8e1ecc 100644 --- a/crates/busbar/src/root/tests/registry.rs +++ b/crates/busbar/src/root/tests/registry.rs @@ -36,9 +36,9 @@ fn linked_registry() -> Registry { /// The shipped transport fold (` ` rows, in build order), as data. const TRANSPORT_FOLD: &str = include_str!("fixtures/transport_fold.txt"); -/// The sealed walk over the forty-five declared claims, most specific first. The decisions plane's +/// The sealed walk over the forty declared claims, most specific first. The decisions plane's /// claim is its door snapshot's (FLIP-DECISIONS), mounted by the serve fold, as the MCP plane's are -/// (FLIP-MCP), so neither is here. +/// (FLIP-MCP) and every other door plane's are, so none of them is here. /// /// Pinned as text rather than as indices so that a diff of it reads as a routing change. See /// the test that reads it for what a change to this snapshot means. The rows are fixture DATA @@ -51,14 +51,17 @@ const SEALED_ORDER: &str = include_str!("fixtures/sealed_order.txt"); #[cfg(linked_every_plane)] const CLAIMS_PER_PLANE: &str = include_str!("fixtures/claims_per_plane.txt"); -/// Whether this build links a plane on the kernel's SESSION loop (the `gauntlet-session` axis of the -/// linked table) — and with it its WS transport, its registry row and its five claims. Every pinned -/// number below is a statement about ONE composition, and the shipped one (the session plane linked, -/// since its row's feature is in `default`) is the one they are pinned against; a build that -/// compiled it out is a different composition, not a smaller one. Read off `LINKED`, so this source -/// names no plane. -fn session_linked() -> bool { - !crate::LINKED.gauntlet_session.is_empty() +/// Whether this build links a wire that opens at an upgrade (a transport row stating an upgrade +/// claim, on the `transport` and `connector-door` axes): the sixth wire. No plane rides the +/// kernel's SESSION loop: the session plane is a door plane (`plane-door` axis) whose claims are +/// its snapshot's and are mounted by the serve fold, not declared in the linked claims table, so +/// the wire is the transport row's alone. Every pinned number below is a statement about ONE +/// composition, the shipped one. Read off `LINKED`, so this source names no wire. +fn upgrade_wire_linked() -> bool { + crate::LINKED + .transports + .iter() + .any(|t| !(t.upgrades)().is_empty()) } /// The non-comment rows of a fixture file, in order. Read only by the shipped-composition pins, @@ -83,7 +86,7 @@ fn six_transports_and_five_planes_register() { let registry = linked_registry(); assert_eq!( registry.count(PluginKind::Transport), - if session_linked() { 6 } else { 5 } + if upgrade_wire_linked() { 6 } else { 5 } ); // Every linked plane and the core one: a plane this build does not link registers nothing. assert_eq!( @@ -116,25 +119,22 @@ fn six_transports_and_five_planes_register() { // The decisions plane is not on this table: it registers through its door (`plane-door` axis), // whose Statement the kernel folds into its row, and its claim is its door snapshot's // (FLIP-DECISIONS). - // The voice plane and its transport are present exactly together: neither is a thing this - // root registers without the other. + // The upgrade wire is registered exactly when its transport row is linked. assert_eq!( registry.resolve(PluginKind::Transport, "ws").is_some(), - session_linked() + upgrade_wire_linked() ); - // And its plane: the claimed-set check above resolves every claimed key and ties the claimed - // count to the registered count, and the plane count moves by exactly one with the session - // plane — so its row is registered exactly when it is linked. } /// The measured claim total, one row per plane. It is pinned as a number because the number is /// what a reader checks the design's own table against; a plane that gains or loses a claim /// should have to say so here. -// Pinned against the SHIPPED composition (voice on). Compiled out with the voice plane -// because the numbers below are that composition's, not a subset of it. +// Pinned against the SHIPPED composition (every linked plane on). Compiled out with any of them +// because the numbers below are that composition's, not a subset of it. A door plane's claims are +// its door snapshot's, mounted by the serve fold, so they are not counted here. #[cfg(linked_every_plane)] #[test] -fn the_planes_declare_forty_five_claims() { +fn the_planes_declare_forty_claims() { let claims = linked_claims(); let count = |plane: &str| claims.iter().filter(|c| c.plane == plane).count(); // One ` ` row per plane, pinned as fixture DATA so this source names none. @@ -145,9 +145,9 @@ fn the_planes_declare_forty_five_claims() { (key.to_string(), n.parse().expect("a claim count")) }) .collect(); - // Four rows: FLIP-MCP and FLIP-DECISIONS each took a plane's claims out of the linked table - // (its door's snapshot claims them, mounted by the serve fold), so six rows became four. - assert_eq!(pinned.len(), 4, "four planes are pinned: {pinned:?}"); + // Three rows: each plane that became a door plane took its claims out of the linked table (its + // door's snapshot claims them, mounted by the serve fold). + assert_eq!(pinned.len(), 3, "three planes are pinned: {pinned:?}"); for (key, n) in &pinned { assert_eq!( count(key), @@ -166,7 +166,7 @@ fn the_planes_declare_forty_five_claims() { claims.len(), "every claim belongs to a pinned plane" ); - assert_eq!(claims.len(), 45); + assert_eq!(claims.len(), 40); } /// The measured overlap, split the way the rule splits it. Both counts are pinned because both @@ -181,15 +181,21 @@ fn the_planes_declare_forty_five_claims() { /// surface one path at a time rather than as a prefix took it from 65 to 63. Joining the decision /// plane (item 251) added one more — its `/v1/models` against the llm plane's tail pattern — for 64. /// FLIP-MCP took the MCP plane's four claims out of the linked table (its door's snapshot claims -/// them, mounted by the serve fold behind the data listener's guest list): 100 cross-family became -/// 90; the 64 same-family pairs were unchanged. FLIP-DECISIONS took the decisions plane's two claims -/// out of it too (its door's snapshot claims `POST /v1/systemone` alone, mounted by the serve fold): -/// ten cross-family pairs and one same-family pair more, so 80 and 63. -// Pinned against the SHIPPED composition (voice on). Compiled out with the voice plane -// because the numbers below are that composition's, not a subset of it. +/// them, mounted by the serve fold behind the data listener's guest list): ten cross-family pairs; +/// the same-family pairs were unchanged. FLIP-DECISIONS took the decisions plane's two claims out of +/// it too (its door's snapshot claims `POST /v1/systemone` alone, mounted by the serve fold): ten +/// cross-family pairs and one same-family pair. The session plane's flip to its door took its five +/// claims out (its door's snapshot claims them, mounted by the serve fold): ten cross-family and 22 +/// same-family pairs. The three planes' claims never overlapped one another (none of them states a +/// header claim, the tool plane's stream name is not on the path planes' transport, and no exact +/// path of one matches another's suffix, substring or prefix selectors), so 100 cross-family and 64 +/// same-family became 70 and 41. +// Pinned against the SHIPPED composition (every linked plane on). Compiled out with any of them +// because the numbers below are that composition's, not a subset of it. A door plane's claims are +// its door snapshot's, mounted by the serve fold, so they are not counted here. #[cfg(linked_every_plane)] #[test] -fn one_hundred_and_forty_three_cross_plane_pairs_overlap() { +fn one_hundred_and_eleven_cross_plane_pairs_overlap() { use busbar_kernel::grammar::family; let claims = linked_claims(); @@ -207,11 +213,11 @@ fn one_hundred_and_forty_three_cross_plane_pairs_overlap() { } } } - assert_eq!(cross_family, 80); - assert_eq!(same_family, 63); + assert_eq!(cross_family, 70); + assert_eq!(same_family, 41); } -/// What the 63 path-family overlaps that remain actually ARE, one class at a time. +/// What the 41 path-family overlaps that remain actually ARE, one class at a time. /// /// A count alone cannot say whether an overlap is a real shape or a gap in the reasoning, and /// that distinction is the whole reason to tighten a grammar rather than to relax a check. So @@ -226,8 +232,9 @@ fn one_hundred_and_forty_three_cross_plane_pairs_overlap() { /// /// A pair that fits none of these would be the interesting one: a conservative answer with no /// account of itself. There is none, and the assertion is that there is none. -// Pinned against the SHIPPED composition (voice on). Compiled out with the voice plane -// because the numbers below are that composition's, not a subset of it. +// Pinned against the SHIPPED composition (every linked plane on). Compiled out with any of them +// because the numbers below are that composition's, not a subset of it. A door plane's claims are +// its door snapshot's, mounted by the serve fold, so they are not counted here. #[cfg(linked_every_plane)] #[test] fn every_remaining_path_overlap_is_a_shape_and_not_a_gap() { @@ -263,27 +270,30 @@ fn every_remaining_path_overlap_is_a_shape_and_not_a_gap() { } // The decisions plane's `/v1/models` against the llm plane's tail pattern left with its claims // (FLIP-DECISIONS). - assert_eq!(tail, 23); + assert_eq!(tail, 17); assert_eq!(variable, 24); - assert_eq!(fragments, 16); + // The fragment pairs were the session plane's audio suffixes against the model plane's; its + // claims left this table when it became a door plane. + assert_eq!(fragments, 0); } -/// **The finding, answered.** Every one of those 143 overlaps is settled by the sealed order, +/// **The finding, answered.** Every one of those 111 overlaps is settled by the sealed order, /// and none of them is a refusal. /// /// The resolved count is pinned against the overlap count above, so the two cannot drift apart /// silently: a pair that stops being resolved has either stopped overlapping or become a tie, /// and each of those is a different thing to have to explain. The refusal list is pinned empty, /// which is the whole claim of this file — the declared set of planes seals. -// Pinned against the SHIPPED composition (voice on). Compiled out with the voice plane -// because the numbers below are that composition's, not a subset of it. +// Pinned against the SHIPPED composition (every linked plane on). Compiled out with any of them +// because the numbers below are that composition's, not a subset of it. A door plane's claims are +// its door snapshot's, mounted by the serve fold, so they are not counted here. #[cfg(linked_every_plane)] #[test] fn every_cross_plane_overlap_is_resolved_by_precedence_and_none_refuses() { let claims = linked_claims(); let sealed = seal_claims(&claims); - assert_eq!(sealed.resolved.len(), 143); + assert_eq!(sealed.resolved.len(), 111); assert!( sealed.refused.is_empty(), "the declared claims do not seal: {:?}", @@ -309,18 +319,19 @@ fn every_cross_plane_overlap_is_resolved_by_precedence_and_none_refuses() { } } -/// The sealed order of the forty-five, written out. +/// The sealed order of the forty, written out. /// /// A snapshot, and deliberately a verbose one: the walk every arriving connection is matched /// against is the thing this file produces, and a change to it is a change to which plane /// answers which request. Each row is the plane and the selector, so a diff of this array reads /// as a routing change rather than as a permutation of opaque indices. A claim added, removed or /// respelled has to update it, on purpose, with the new order visible in the same diff. -// Pinned against the SHIPPED composition (voice on). Compiled out with the voice plane -// because the numbers below are that composition's, not a subset of it. +// Pinned against the SHIPPED composition (every linked plane on). Compiled out with any of them +// because the numbers below are that composition's, not a subset of it. A door plane's claims are +// its door snapshot's, mounted by the serve fold, so they are not counted here. #[cfg(linked_every_plane)] #[test] -fn the_sealed_order_of_the_forty_five_claims_is_pinned() { +fn the_sealed_order_of_the_forty_claims_is_pinned() { let claims = linked_claims(); let sealed = seal_claims(&claims); let walk: Vec = sealed @@ -501,7 +512,7 @@ fn the_shipped_transport_stack_composes() { // NO TRANSPORT NAMES ANOTHER (ARCHITECT Q128 U7; TRANSPORT-STACK (2), :4721): every row is // built over nothing, the carrier being the connector's choice from the target's scheme, and // `sse` is a claim of the http entry, served by its wire, not a layer over it. - if session_linked() { + if upgrade_wire_linked() { assert_eq!(composed_over("ws"), None); } assert_eq!(composed_over("grpc"), None); @@ -614,15 +625,16 @@ fn a_claim_on_a_transport_with_no_crate_refuses_at_boot() { /// seal answers. This is the assertion the previous shape of the test above could not make — /// the claims sealed, and the one transport gap was all that stood between the declared /// composition and a node that boots. -// Pinned against the SHIPPED composition (voice on). Compiled out with the voice plane -// because the numbers below are that composition's, not a subset of it. +// Pinned against the SHIPPED composition (every linked plane on). Compiled out with any of them +// because the numbers below are that composition's, not a subset of it. A door plane's claims are +// its door snapshot's, mounted by the serve fold, so they are not counted here. #[cfg(linked_every_plane)] #[test] fn the_seal_answers_now_that_every_claim_names_a_registered_transport() { let sealed = seal(&crate::LINKED, Dropped::NONE, TransportSettings::default()) .expect("every claim names a live transport"); - assert_eq!(sealed.claims.len(), 45); - assert_eq!(sealed.precedence.len(), 45); + assert_eq!(sealed.claims.len(), 40); + assert_eq!(sealed.precedence.len(), 40); } /// The operator's request-body cap reaches every linked transport. @@ -995,7 +1007,7 @@ fn a_dropped_in_wire_on_a_linked_key_is_refused_as_a_second_linked_row_is() { #[test] fn the_upgrade_lines_are_read_off_the_door_statements() { let lines = crate::root::serve::upgrade_carriers(crate::LINKED.transports); - if session_linked() { + if upgrade_wire_linked() { assert_eq!( lines, ["ws"], diff --git a/crates/busbar/src/root/tests/secret_source.rs b/crates/busbar/src/root/tests/secret_source.rs index a095c597c4..fbf2106bdc 100644 --- a/crates/busbar/src/root/tests/secret_source.rs +++ b/crates/busbar/src/root/tests/secret_source.rs @@ -215,11 +215,15 @@ fn statement_name(door: busbar_contract::abi::mechanism::door::DoorFn) -> String /// The doors this file drives, by Statement name. fn driven() -> Vec { + // Mutated only where the build links a door beyond the line door. + #[allow(unused_mut)] let mut names = vec![statement_name( crate::root::door_steps::tests::tool_door::line_door(), )]; #[cfg(linked_section_decisions)] names.push(super::door_tests::decisions_name()); + #[cfg(linked_section_streams)] + names.extend(super::door_tests::session_door::name()); #[cfg(linked_fold_on_driver)] names.push(statement_name(super::hook_seat_tests::pools_door())); names @@ -433,6 +437,78 @@ async fn decisions_a_provider_key_gone_by_an_apply_reaches_the_caller_without_it assert_clean(instance, status, &headers, &body, &secret); } +/// THE SESSION DOOR'S CELLS, compiled where the build links that door: a door not found is a +/// failure, never a skip. +#[cfg(linked_section_streams)] +mod session_door_cells { + use super::*; + + /// THE SESSION DOOR, COMPOSE TIME: a provider whose `api_key` does not resolve refuses the boot + /// with the operator's text, which names the source; nothing is served to a caller. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn session_door_an_unresolved_provider_key_refuses_the_boot_naming_its_source() { + let _one = PUBLISHING.lock().await; + let instance = "session-key-compose"; + let path = missing(instance); + let secret = SecretRef::file(path.clone()); + refused_naming_its_source(&secret); + let composed = super::super::door_tests::session_door::session_keyed( + instance, + None, + &format!("{{file: '{path}'}}"), + ) + .await + .expect("the build links the session door"); + let Err(refusal) = composed else { + panic!("a provider key that does not resolve refuses the composition"); + }; + println!("SECRET-SOURCE {instance}: compose refused (operator): {refusal}"); + assert!( + !source_in(&refusal, &secret).is_empty(), + "the operator's refusal names the source: {refusal}" + ); + } + + /// THE SESSION DOOR, ACROSS AN APPLY: a provider served while its `api_key` file was there, the + /// file then gone and the configuration applied: the door keeps the generation it serves, and the + /// caller's mint is served by it, its render clean of the source. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn session_door_a_provider_key_gone_by_an_apply_reaches_the_caller_without_its_source() { + let _one = PUBLISHING.lock().await; + let instance = "session-key-apply"; + let path = missing(instance); + std::fs::write(&path, "sk-secret-source-test").expect("the credential file"); + let secret = SecretRef::file(path.clone()); + let composed = super::super::door_tests::session_door::session_keyed( + instance, + None, + &format!("{{file: '{path}'}}"), + ) + .await; + let Some(composed) = composed else { + let _ = std::fs::remove_file(&path); + panic!("the build links the session door"); + }; + let serving = composed.expect("the door composes"); + std::fs::remove_file(&path).expect("the credential file goes"); + refused_naming_its_source(&secret); + let before = serving.live.current().generation; + serving.live.apply(&serving.app); + assert_eq!( + serving.live.current().generation, + before, + "the door keeps the generation it serves" + ); + let response = serving.mint().await; + let status = response.status(); + let headers = response.headers().clone(); + let body = axum::body::to_bytes(response.into_body(), 1 << 20) + .await + .expect("the body reads"); + assert_clean(instance, status, &headers, &body, &secret); + } +} + /// THE POOLS DOOR, COMPOSE TIME: a provider whose `api_key` does not resolve refuses the boot with the /// operator's text, which names the source; nothing is served to a caller. #[cfg(linked_fold_on_driver)] diff --git a/crates/busbar/src/root/tests/serve_hook_seats.rs b/crates/busbar/src/root/tests/serve_hook_seats.rs index b6b67ad79b..0bc61738a2 100644 --- a/crates/busbar/src/root/tests/serve_hook_seats.rs +++ b/crates/busbar/src/root/tests/serve_hook_seats.rs @@ -896,6 +896,7 @@ pub(super) async fn try_rig(instance: &'static str, opts: RigOpts<'_>) -> Result conns: Arc::clone(&connector) as Arc, stream_ceiling_secs: opts.stream_ceiling_secs.unwrap_or(600), models: Some(&models), + catalog: None, upgrades: Vec::new(), }; let stage = HookStage { @@ -948,6 +949,7 @@ pub(super) async fn try_rig(instance: &'static str, opts: RigOpts<'_>) -> Result conns: Arc::clone(&connector) as Arc, stream_ceiling_secs, models: Some(&models), + catalog: None, upgrades: Vec::new(), }; appliers.refresh_models( diff --git a/crates/busbar/src/root/tests/serve_money.rs b/crates/busbar/src/root/tests/serve_money.rs index d429107341..15e513fc57 100644 --- a/crates/busbar/src/root/tests/serve_money.rs +++ b/crates/busbar/src/root/tests/serve_money.rs @@ -833,6 +833,7 @@ async fn the_boot_composition_serves_a_dropped_in_door_plane_in_every_build() { conns: Arc::new(busbar_core_connector::Connector::new()), models: None, stream_ceiling_secs: 1, + catalog: None, upgrades: Vec::new(), }; let mut sections = BTreeMap::new(); diff --git a/crates/busbar/src/root/tests/serve_planes.rs b/crates/busbar/src/root/tests/serve_planes.rs index f4ae9916ef..15fce23fdf 100644 --- a/crates/busbar/src/root/tests/serve_planes.rs +++ b/crates/busbar/src/root/tests/serve_planes.rs @@ -202,6 +202,48 @@ fn no_door_plane_composes_nothing_and_needs_no_services() { assert!(served.planes.is_empty()); } +/// THE CATALOG ROUTE (ARCHITECT Q-L5B-ROUTE, 2026-10-03): a section whose `session.model` names a +/// top-level catalog model has that entry folded into its route table, so the door steps resolve the +/// door's DIRECT route to it (unchanged); a section with its own `models:`, or naming a model the +/// catalog lacks, keeps its table as written. The settings the door opens with are never folded. +#[test] +fn a_session_model_from_the_top_level_catalog_is_folded_into_the_route_table() { + use crate::root::door_steps::DoorPools; + use busbar_contract::abi::plane::ROUTE_DIRECT; + let catalog: std::collections::HashMap = + serde_yaml::from_str( + "gpt-rt: { provider: p, upstream_model: gpt-realtime, attempt_timeout_ms: 900 }\n", + ) + .expect("the catalog"); + let section: serde_yaml::Value = + serde_yaml::from_str("session: { model: gpt-rt }\nsession_max_secs: 60\n").expect("yaml"); + let routes = super::catalog_routes(§ion, &catalog); + assert_eq!( + routes["models"]["gpt-rt"], + serde_yaml::from_str::( + "{ provider: p, upstream_model: gpt-realtime, attempt_timeout_ms: 900 }" + ) + .expect("yaml"), + "the catalog entry's routing facts, folded" + ); + assert_eq!( + routes["session"], section["session"], + "the section stays as written" + ); + let pools = DoorPools::of(&routes); + assert_eq!( + pools.resolve(ROUTE_DIRECT, Some(b"gpt-rt")), + Some((String::new(), vec!["gpt-rt".to_string()])), + "the door's DIRECT route resolves to the folded entry" + ); + let unknown: serde_yaml::Value = + serde_yaml::from_str("session: { model: nope }\n").expect("yaml"); + assert_eq!(super::catalog_routes(&unknown, &catalog), unknown); + let own: serde_yaml::Value = + serde_yaml::from_str("session: { model: gpt-rt }\nmodels: { mine: {} }\n").expect("yaml"); + assert_eq!(super::catalog_routes(&own, &catalog), own); +} + /// A PLANE NEVER RECEIVES SECRET BYTES (THE DESIGN §6; SECURITY ruling 2026-10-07). `${VAR}` is /// spliced into the config text before it is parsed, so its plaintext used to reach a door plane's /// `open` settings, its owned sections and its `refresh` settings. The test plane records the bytes diff --git a/crates/busbar/src/root/tests/serve_tests.rs b/crates/busbar/src/root/tests/serve_tests.rs index 6e5b7fb3e4..c04c8a17ed 100644 --- a/crates/busbar/src/root/tests/serve_tests.rs +++ b/crates/busbar/src/root/tests/serve_tests.rs @@ -17,6 +17,9 @@ //! The `mcp` module below is the `root-mcp` leg's loop cells (`qa/capability-equality.json`, U14): //! the MCP plane's door, composed here the same way, each core capability driven through the data //! router and asserted where it lands. +//! +//! The `session_door` module below is the session door's root-leg cells (ARCHITECT 2026-10-05 Q1): +//! the linked plane door that meters audio, served end to end the same way. #[cfg(feature = "plane-decisions")] use std::collections::BTreeMap; @@ -328,6 +331,7 @@ async fn a_claimed_request_is_served_through_the_door_and_its_money_posted() { auths: Arc::new(auths), conns: Arc::clone(&connector) as Arc, stream_ceiling_secs: 600, + catalog: None, models: None, upgrades: Vec::new(), }; @@ -587,6 +591,543 @@ async fn a_door_claiming_one_path_over_two_carriers_mounts_it_once() { ); } +/// THE SESSION DOOR'S ROOT-LEG CELLS (ARCHITECT 2026-10-05 Q1): the session door, served as +/// production composes it — the browser's ephemeral-secret mint, a keyed caller, the door's DIRECT +/// route to the top-level catalog model its section's `session.model` names, the provider's own +/// credential presented by the auth plugin, the caller's spend posted against the key that +/// presented it. The door is found among the linked plane doors by the audio class it meters; the +/// module is compiled where the build links that door, and there a door not found is a failure, +/// never a skip. The secret-source scan (`secret_source.rs`) drives the same door. +#[cfg(linked_section_streams)] +pub(super) mod session_door { + use std::collections::BTreeMap; + use std::sync::Arc; + + use axum::http::StatusCode; + use busbar_contract::caps::ReasonCode; + use busbar_contract::conn::{DeclaredConns, PollConns}; + use busbar_kernel::cost::CostModel; + use busbar_kernel::governance::signing::{TokenSigner, DEFAULT_KID}; + use busbar_kernel::governance::{GovState, MemoryStore, NewKeySpec}; + use busbar_kernel::plane_driver::{refusal_status, EndPost, PlaneMoney}; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + + use crate::root::door_steps::{provider_routes, DoorReach, OutboundAuths}; + use crate::root::loader::dispatch::kinds::plane::Plane; + use crate::root::loader::dispatch::{ + load_linked, Bind, DispatchConfig, Dispatcher, LinkedRow, NoSink, + }; + use crate::root::plane_node::{Node, NodeEndPost}; + use crate::root::serve::planes_tests::{composed_services, Published, PUBLISHING}; + use crate::root::serve::{compose_planes, door_routes, DoorEgress}; + + /// The deployment's dated card history the served unit is pinned to at its door: one entry, no + /// price (billing off: the counts are the unit's fact and price at nothing). + static CARD: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + let holder = crate::root::kernel::RootHistory::default(); + holder.apply( + crate::root::kernel::card_from_config( + std::iter::empty::<(&str, busbar_contract::billing::RawTierRates)>(), + 0, + true, + ), + 1_000, + ); + holder + }); + + /// The provider's credential, as its file holds it. + const CREDENTIAL: &str = "sk-door-test"; + + /// The session door's mint claim. + const MINT: &str = "/v1/realtime/client_secrets"; + + /// The provider's answer to the mint: an ephemeral secret and its expiry. + const MINTED: &str = r#"{"value":"ek_door_0001","expires_at":1767225600}"#; + + /// The deployment's public base URL the session door fronts. + const PUBLIC: &str = "http://gw.test"; + + /// The billable class the session door meters a caller's audio under: how the harness finds that + /// door among the linked plane doors without spelling the plane. + const AUDIO_CLASS: &str = "audio_seconds_in"; + + /// A POST of `body` (JSON) to `path` on `router`, with `token` as its bearer or with none. + async fn send_body( + router: &axum::Router, + path: &str, + token: Option<&str>, + body: &'static str, + ) -> axum::response::Response { + use tower::ServiceExt as _; + let mut req = axum::http::Request::builder() + .method("POST") + .uri(path) + .header("content-type", "application/json"); + if let Some(token) = token { + req = req.header("authorization", format!("Bearer {token}")); + } + let req = req.body(axum::body::Body::from(body)).expect("a request"); + router + .clone() + .oneshot(req) + .await + .expect("the router answers") + } + + /// A far end on loopback answering every request 200 with `answer` (JSON); what it was sent comes + /// back on the channel, one request (head and body) per connection. + async fn far_end_answering( + answer: &'static str, + ) -> (u16, tokio::sync::mpsc::UnboundedReceiver) { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0") + .await + .expect("a loopback port"); + let port = listener.local_addr().expect("its address").port(); + let (sent, heard) = tokio::sync::mpsc::unbounded_channel(); + tokio::spawn(async move { + while let Ok((mut socket, _)) = listener.accept().await { + let sent = sent.clone(); + tokio::spawn(async move { + let mut buf = vec![0u8; 16 * 1024]; + let mut got = Vec::new(); + // The head, then the body its content-length states. + loop { + let Ok(n) = socket.read(&mut buf).await else { + return; + }; + if n == 0 { + return; + } + got.extend_from_slice(&buf[..n]); + let text = String::from_utf8_lossy(&got).to_string(); + if let Some(at) = text.find("\r\n\r\n") { + let length = text[..at] + .lines() + .find_map(|l| { + let (name, value) = l.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then(|| value.trim().parse::().ok()) + .flatten() + }) + .unwrap_or(0); + if got.len() >= at + 4 + length { + let _ = sent.send(text); + break; + } + } + } + let reply = format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\n\ + connection: close\r\n\r\n{answer}", + answer.len() + ); + let _ = socket.write_all(reply.as_bytes()).await; + let _ = socket.shutdown().await; + }); + } + }); + (port, heard) + } + + /// The session door, served as production composes it, and what each root-leg cell reads. + pub(in crate::root::serve) struct SessionDoor { + _published: Published, + router: axum::Router, + heard: tokio::sync::mpsc::UnboundedReceiver, + gov: Arc, + pub(in crate::root::serve) app: Arc, + key: busbar_contract::records::VirtualKey, + /// The key's data-plane token. + plain: String, + /// What a config apply refreshes the served door onto (`DoorApply::apply`). + pub(in crate::root::serve) live: Arc, + money: Arc, + post: Arc, + book: crate::root::durability::NodeBook, + } + + /// The linked plane door that meters audio (the session door), bound under `instance`; `None` in + /// a build that links none. + fn session_door( + instance: &'static str, + dispatcher: &Arc, + conns: Arc, + ) -> Option> { + crate::LINKED.plane_doors.iter().find_map(|door| { + let row = LinkedRow::of(*door).expect("the door states its Statement"); + let plane = load_linked::( + &row, + Bind { + instance: Arc::from(instance), + max_inflight_cap: 64, + sink: Arc::new(NoSink), + dispatcher: dispatcher.adopter(), + conns: crate::root::loader::dispatch::ConnTable::Host(Arc::clone(&conns)), + }, + ) + .expect("the linked door binds"); + plane + .served() + .billable_classes + .contains(&AUDIO_CLASS) + .then_some(plane) + }) + } + + /// The process's connector over every linked transport door, its destination guard letting + /// the loopback far end through. + fn connector() -> Arc { + let judge = crate::root::connector::guard_for(&busbar_kernel::config::Destinations { + block_private_addresses: false, + ..Default::default() + }) + .expect("the guard"); + busbar_core_connector::process::build( + || { + crate::root::connector::entries( + crate::LINKED_TRANSPORT_DOORS, + &busbar_contract::transport::TransportSettings::default(), + ) + }, + judge, + &[], + Arc::new(|_| {}), + busbar_core_connector::pool::PoolPosture::NONE, + ) + .expect("the connector builds") + } + + /// The linked session door's Statement name (the plane door that meters audio, bound only to + /// read it); `None` in a build that links none. It holds the publishing lock, so it is called + /// outside a runtime. + pub(in crate::root::serve) fn name() -> Option { + let _one = PUBLISHING.blocking_lock(); + let instance = "session-door-name"; + let _published = Published(instance); + let dispatcher = Arc::new(Dispatcher::new(DispatchConfig::default())); + session_door(instance, &dispatcher, connector() as Arc) + .map(|plane| plane.name().to_owned()) + } + + /// [`session_keyed`] over a provider credential its file holds; the composition must succeed. + async fn session_served(instance: &'static str, budget: Option) -> Option { + let key_file = std::env::temp_dir().join(format!( + "busbar-serve-door-{instance}-{}", + std::process::id() + )); + std::fs::write(&key_file, CREDENTIAL).expect("the credential file"); + let api_key = format!("{{file: '{}'}}", key_file.display()); + let served = session_keyed(instance, budget, &api_key).await; + let _ = std::fs::remove_file(&key_file); + served.map(|s| { + s.unwrap_or_else(|e| panic!("the session door composes, its egress sealed: {e}")) + }) + } + + /// [`SessionDoor`]: the linked session door (the plane door that meters audio) bound through + /// the loader's one load, its needs declared on the connector over every linked transport door; + /// a signing governance book with one key (in a group whose all-time budget is `budget`, the + /// door's per-request fee one, or no group and no fee); a provider on the protocol the door + /// states a bearer default for, its credential the secret reference `api_key` (YAML), on a + /// loopback far end answering the mint, reached through the catalog model its section's + /// `session.model` names; the door composed under [`PUBLIC`] with its egress sealed by the + /// composition itself, and its claims mounted on the data router. `None` in a build that links + /// no session door; the composition's refusal is answered rather than panicked on: what the + /// root's boot dies on. + pub(in crate::root::serve) async fn session_keyed( + instance: &'static str, + budget: Option, + api_key: &str, + ) -> Option> { + let published = Published(instance); + let (port, heard) = far_end_answering(MINTED).await; + let connector = connector(); + let dispatcher = Arc::new(Dispatcher::new(DispatchConfig::default())); + let plane = session_door( + instance, + &dispatcher, + Arc::clone(&connector) as Arc, + )?; + let plane_name = plane.name().to_owned(); + let section_key = plane.served().section; + // THE PROVIDER'S PROTOCOL, read off the door's Statement rather than spelled here: the last + // dialect it states a `bearer` default outbound style for (its far-end spellings follow its + // arrival dialects), so this harness names no dialect. + let protocol = { + let served = plane.served(); + served + .dialect_auth + .iter() + .rev() + .filter(|(_, style, _)| *style == "bearer") + .find_map(|(d, _, _)| { + usize::try_from(*d) + .ok() + .and_then(|i| served.dialects.get(i).copied()) + }) + .expect("the session door states a bearer-style dialect") + }; + + let groups: BTreeMap = budget + .map(|amount| { + let limit = busbar_kernel::config::groups::LimitCfg { + metric: busbar_kernel::config::groups::LimitMetric::Budget, + amount, + per: Some(busbar_kernel::config::groups::LimitWindow::Total), + scope: None, + on_exhaust: None, + downgrade_to: None, + admission: None, + on_exhaustion: None, + }; + let cfg = busbar_kernel::config::GroupCfg { + parent: None, + enabled: true, + limits: vec![limit], + ..Default::default() + }; + (format!("{instance}-group"), cfg) + }) + .into_iter() + .collect(); + let cost = match budget { + None => CostModel::flat(1), + Some(_) => { + let fees: busbar_kernel::config::PlaneFeesMap = [( + plane_name.clone(), + busbar_kernel_ledger::cost::PlaneFees { + per_request: 1, + per_session: 0, + }, + )] + .into_iter() + .collect(); + CostModel::resolve_parts(None, 0, &groups).with_plane_fees(&fees) + } + }; + let signer = TokenSigner::from_secret_bytes(&[7u8; 32], DEFAULT_KID); + let gov = Arc::new( + GovState::new_with_signer(Arc::new(MemoryStore::new()), None, Some(signer)) + .expect("governance"), + ); + let (key, plain) = gov + .mint_signed( + NewKeySpec { + name: "browser".to_string(), + group: groups.keys().next().cloned(), + ..Default::default() + }, + 4_000_000_000, + 1_700_000_000, + ) + .expect("mint"); + gov.hydrate_budgets(&cost, 0).expect("hydrate"); + let node = Arc::new(Node::new()); + let book = crate::root::durability::node_book_over(Box::new(|| CARD.pin())); + node.bind_book(Arc::clone(&book.durability)); + let post = Arc::new(NodeEndPost::new(Arc::clone(&node))); + let money = Arc::new(PlaneMoney::new( + Arc::clone(&gov), + Arc::clone(&post) as Arc, + )); + let one = Arc::clone(&money); + + let provider: busbar_kernel::config::ProviderCfg = serde_yaml::from_str(&format!( + "{{protocol: {protocol}, base_url: 'http://127.0.0.1:{port}', api_key: {api_key}, error_map: {{}}}}", + )) + .expect("a provider entry"); + let providers = provider_routes( + &std::collections::HashMap::from([("p".to_string(), provider)]), + &busbar_kernel::config_validate::MetadataPosture::default(), + ); + let catalog: std::collections::HashMap = + serde_yaml::from_str("{m-cap: {provider: p}}").expect("the models catalog"); + let secrets = busbar_kernel::config::secret::SecretResolver::builtins_only(); + let auths = OutboundAuths::new( + Arc::clone(&dispatcher), + crate::LINKED.auths, + None, + crate::root::loader::dispatch::ConnTable::Host( + Arc::clone(&connector) as Arc + ), + ); + let reach = DoorReach { + providers: &providers, + secrets: &secrets, + auths: Arc::new(auths), + conns: Arc::clone(&connector) as Arc, + stream_ceiling_secs: 600, + catalog: Some(&catalog), + models: None, + upgrades: Vec::new(), + }; + let mut sections = BTreeMap::new(); + sections.insert( + section_key, + serde_yaml::from_str("session: {model: m-cap}").expect("a section"), + ); + let mut served = match compose_planes( + &[(instance.to_string(), plane)], + &dispatcher, + &composed_services(), + §ions, + Some(PUBLIC), + &move || Arc::clone(&one), + Some(&DoorEgress { + reach: &reach, + journal: Arc::clone(&post) as Arc, + }), + None, + ) { + Ok(served) => served, + Err(refusal) => return Some(Err(refusal)), + }; + assert!( + served.planes[0].live.current().egress.is_some(), + "the composition sealed its egress" + ); + let live = Arc::clone(&served.planes[0].live); + served.post = Some(Arc::clone(&post)); + let app = busbar_kernel::test_support::TestApp::new().keys_chain(); + let app = groups + .iter() + .fold(app, |app, (name, cfg)| app.group(name, cfg.clone())); + let app = app.governance(Arc::clone(&gov)).cost(cost).build(); + let doors = door_routes(served, || CARD.pin(), &[], &[]).expect("its claims mount"); + let (router, _admin, _handle) = + busbar_kernel::build_split_routers_serving(Arc::clone(&app), doors, 1 << 20, 0, false); + Some(Ok(SessionDoor { + _published: published, + router, + heard, + gov, + app, + key, + plain: plain.expose_secret().clone(), + live, + money, + post, + book, + })) + } + + impl SessionDoor { + /// The keyed caller's mint through the door: its response. + pub(in crate::root::serve) async fn mint(&self) -> axum::response::Response { + send_body(&self.router, MINT, Some(&self.plain), "{}").await + } + + /// The requests the governance book admitted for the key, this window. + fn requests(&self) -> u64 { + self.gov + .usage_for(&self.app.cost, &self.key.id, busbar_kernel::store::now()) + .expect("a read") + .expect("the key exists") + .requests + } + + /// The node book's lines for the session door's catalog route. + fn lines(&self) -> usize { + let rows = self.book.durability.lock().expect("unpoisoned").read_back(); + rows.iter() + .filter(|p| p.counts.as_ref().is_some_and(|c| c.lane.ends_with("m-cap"))) + .count() + } + } + + /// THE SESSION DOOR'S MINT, SERVED (its root leg, ARCHITECT 2026-10-05 Q1): a keyed caller's + /// `POST /v1/realtime/client_secrets` is one unit the door carries (ARRIVAL), admitted for the + /// key its token resolves (AUTHENTICATE), dialled to the destination the guard judged (VERIFY) + /// on the DIRECT route to the catalog model its section's `session.model` names (ROUTE), sent + /// WITH THE PROVIDER'S OWN CREDENTIAL and never the caller's token (EGRESS-AUTH), charged to + /// the key that presented it (METER, GOVERNANCE-BUDGET), and closed once with its one line on + /// the node's book (AUDIT, EXIT). (The door's audience binding is the deployment's mount + /// table's, judged end to end by the oracle's wrong-audience mint cell.) + /// + /// RED ARM: the same request with no credential is refused before anything is dialled or charged. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn a_session_door_mint_is_served_under_the_providers_credential() { + let _one = PUBLISHING.lock().await; + let mut s = session_served("serve-door-session", None) + .await + .expect("the build links the session door"); + + let refused = send_body(&s.router, MINT, None, "{}").await; + assert_eq!( + refused.status().as_u16(), + u16::try_from(refusal_status(ReasonCode::Unauthenticated)).expect("a status"), + "an unkeyed caller is refused at the session door" + ); + assert!(s.heard.try_recv().is_err(), "nothing was dialled for it"); + assert_eq!(s.requests(), 0, "nor charged"); + + let response = send_body(&s.router, MINT, Some(&s.plain), "{}").await; + assert_eq!(response.status(), StatusCode::OK, "served through the door"); + let body = axum::body::to_bytes(response.into_body(), 1 << 16) + .await + .expect("the body"); + let body: serde_json::Value = serde_json::from_slice(&body).expect("the door's JSON"); + assert_eq!( + body, + serde_json::json!({"value": "ek_door_0001", "expires_at_unix": 1_767_225_600_u64}), + "the minted secret and its expiry" + ); + + let head = s.heard.recv().await.expect("the provider was dialled"); + assert!(head.starts_with(&format!("POST {MINT} ")), "{head}"); + assert!( + head.lines() + .any(|l| l.eq_ignore_ascii_case(&format!("authorization: Bearer {CREDENTIAL}"))), + "the provider's own credential: {head}" + ); + assert!( + !head.contains(&s.plain), + "the caller's token never reaches the provider: {head}" + ); + assert!( + head.contains(r#""model":"m-cap""#), + "the locked session on the catalog model: {head}" + ); + + assert_eq!( + s.requests(), + 1, + "one admitted request, on the presenting key" + ); + assert_eq!(s.money.open_units(), 0, "its money facts closed"); + assert_eq!(s.post.open_units(), 0, "its node facts closed"); + assert_eq!(s.lines(), 1, "its one line on the node's book"); + } + + /// ADMIT, at the session door (its root leg): a key whose group budget is already spent is + /// refused at admission — over budget, before the provider is dialled, charged nothing, no line + /// written — while a key with room is served (the mint cell above). RED: a door that dialled + /// before admission would have reached the far end. + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn a_spent_key_is_refused_at_the_session_door_before_any_dial() { + let _one = PUBLISHING.lock().await; + let mut s = session_served("serve-door-session-spent", Some(0)) + .await + .expect("the build links the session door"); + let response = send_body(&s.router, MINT, Some(&s.plain), "{}").await; + assert_eq!( + response.status().as_u16(), + 429, + "an over-budget caller is refused at admission" + ); + assert!( + s.heard.try_recv().is_err(), + "the provider was never dialled" + ); + assert_eq!(s.requests(), 0, "the refusal charged nothing"); + assert_eq!(s.money.open_units(), 0); + assert_eq!(s.post.open_units(), 0); + assert_eq!(s.lines(), 0, "no line on the node's book"); + } +} + /// THE ROOT-MCP LEG'S LOOP CELLS (U14): the MCP plane served through its memory-ABI door, composed by /// this file's composition (`compose_planes`, `door_routes`) on the kernel's plane driver, each core /// capability of the capability-equality matrix (`qa/capability-equality.json`, the `mcp-client` and @@ -2567,6 +3108,7 @@ async fn serve_configured( auths: Arc::new(auths), conns: Arc::clone(&connector) as Arc, stream_ceiling_secs: 600, + catalog: None, models: None, upgrades: Vec::new(), }; diff --git a/crates/busbar/tests/composition_root_names_no_linked_plugin.rs b/crates/busbar/tests/composition_root_names_no_linked_plugin.rs index 3a35e18df9..05a327849c 100644 --- a/crates/busbar/tests/composition_root_names_no_linked_plugin.rs +++ b/crates/busbar/tests/composition_root_names_no_linked_plugin.rs @@ -418,7 +418,7 @@ fn every_on_driver_switch_links_its_door_and_the_default_build_does_not() { let switched = doors_of(&|f: &str| f == switch || default.iter().any(|d| d == f)); let added: Vec<&str> = switched .split(", ") - .filter(|d| d.ends_with("::plane_door::door") && !shipped.contains(*d)) + .filter(|d| d.ends_with("::door") && !shipped.contains(*d)) .collect(); assert!( !added.is_empty(), diff --git a/crates/busbar/tests/fixtures/billing_plane_legs.txt b/crates/busbar/tests/fixtures/billing_plane_legs.txt index 4777ddb6f9..710fc27ed7 100644 --- a/crates/busbar/tests/fixtures/billing_plane_legs.txt +++ b/crates/busbar/tests/fixtures/billing_plane_legs.txt @@ -4,9 +4,8 @@ # unit / # served / # session / / -# A door plane (the `plane-door` axis: busbar-plane-mcp since FLIP-MCP) has no row: it is not a -# linked plane, and its units bill through the kernel plane driver's one money seam -# (`plane_driver::money`, PlaneMoney), proven by the driver suites. +# A door plane (the `plane-door` axis: busbar-plane-mcp since FLIP-MCP, busbar-plane-streaming since +# FLIP-STREAMING) has no row: it is not a linked plane, and its units bill through the kernel plane +# driver's one money seam (`plane_driver::money`, PlaneMoney), proven by the driver suites. busbar-llm unit busbar-llm/src/unit/node.rs busbar-a2a served busbar-a2a/src/a2a/receive.rs ledger_hop_bytes CLASS_BYTES -busbar-voice session busbar-voice/src/runtime/metering.rs report_turn busbar-plane-streaming/src/session.rs class_counts CLASS_AUDIO_TOKENS_OUT diff --git a/crates/busbar/tests/fixtures/law7_plane_doors.txt b/crates/busbar/tests/fixtures/law7_plane_doors.txt index 807c752f4b..d7b932a0fa 100644 --- a/crates/busbar/tests/fixtures/law7_plane_doors.txt +++ b/crates/busbar/tests/fixtures/law7_plane_doors.txt @@ -2,7 +2,7 @@ # is configured, and that a 1.5.5 config must leave to the fallback catch-all. One door per line: # where is `both` (probed unconfigured AND configured) or `unconfigured` # (probed on the 1.5.5 config only). Order is probe order. -/v1/realtime both +/v1/realtime/client_secrets both /v1/realtime/gemini unconfigured /mcp both /a2a both diff --git a/crates/busbar/tests/fixtures/plane_doctrine.txt b/crates/busbar/tests/fixtures/plane_doctrine.txt index 610c745b39..cd7040bb67 100644 --- a/crates/busbar/tests/fixtures/plane_doctrine.txt +++ b/crates/busbar/tests/fixtures/plane_doctrine.txt @@ -33,7 +33,6 @@ crate llm llm # (`crate mcp` STRUCK, P3 DEL-MCP, ARCHITECT 2026-10-05: crates/busbar-mcp is deleted; the mcp plane is # `plane-mcp` below.) crate a2a a2a-client a2a-server -crate voice streaming-client streaming-server crate plane-llm llm crate plane-mcp mcp-client mcp-server crate plane-a2a a2a-client a2a-server @@ -45,13 +44,13 @@ crate plane-decisions decisions # # installed ...: the doctrine's installed-plane axis (plane_isomorphism.rs) # — each plane the composition root installs from its own linked entry, and the directional ledger -# column(s) it answers to. +# column(s) it answers to. The streaming plane is not on it since FLIP-STREAMING: it is served only +# through its memory-ABI door (`plane-door` axis), which has no linked PlaneDecl to compare. installed llm llm # (`installed mcp` STRUCK, P3 DEL-MCP, ARCHITECT 2026-10-05: the mcp plane is served through its # memory-ABI door alone and installs no PlaneDecl, so it is not on the reflected axis; its columns are # still answered by the `root-mcp` leg.) installed a2a a2a-client a2a-server -installed streaming streaming-client streaming-server # # owned-section ...: the linked planes that own the # grammar of a top-level section, and those sections (plane_owned_sections_boot.rs). @@ -59,4 +58,6 @@ installed streaming streaming-client streaming-server # PlaneDecl planes' owned sections, and the mcp plane installs no PlaneDecl any more. Its `tools` / # `mcp` sections are declared by its door's Statement, which the config-schema gate reads as a # door-only plane's declaration (xtask/src/gates/config_schema/declared.rs).) -owned-section streaming streams streams +# (`owned-section streaming streams streams` STRUCK, FLIP-STREAMING: `streams:` is the streaming +# door's, folded from its Statement into the kernel registry (DECL-FOLD, ARCHITECT Q-DEL-A2A-DECL; +# spec #49), not a linked plane's. No linked plane owns a section now.) diff --git a/crates/busbar/tests/plane_isomorphism.rs b/crates/busbar/tests/plane_isomorphism.rs index 2e173cea25..0348f7f1e3 100644 --- a/crates/busbar/tests/plane_isomorphism.rs +++ b/crates/busbar/tests/plane_isomorphism.rs @@ -533,13 +533,14 @@ fn the_reflected_hook_set_and_constants_are_the_doctrine() { const { assert!(MIN_HOOK_FIELDS >= 15 && MIN_ASYMMETRIES >= 10); } - // The doctrine's installed-plane axis, verbatim (the same three the composition root installs as + // The doctrine's installed-plane axis, verbatim (the same two the composition root installs as // a PlaneDecl: P3 DEL-MCP, ARCHITECT 2026-10-05, took mcp off it — the plane is served through its - // memory-ABI door alone and installs no PlaneDecl). + // memory-ABI door alone and installs no PlaneDecl — and the session plane left it when it too + // became door-only). let keys: Vec<&str> = plane_ledger_columns().iter().map(|(k, _)| *k).collect(); assert_eq!( keys.len(), - 3, + 2, "the installed-plane axis is the owner's ruling; changing it is a doctrine change: {keys:?}" ); // With every plane compiled in, the pinned axis is exactly the planes the root installs from diff --git a/crates/busbar/tests/plane_owned_sections_boot.rs b/crates/busbar/tests/plane_owned_sections_boot.rs index 3520d02622..ca86347f65 100644 --- a/crates/busbar/tests/plane_owned_sections_boot.rs +++ b/crates/busbar/tests/plane_owned_sections_boot.rs @@ -29,10 +29,16 @@ fn owning_planes() -> Vec<&'static PlaneDecl> { .iter() .filter(|d| !d.owned_config_sections.is_empty()) .collect(); + // Non-vacuity, against the doctrine: with every plane compiled in, some linked plane owns a + // section exactly when the doctrine pins an `owned-section` row. It pins none: both + // section-owning planes are door planes, whose sections their doors' Statements declare + // (DECL-FOLD), not a linked PlaneDecl. #[cfg(linked_every_plane)] - assert!( - !owning.is_empty(), - "non-vacuity: with every plane compiled in, some linked plane owns a section" + assert_eq!( + owning.is_empty(), + common::doctrine_rows("owned-section").is_empty(), + "non-vacuity: with every plane compiled in, the linked planes own a section exactly when \ + the doctrine pins one" ); owning } diff --git a/crates/busbar/tests/plane_registration_is_contract_data.rs b/crates/busbar/tests/plane_registration_is_contract_data.rs index 08de194def..9b4b862ebd 100644 --- a/crates/busbar/tests/plane_registration_is_contract_data.rs +++ b/crates/busbar/tests/plane_registration_is_contract_data.rs @@ -137,15 +137,14 @@ fn every_plane_registration_item_is_contract_data_naming_no_kernel_type() { offenders.join("\n") ); - // NON-VACUITY: the engine planes register through the contract type: llm, a2a and voice. The - // floor was five (those three, mcp and decisions); FLIP-MCP and FLIP-DECISIONS each deleted one - // item, since each of those planes' rows is now its door's Statement, folded by the composition - // root. Fewer than three read means the scan read nothing, and zero offenders over zero items is - // not a pass. + // NON-VACUITY: the engine planes register through the contract type: llm and a2a. The floor was + // five; each plane flip to a door since deleted one item, since each flipped plane's row is now + // its door's Statement, folded by the composition root. Fewer than two read means the scan read + // nothing, and zero offenders over zero items is not a pass. let contract_typed = items.iter().filter(|i| i.ty == CONTRACT_TYPE).count(); assert!( - contract_typed >= 3, - "expected at least three `{CONTRACT_TYPE}` registration items under crates/, found \ + contract_typed >= 2, + "expected at least two `{CONTRACT_TYPE}` registration items under crates/, found \ {contract_typed}: {:?}", items.iter().map(|i| &i.at).collect::>() ); diff --git a/crates/plugin-loader/src/dispatch/kinds/transport.rs b/crates/plugin-loader/src/dispatch/kinds/transport.rs index b39d701eec..b56707f513 100644 --- a/crates/plugin-loader/src/dispatch/kinds/transport.rs +++ b/crates/plugin-loader/src/dispatch/kinds/transport.rs @@ -76,6 +76,19 @@ pub struct TransportFacts { pub status_rows: Vec<(u32, u32, u32)>, } +/// WHAT A COMPILED-IN TRANSPORT DOOR STATES, read off its Statement without binding it (no +/// `validate` or `open` runs), checked as a bind checks it. +/// +/// # Errors +/// +/// The door, its Statement or its transport tail is refused. +pub fn stated( + door_fn: busbar_contract::abi::mechanism::door::DoorFn, +) -> Result { + let st = crate::dispatch::load::statement_of(door_fn).map_err(|e| e.to_string())?; + tail_facts(&st) +} + /// A plugin string, interned. fn owned(s: AbiStr, field: &str) -> Result<&'static str, String> { if s.len == 0 { diff --git a/crates/plugin-loader/src/dispatch/load.rs b/crates/plugin-loader/src/dispatch/load.rs index 4e489dff53..c1fe92201d 100644 --- a/crates/plugin-loader/src/dispatch/load.rs +++ b/crates/plugin-loader/src/dispatch/load.rs @@ -104,6 +104,17 @@ impl LinkedRow { } } +/// A compiled-in door's Statement, checked as [`rendering_of`] checks it, for a kind's own reader +/// of its tail (`kinds::transport::stated`). +/// +/// # Errors +/// +/// The refusal the door or its Statement earns. +pub(crate) fn statement_of(door_fn: DoorFn) -> Result { + let (door, _) = read_door(door_fn(), None)?; + statement(&door) +} + /// THE RENDERING a door's Statement makes, for any kind: the door's head checks (magic, mechanism, /// size, a known kind at this host's ABI version for it) and the Statement's, then /// [`render`]. The pack tool signs it into a dropped plugin's manifest; [`LinkedRow::of`] states it diff --git a/deny.toml b/deny.toml index b12a84ccff..8048a18495 100644 --- a/deny.toml +++ b/deny.toml @@ -18,7 +18,7 @@ ignore = [ "RUSTSEC-2024-0436", # RUSTSEC-2024-0449: `unsafe-libyaml` 0.2.11 is unmaintained. It is pulled in only via # serde_yaml_ng 0.10, busbar's YAML config parser (`cargo tree -i unsafe-libyaml`: - # busbar, busbar-a2a, busbar-contract, busbar-kernel, busbar-voice, busbar-core-oauth2, + # busbar, busbar-a2a, busbar-contract, busbar-kernel, busbar-core-oauth2, # xtask and others -> serde_yaml_ng -> unsafe-libyaml). Advisory is "unmaintained", not a # security hole. There is no safe swap now: a different YAML parser risks 1.5.5 config-parse # parity (customer-visible behaviour, guarded by the oracle). Re-evaluate when serde_yaml_ng diff --git a/docs/design/1.6.0-TODO.md b/docs/design/1.6.0-TODO.md index ed7a161d8a..2da2b2a99c 100644 --- a/docs/design/1.6.0-TODO.md +++ b/docs/design/1.6.0-TODO.md @@ -1712,6 +1712,8 @@ Three HIGH, two of them money, all verified by hand. ## The caller's audio is silently discarded, and the call still settles usage +**FIXED (lane FOLD-STREAMING, `$` PR lane-fold-streaming-telephony-money):** the telephony leg reads every caller frame through the Twilio envelope (`codec::topology::twilio::TwilioBridge`, its one home); caller `media` reaches the far end as an uplink append and is counted, `stop` or a refused frame ends the call, and the far end's audio reaches the caller as Twilio `media`. Tests: `busbar-voice` `topology/tests/mod.rs`. + `busbar-voice`'s telephony endpoint `/v1/realtime/telephony/{call_id}` is live, mounted, governed and provider-dialing. It feeds raw client bytes straight into `OpenAiRealtimeCodec::read_up`, which understands only OpenAI's `type`-tagged JSON. **Real Twilio Media Streams frames are `event`-tagged**, @@ -1735,6 +1737,8 @@ which nobody could hear them. ## The session cost ceiling is documented, plumbed, and never compared to anything +**CLOSED on predev (Q21a/Q37):** an optional `streams.session_max_secs` is compared on the sweep tick (`SessionCore::enforce_ceiling`), and absent means no limit. Test: `busbar-voice` `runtime/tests.rs` `a_configured_ceiling_closes_the_session_and_settles_its_counts`. + `runtime/mod.rs:67` declares `session_max_secs` as *"the hard session wall-clock ceiling (`streams.session_max_secs:`) **the pump enforces**"*, default 3600s. I traced every reference: a doc comment, a field declaration, a default fn, a serde attribute, and four plumbing assignments. diff --git a/qa/capability-equality.json b/qa/capability-equality.json index b109bd3007..8636e74e34 100644 --- a/qa/capability-equality.json +++ b/qa/capability-equality.json @@ -100,12 +100,12 @@ "note": "the A2A plane's SERVED root leg: the inbound call driven through the real router and the kernel-loop rider `gauntlet_install::install()` registers for the plane's key (DECISIONS #28), each root cell running the plane's own served witness (`busbar_a2a::testkit::SERVED`) and requiring its `drive` to have run inside the loop" }, "root-voice": { - "file": "crates/busbar/src/root/tests/gauntlet_kernel.rs", + "file": "crates/busbar/src/root/tests/serve_tests.rs", "columns": [ "streaming-client", "streaming-server" ], - "note": "the voice (streaming) plane's SERVED root leg: a live session opened at the plane's served door through the kernel-loop SESSION rider `gauntlet_install::install()` registers for the plane's key (DECISIONS #28), each root cell running the plane's own served witness (`busbar_voice::testkit::SERVED`) and requiring the session's open to have run through the loop's door; the turns, their metering on the kernel's session account (Q21b), the audit row and the durable chain are the served session the witness then drives" + "note": "the streaming plane's SERVED root leg since FLIP-STREAMING: the streaming door, linked and bound through the loader's one load, composed as production seals it (its egress over the connector, its DIRECT route to the catalog model streams.session.model names) and mounted on the data router, driven end to end by root/tests/serve_tests.rs (carried by serve.rs through #[path]); the kernel-loop session rider no longer carries this plane" }, "root-admin": { "file": "crates/busbar/src/root/units_admin/tests/units_admin.rs", @@ -981,12 +981,11 @@ { "capability": "breaker-trip", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/breaker_tests.rs::a_hard_down_provider_records_into_the_core_cell_and_opens_it", + "state": "missing", "root": { "state": "none", "leg": "root-voice", - "reason": "the served session witness trips the provider cell with a guard-refused dial only to show the fast-fail after it; no served witness records a failing dial into the breaker cell and asserts the trip itself as its claim" + "reason": "no streaming-door cell dials a failing provider and asserts the trip of the core breaker cell; the door's served cells reach a live loopback provider only" } }, { @@ -1002,12 +1001,11 @@ { "capability": "breaker-fastfail", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/breaker_tests.rs::a_tripped_provider_cell_refuses_before_the_dial", + "state": "missing", "root": { - "state": "proven", + "state": "none", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_fast_fails_a_tripped_upstream" + "reason": "no streaming-door cell trips the provider's breaker cell before the mint; the served mint and the spent-key refusal are the door's two cells, neither drives a tripped upstream" } }, { @@ -1044,88 +1042,82 @@ "capability": "hooks-tap", "plane": "streaming-client", "state": "proven", - "test": "crates/busbar-voice/src/tests/hook_tap_tests.rs::a_rewrite_hook_edits_the_session_open_params_before_the_provider_dial", + "test": "crates/busbar-plane-streaming/tests/conformance.rs::a_session_open_is_projected_for_the_hooks_and_a_rewrite_patches_it", "root": { "state": "none", "leg": "root-voice", - "reason": "no served session witness attaches an observe or transform hook; the witnesses open sessions at the served door with no hook attached, so no payload on this leg passes through one" + "reason": "no streaming-door cell attaches an observe or transform hook to the mint; the door projects a session open for the hooks (its own conformance cell), but no root cell drives a hook stage over it" } }, { "capability": "hooks-tap", "plane": "streaming-server", "state": "proven", - "test": "crates/busbar-voice/src/tests/hook_tap_tests.rs::a_rewrite_hook_edits_the_session_open_params_before_the_provider_dial", + "test": "crates/busbar-plane-streaming/tests/conformance.rs::a_session_open_is_projected_for_the_hooks_and_a_rewrite_patches_it", "root": { "state": "none", "leg": "root-voice", - "reason": "no served session witness attaches an observe or transform hook; the witnesses open sessions at the served door with no hook attached, so no payload on this leg passes through one" + "reason": "the streaming door's served cells run with no hook stage composed (compose_planes is handed no HookStage), so no payload on the root leg passes through a tap or a rewrite" } }, { "capability": "hooks-gate", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/hook_gate_tests.rs::a_reject_all_operator_gate_refuses_a_ws_accept_before_the_upgrade", + "state": "missing", "root": { "state": "none", "leg": "root-voice", - "reason": "the refusal a session meets on the served leg's witnesses is the session-scope grant's at the door; no served witness attaches an operator gate, so hooks::gate::decide is reached by no cell on this leg" + "reason": "no streaming-door cell composes an operator gate; the refusals the door's root cells meet are the audience check and the spent budget, never hooks::gate::decide" } }, { "capability": "hooks-gate", "plane": "streaming-server", - "state": "proven", - "test": "crates/busbar-voice/src/tests/hook_gate_tests.rs::a_reject_all_operator_gate_refuses_a_ws_accept_before_the_upgrade", + "state": "missing", "root": { "state": "none", "leg": "root-voice", - "reason": "the refusal a session meets on the served leg's witnesses is the session-scope grant's at the door; no served witness attaches an operator gate, so hooks::gate::decide is reached by no cell on this leg" + "reason": "the served mint and the spent-key refusal reach the door with no gate hook composed, so a gate refusal before dispatch is driven by no root cell on this leg" } }, { "capability": "audit-chain", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/audit_chain_tests.rs::an_opened_voice_session_seals_a_genesis_event_and_survives_reattach_on_the_durable_chain", + "state": "missing", "root": { - "state": "proven", + "state": "none", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_chains_what_it_audits" + "reason": "the served mint's one line lands on the node's book, but no streaming-door cell reads the tamper-evident chain back and verifies its link" } }, { "capability": "audit-chain", "plane": "streaming-server", - "state": "proven", - "test": "crates/busbar-voice/src/tests/audit_chain_tests.rs::an_inbound_session_open_lands_a_sealed_genesis_event_the_front_door_wrote", + "state": "missing", "root": { - "state": "proven", + "state": "none", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_chains_what_it_audits" + "reason": "no root cell on the streaming door verifies the audit record chain across the mint's line or the spent-key refusal; the chain's own cells live on the durability tests" } }, { "capability": "governance-budget", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/governance_budget_tests.rs::a_session_lease_settled_past_its_cap_hard_closes_and_refuses_further_spend", + "state": "missing", "root": { "state": "proven", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_charges_the_presenting_key" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential" } }, { "capability": "governance-budget", "plane": "streaming-server", - "state": "proven", - "test": "crates/busbar-voice/src/tests/governance_budget_tests.rs::the_session_lease_bills_the_presenting_key_and_refuses_past_the_cap_with_a_hard_close", + "state": "missing", "root": { "state": "proven", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_charges_the_presenting_key" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_spent_key_is_refused_at_the_session_door_before_any_dial" } }, { @@ -1151,23 +1143,21 @@ { "capability": "metrics", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/metrics_tests.rs::a_voice_session_open_increments_the_plane_labelled_counter", + "state": "missing", "root": { - "state": "proven", + "state": "none", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_reports_each_session_it_opens" + "reason": "no streaming-door cell scrapes the metrics registry after the mint; the door's root cells assert the money and the wire, not the plane-labelled counters" } }, { "capability": "metrics", "plane": "streaming-server", - "state": "proven", - "test": "crates/busbar-voice/src/tests/metrics_tests.rs::a_voice_session_open_increments_the_plane_labelled_counter", + "state": "missing", "root": { - "state": "proven", + "state": "none", "leg": "root-voice", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_reports_each_session_it_opens" + "reason": "the door's served cells build no metrics recorder, so no session or mint counter under the plane's label is observed on this leg" } }, { @@ -1213,12 +1203,11 @@ { "capability": "egress-auth", "plane": "streaming-client", - "state": "proven", - "test": "crates/busbar-voice/src/tests/egress_tests.rs::the_provider_dial_carries_the_planned_credential_and_never_a_caller_token", + "state": "missing", "root": { - "state": "none", + "state": "proven", "leg": "root-voice", - "reason": "the served session witnesses compose no provider endpoint; the credential a provider dial is reached with is composed by the plane's compose step and no served witness drives its injection" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential" } }, { diff --git a/qa/construction.toml b/qa/construction.toml index dba9b40cb2..991c7c6102 100644 --- a/qa/construction.toml +++ b/qa/construction.toml @@ -53,12 +53,13 @@ scan_roots = ["crates/*/src", "crates/*/tests", "crates/*/benches"] # two lists share zero members. manifest-allowlist reads THIS one to decide what "a plane names # another plane" means. The two collapse into one definition when the legacy engines are deleted; # until then a reader must not take either as "the plane kind". +# `busbar-voice` is struck: FLIP-STREAMING deleted the legacy streams crate (#18). # # `busbar-mcp` IS STRUCK (P3 DEL-MCP, ARCHITECT 2026-10-05: "delete busbar-mcp, add `crates/busbar-mcp` # to [gate.census.deleted]"). The engine is deleted and the mcp plane is its door crate # `busbar-plane-mcp`, which `gate.plugin_kinds.plane` already scans; the census floor below drops -# 4 -> 3 by the one ruled deletion, and `ceiling-census` excuses exactly that one entry. -plane_crates = ["busbar-llm", "busbar-a2a", "busbar-voice"] +# 4 -> 2 by the two ruled deletions (mcp, voice), and `ceiling-census` excuses exactly those entries. +plane_crates = ["busbar-llm", "busbar-a2a"] # A file at one of these path fragments is TEST code (same rule the purity lint applies), in # addition to any `#[cfg(test)] mod … { … }` block inside a production file. # A bare `tests.rs` (Rust's `#[cfg(test)] mod tests;` file convention) is test code too. @@ -268,7 +269,7 @@ busbar-llm = 226 # busbar-a2a 233 -> 232 (downward, at the measure): push delivery names the kernel's engine seam once # instead of per signature. busbar-a2a = 232 -busbar-voice = 88 +# busbar-voice is struck: FLIP-STREAMING deleted the crate, and the row went with it. [rules.ports-only-tests] why = """The same reach from test code. Tests that name core internals pin the plane to core's \ @@ -288,7 +289,7 @@ counts can trend to zero at their own pace.""" busbar-llm = 1027 # STRUCK: busbar-mcp = 185 (P3 DEL-MCP, ARCHITECT 2026-10-05 — the crate is deleted). busbar-a2a = 98 -busbar-voice = 43 +# busbar-voice is struck: FLIP-STREAMING deleted the crate, and the row went with it. # ── 4. no-uninstalled-seam ─────────────────────────────────────────────────────────────────────── [rules.no-uninstalled-seam] @@ -747,6 +748,8 @@ busbar-transport-webrtc = ["str0m", "ring", "aes", "zeroize"] # The MCP plane's outbound wire (fold A5, ARCHITECT 2026-09-29 under the PLUGIN LIBRARIES ruling): # the `Mcp-Name` header's `=?base64?...?=` sentinel for a name a header value cannot carry. busbar-plane-mcp = ["base64", "indexmap"] +# The streaming plane's door reads the deployment's public base URL once (`door::absolute`). +busbar-plane-streaming = ["url"] # Ratchet, NOT a review: each busbar-plane-* adapter names its own dialect's legacy engine crate # (busbar-llm/-mcp/-a2a/-voice) for codecs only, read-only, no engine state crossing back — but # those engine crates are ALSO named in gate.plane_crates, so this is structurally the exact "a @@ -766,7 +769,6 @@ busbar-plane-llm = ["busbar-llm"] # + three third-party crates, and it names no engine crate in either spelling. The edge that exists # now runs the other way — `busbar-mcp` reads the plane's wire dialect — which is the direction a # plane's definition asks for and not a coupling this rule is about. -busbar-plane-streaming = ["busbar-voice"] # ── 16. source-denylist (BUSBAR-1.6.0.md THE DESIGN, §2) ─────────────────────────────────────────────────────── [rules.source-denylist] @@ -1075,15 +1077,18 @@ max_sites = 0 # kernel-minted Pass (`KernelSeal::acquire_for_kernel()`) instead of the test seal, and egress's # test-seal dev-dependency went with it. The rule's pattern matches no file under either crate, so # the entry is not repointed (a row on a path with no finding would only excuse a future forgery); -# it is struck, and both paths are held to the rule. The seal debt that remains is the two below. +# it is struck, and both paths are held to the rule. +# # STRUCK 2026-10-04: `crates/busbar-transport-stdio/src/tests/battery.rs`. GONE — the stdio row was # made door-only (ARCHITECT ruling (A)): its in-process `impl Transport` battery, which forged a # `TestKernelSeal` to drive that transport, was deleted and its coverage folded onto the carrier and # door batteries, none of which forge a seal. The rule's `impl … KernelSeal for` pattern now matches # NO file under `crates/busbar-transport-stdio/`, so the entry is not repointed (a row on a path with # no finding would only excuse a future forgery); it is struck, and the crate is held to the rule. +# +# STRUCK 2026-10-04: `crates/busbar-plane-streaming/src/tests/harness.rs`, deleted by the streaming +# fold (its tests drive the door). No seal debt remains. known_sites = [ - "crates/busbar-plane-streaming/src/tests/harness.rs", ] # ── 22. forbid-unsafe ──────────────────────────────────────────────────────────────────────────── @@ -2007,9 +2012,10 @@ because = "a type alias over the rows the loop posted, and a reader of it; the n # silent as deleting the table alone was. [gate.census] legacy_reach_prefixes = 4 -# 4 -> 3: `busbar-mcp` left `gate.plane_crates` as a ruled deletion ([gate.census.deleted] -# "crates/busbar-mcp", P3 DEL-MCP, ARCHITECT 2026-10-05) — the one drop `ceiling-census` excuses. -plane_crates = 3 +# 4 -> 2: `busbar-mcp` (P3 DEL-MCP) and `busbar-voice` (P3 streaming flip) left `gate.plane_crates` as +# ruled deletions, each named in [gate.census.deleted] (ARCHITECT 2026-10-05) — the drops +# `ceiling-census` excuses. +plane_crates = 2 # HOW MANY CRATE DIRECTORIES EACH KIND GLOB MATCHES. # @@ -2174,6 +2180,7 @@ abi = 1 # an unlisted vanished dir stays RED. [gate.census.deleted] "crates/hook-test-plugin" = "TODO P5: delete hook-test-plugin (ARCHITECT 2026-10-04)" +"crates/busbar-voice" = "P3 streaming flip: legacy crate deleted (ARCHITECT 2026-10-04)" "crates/busbar-mcp" = "P3 DEL-MCP: the mcp plane is served through its door alone; the legacy engine crate is deleted (ARCHITECT 2026-10-05, U11 / Q1)" "crates/plane-example" = "TODO P5: delete plane-example (Q128 audit BLOCK 11/12: a fixture plane plugin riding the HOT lane with C-unwind doors)" diff --git a/qa/dep-wall.ledger b/qa/dep-wall.ledger index 29234bb4e9..9cbedc8503 100644 --- a/qa/dep-wall.ledger +++ b/qa/dep-wall.ledger @@ -33,11 +33,3 @@ busbar-transport-http hyper-util busbar-transport-http socket2 busbar-transport-http tokio (net) busbar-transport-ws socket2 -busbar-voice hyper-util -busbar-voice reqwest -busbar-voice rustls -busbar-voice socket2 -busbar-voice std::net -busbar-voice tokio (net) -busbar-voice tokio-rustls -busbar-voice tokio::net diff --git a/qa/door-only.toml b/qa/door-only.toml index 324364976e..56d4da58ec 100644 --- a/qa/door-only.toml +++ b/qa/door-only.toml @@ -109,41 +109,6 @@ rule = "door-only:plugin-path" file = "crates/busbar-llm/src/unit/meter.rs" item = "busbar_plane_llm" -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/bin/voice-conform.rs" -item = "busbar_plane_streaming" - -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/lib.rs" -item = "busbar_plane_streaming" - -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/runtime/metering.rs" -item = "busbar_plane_streaming" - -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/runtime/mod.rs" -item = "busbar_plane_streaming" - -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/runtime/session.rs" -item = "busbar_plane_streaming" - -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/runtime/voice_plane_unit_oracle.rs" -item = "busbar_plane_streaming" - -[[finding]] -rule = "door-only:plugin-path" -file = "crates/busbar-voice/src/topology/mod.rs" -item = "busbar_plane_streaming" - [[finding]] rule = "door-only:static-seam" file = "crates/busbar-core-admin/src/v1/service.rs" @@ -197,11 +162,6 @@ rule = "door-only:static-seam" file = "crates/busbar-llm/src/unit/node.rs" item = "NATIVE_SEATS" -[[finding]] -rule = "door-only:static-seam" -file = "crates/busbar-voice/src/mount.rs" -item = "COMPOSED_GOVERNED_CALLS" - [[finding]] rule = "door-only:static-seam" file = "crates/plugin-loader/src/observe.rs" diff --git a/qa/kind-isolation.standing.txt b/qa/kind-isolation.standing.txt index 33d39201af..ea422af5a8 100644 --- a/qa/kind-isolation.standing.txt +++ b/qa/kind-isolation.standing.txt @@ -2,7 +2,5 @@ # One line per finding: \t\t\t
. The `--posture` run excuses exactly these, # at or below these figures, and reds anything new or higher. Written ONLY by # `cargo xtask gate --posture --write-standing`, which strikes and lowers and never adds. -kind-isolation:deps unlisted-dep-edge legacy -> plane 2 +kind-isolation:deps unlisted-dep-edge legacy -> plane 1 kind-isolation:deps unlisted-transitional crates/busbar-llm 1 -kind-isolation:test-deps unlisted-dep-edge cleanliness -> legacy 3 -kind-isolation:test-deps unlisted-dep-edge legacy -> cleanliness 2 diff --git a/qa/kind-isolation.toml b/qa/kind-isolation.toml index a5898f5ce4..6972c0b17f 100644 --- a/qa/kind-isolation.toml +++ b/qa/kind-isolation.toml @@ -32,15 +32,10 @@ # any row here whose `from` crate STILL EXISTS. A legacy crate that survives to the tag is the # retirement having failed, and this row is where that is reported, by name. -# `busbar-voice` is the retiring 1.5.x streams crate and `busbar-plane-streaming` is the plane it -# is draining into (`busbar-plane-voice` was deleted into it, #18/#83). The rule is written about -# the drain, not about any one crate, so this edge is named on exactly the same terms — leaving -# it out would have meant a rule that only sees the drain the ruling happened to mention. -[[transitional]] -from = "busbar-voice" -to = "busbar-plane-streaming" -reason = "legacy drain" - +# `busbar-voice` (the retiring 1.5.x streams crate) is GONE: FLIP-STREAMING deleted it once the +# streaming plane's door served every route (#18/#83), and its drain row into +# `busbar-plane-streaming` went with it. +# # The A2A and MCP planes were folded out of busbar-core the same way (W4.a/W4.b, 1.6.0): each # retiring plane names the plane-host half it is draining its transport/routing surface into. # `busbar-plane-a2a-host` is GONE (folded back into `busbar-a2a`, 858c93966). The drain it named @@ -503,14 +498,6 @@ kind = "plane" crate = "busbar-transport-stdio" kind = "plane" -[[cell]] -crate = "busbar-voice" -kind = "contract" - -[[cell]] -crate = "busbar-voice" -kind = "plane" - # ------------------------------------------------------------------------------------------------ # # `kind-isolation:deps` held the manifests to a table of kind-to-kind CLASSES in Rust source, and a @@ -683,16 +670,6 @@ cite = "BUSBAR-1.6.0.md THE DESIGN, §1 / §9 — the composition root is the why = "busbar names busbar-transport-webrtc: declared normal, optional in the shipped graph, 1 declaration(s) at this commit — the webrtc wire's one registration (its `transport-webrtc` linked row, off by default). The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." drain = "none: BUSBAR-1.6.0.md grants this class. Draining it would be an architecture change and not a landing — what this row holds is the NUMBER, so a second declaration of a granted edge is still a landing that has to say why." -[[dep]] -from = "busbar" -to = "busbar-voice" -half = "shipped" -count = "1" -verdict = "allowed" -cite = "BUSBAR-1.6.0.md THE DESIGN, §1 / §9 — the composition root is the one thing that names all three axes." -why = "busbar names busbar-voice: declared normal, optional in the shipped graph, 1 declaration(s) at this commit. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." -drain = "none: BUSBAR-1.6.0.md grants this class. Draining it would be an architecture change and not a landing — what this row holds is the NUMBER, so a second declaration of a granted edge is still a landing that has to say why." - [[dep]] from = "busbar-kernel" to = "busbar-contract" @@ -858,16 +835,6 @@ cite = "BUSBAR-1.6.0.md THE DESIGN, §1 / §9 — no clause grants this pair. why = "busbar-llm names busbar-kernel: declared normal in the shipped graph, 1 declaration(s) at this commit. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." drain = "delete the busbar-kernel dependency from busbar-llm's Cargo.toml. A crate reaches another kind through the contract face it is written against, never by naming the crate; `cargo xtask gate kind-isolation` prints the declaration that states it." -[[dep]] -from = "busbar-voice" -to = "busbar-kernel" -half = "shipped" -count = "1" -verdict = "not-allowed" -cite = "BUSBAR-1.6.0.md THE DESIGN, §1 / §9 — no clause grants this pair. Measured debt surfaced by the W4.a/W4.b busbar-core -> busbar-kernel and busbar-substrate -> busbar-substrate-values absorption (1.6.0): the edge is real and shipped, and this row holds the number rather than passing it in silence." -why = "busbar-voice names busbar-kernel: declared normal in the shipped graph, 1 declaration(s) at this commit. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." -drain = "delete the busbar-kernel dependency from busbar-voice's Cargo.toml. A crate reaches another kind through the contract face it is written against, never by naming the crate; `cargo xtask gate kind-isolation` prints the declaration that states it." - @@ -1325,16 +1292,6 @@ cite = "BUSBAR-1.6.0.md THE DESIGN, §9 (BUSBAR-1.6.0.md:1140-1141): busbar i why = "busbar names busbar-contract: declared dev in the test graph (features test-seal), 1 declaration(s) at this commit, beside the shipped edge. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." drain = "none: BUSBAR-1.6.0.md grants this class. What this row holds is the NUMBER." -[[dep]] -from = "busbar-voice" -to = "busbar-contract" -half = "shipped" -count = "1" -verdict = "allowed" -cite = "ARCHITECTURE_ALLOWED legacy -> contract (xtask/src/gates/kind_isolation.rs:594), as its siblings busbar-llm / busbar-a2a / busbar-mcp -> busbar-contract are; BUSBAR-1.6.0.md THE DESIGN, §9 (BUSBAR-1.6.0.md:1140-1141): busbar-contract is \"the shapes and the author SDK\"." -why = "busbar-voice names busbar-contract: declared normal in the shipped graph, 1 declaration(s) at this commit. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." -drain = "struck when P3 deletes the legacy crate busbar-voice: the edge goes with the crate, and a row naming a crate the tree no longer has is a dead allowance." - [[dep]] from = "busbar" to = "busbar-a2a" @@ -1345,16 +1302,6 @@ cite = "ARCHITECTURE_ALLOWED root -> legacy (xtask/src/gates/kind_isolation.r why = "busbar names busbar-a2a: declared dev in the test graph (features test-support), 1 declaration(s) at this commit, beside the shipped edge. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." drain = "struck when P3 deletes the legacy crate busbar-a2a: the edge goes with the crate, and a row naming a crate the tree no longer has is a dead allowance." -[[dep]] -from = "busbar" -to = "busbar-voice" -half = "test" -count = "1" -verdict = "allowed" -cite = "ARCHITECTURE_ALLOWED root -> legacy (xtask/src/gates/kind_isolation.rs:617); BUSBAR-1.6.0.md THE DESIGN, §9 (BUSBAR-1.6.0.md:1140): busbar is \"the binary and composition root\", which names every crate it links." -why = "busbar names busbar-voice: declared dev in the test graph (features test-support), 1 declaration(s) at this commit, beside the shipped edge. The count is TODAY'S MEASUREMENT, exact in both directions — above it is the landing that grew the coupling, below it is stale slack that was not drained on the commit that drained the edge." -drain = "struck when P3 deletes the legacy crate busbar-voice: the edge goes with the crate, and a row naming a crate the tree no longer has is a dead allowance." - [[dep]] from = "busbar" to = "busbar-auth-oauth" @@ -1590,8 +1537,8 @@ cite = "measured on predev f830faa691 (ARCHITECT 2026-10-07, Law 9); the drain [[law0]] crate = "busbar" axis = "plane" -count = "826" -cite = "measured on predev f830faa691 (ARCHITECT 2026-10-07, Law 9); the drain is THE DESIGN §1 / DoD item 2: core names no instance; RE-ARMED 798 -> 822 (Law 9, BUSBAR-1.6.0.md: it pins its ceiling at TODAY'S MEASURED VALUE): the row went live on predev at dc83ff1553 (#597) and measured 826 there; the 798 was f830faa691's, nine hours older than the arming, so the row armed red. 822 is predev 9c0a394771's measure, at or under the arming value; REBALANCED 822 -> 826 (Q128, PR #686 architect ruling, BUSBAR-1.6.0.md l.475-483): the 4 hits busbar-kernel × plane gave up (1172 -> 1168), carried verbatim by the cross-plane cells that moved from crates/busbar-kernel/tests/ into crates/busbar/tests/ (the `LLM`/`llm-plane` prose in auth_cross_plane.rs, alarm_silence_cross_plane.rs, key_expires_at_cross_plane.rs and residual_envelope_cross_plane.rs); the law0 total does not rise" +count = "810" +cite = "measured on predev f830faa691 (ARCHITECT 2026-10-07, Law 9); the drain is THE DESIGN §1 / DoD item 2: core names no instance; RE-ARMED 798 -> 822 (Law 9, BUSBAR-1.6.0.md: it pins its ceiling at TODAY'S MEASURED VALUE): the row went live on predev at dc83ff1553 (#597) and measured 826 there; the 798 was f830faa691's, nine hours older than the arming, so the row armed red. 822 is predev 9c0a394771's measure, at or under the arming value; REBALANCED 822 -> 826 (Q128, PR #686 architect ruling, BUSBAR-1.6.0.md l.475-483): the 4 hits busbar-kernel × plane gave up (1172 -> 1168), carried verbatim by the cross-plane cells that moved from crates/busbar-kernel/tests/ into crates/busbar/tests/ (the `LLM`/`llm-plane` prose in auth_cross_plane.rs, alarm_silence_cross_plane.rs, key_expires_at_cross_plane.rs and residual_envelope_cross_plane.rs); the law0 total does not rise; LOWERED 826 -> 810 by FLIP-STREAMING (#503), this PR's own delta re-applied over predev 872b6ae0e2's row (Law 9: the ceiling ratchets down): the flip measured 808 against predev 62fdbdf3bc's 824, 16 under it, because the deleted plane's rows leave the root's claim fixtures and its session-door cells, registry pins and test comments name no plane, while the root-crate no-silent-skip rule (#712) makes the session door's root cells a cfg naming the subject (`linked_section_streams` at three sites: serve_tests' session_door module, secret_source's session_door_cells module and its driven() roster)" [[law0]] crate = "busbar" diff --git a/qa/loc.toml b/qa/loc.toml index 9c4b4abae0..f52c2e95f0 100644 --- a/qa/loc.toml +++ b/qa/loc.toml @@ -50,8 +50,9 @@ crates = [ "busbar-plane-decisions", "busbar-plane-mcp", "busbar-plane-streaming", - "busbar-voice", ] +# `busbar-voice` HAS NO ROW ANY MORE: FLIP-STREAMING deleted the legacy streams crate once the +# streaming plane's door served every route (#18). # `busbar-voice-codec` HAS NO ROW ANY MORE (owner ruling R7, 2026-09-27, #39): it dissolved into # `busbar-plane-streaming`'s own `codec` module, a pure move — the LOC it carried is still counted, # now under the `busbar-plane-streaming` row above. diff --git a/qa/plane-hook-isomorphism.allow b/qa/plane-hook-isomorphism.allow index 2f8326c580..369049454e 100644 --- a/qa/plane-hook-isomorphism.allow +++ b/qa/plane-hook-isomorphism.allow @@ -128,70 +128,6 @@ ], "capability": "trust-pinning", "reason": "the card key-id prefix is part of A2A-only agent-card signing; the LLM plane mints no card key" - }, - { - "field": "admin_routes", - "planes_none": [ - "streaming" - ], - "capability": "catalogue", - "reason": "the voice plane's streams: is a singular section, not a named-definition map, so it has no per-registration admin surface to mount" - }, - { - "field": "openapi", - "planes_none": [ - "streaming" - ], - "capability": "catalogue", - "reason": "the voice plane contributes no plane-owned OpenAPI path document; its duplex session ingress is not described as an OpenAPI fragment" - }, - { - "field": "config_validate", - "planes_none": [ - "streaming" - ], - "capability": "catalogue", - "reason": "the voice streams: grammar is validated through parse_section/default_section, not a separate plane-owned config_validate hook" - }, - { - "field": "registry_contains", - "planes_none": [ - "streaming" - ], - "capability": "catalogue", - "reason": "the voice plane keeps no named-registration registry to answer containment on; streams: is a singular section with no named stream defs" - }, - { - "field": "reresolve_gates", - "planes_none": [ - "streaming" - ], - "capability": "trust-pinning", - "reason": "the voice plane pins no per-peer artifact (providers are operator-declared endpoints), so there are no container gates to re-resolve on a swap" - }, - { - "field": "retain_verify_gates", - "planes_none": [ - "streaming" - ], - "capability": "trust-pinning", - "reason": "the voice plane retains no verify gates because it holds no pinned peer artifacts across generations" - }, - { - "field": "card_signing_domain", - "planes_none": [ - "streaming" - ], - "capability": "trust-pinning", - "reason": "agent-card signing identity is an A2A-only capability; the voice plane issues no signed card for a peer to verify" - }, - { - "field": "card_kid_prefix", - "planes_none": [ - "streaming" - ], - "capability": "trust-pinning", - "reason": "the card key-id prefix is part of A2A-only agent-card signing; the voice plane mints no card key" } ] } diff --git a/qa/plane-purity-strict.toml b/qa/plane-purity-strict.toml index 1b6c41070f..815f762d02 100644 --- a/qa/plane-purity-strict.toml +++ b/qa/plane-purity-strict.toml @@ -45,9 +45,9 @@ BACKWARDS = 33 # this file, the scanner's own needle in `xtask/src/gates/plane_purity/scanner.rs` (the `Mode::Reverse` # arm). Until then these four figures are ceilings over a needle that cannot match, NOT a measured # agreement between two gates. +# `voice = 0` is struck: FLIP-STREAMING deleted crates/busbar-voice, and xtask::planes no longer names it. [test-reach] llm = 24 # `mcp = 1` STRUCK (P3 DEL-MCP, ARCHITECT 2026-10-05): crates/busbar-mcp is deleted and `mcp` left # the legacy-engine roster (xtask::planes::PLANE_KEYS), so its test-reach row goes with it. a2a = 3 -voice = 0 diff --git a/qa/plane-secret-blindness.toml b/qa/plane-secret-blindness.toml index a8f5e09621..0b46347a5d 100644 --- a/qa/plane-secret-blindness.toml +++ b/qa/plane-secret-blindness.toml @@ -29,8 +29,3 @@ item = "secret_resolver" rule = "plane-secret-blindness:names-a-resolver" file = "crates/busbar-a2a/src/a2a/transport.rs" item = "SecretResolve" - -[[finding]] -rule = "plane-secret-blindness:names-a-resolver" -file = "crates/busbar-voice/src/mount.rs" -item = "SecretResolve" diff --git a/qa/teller-steps.json b/qa/teller-steps.json index 6b44597bfe..5b619fbfbb 100644 --- a/qa/teller-steps.json +++ b/qa/teller-steps.json @@ -102,7 +102,7 @@ }, "root-streaming": { "plane": "streaming", - "file": "crates/busbar/src/root/tests/gauntlet_kernel.rs", + "file": "crates/busbar/src/root/tests/serve_tests.rs", "feature": "plane-streaming" }, "root-decisions": { @@ -460,8 +460,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_carries_one_unit_per_call", - "note": "on the served session leg (the kernel-loop session rider `install()` registers for the streaming plane's key): each call at the streaming door is one session open the rider carries and one report, so two calls are two units, never one" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the browser's mint is one unit the door carries end to end, from the data router's claim to the provider's answer relayed to the caller" } }, "decode": { @@ -469,10 +469,9 @@ "status": "mapped", "note": "testing/voice-conformance/legs/spec-per-dialect.sh; the per-dialect spec battery includes decode-shape checks", "root": { - "state": "proven", + "state": "none", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_refuses_a_body_that_does_not_decode", - "note": "on the served session leg: a client event the plane reads is carried to the upstream, and a frame the plane cannot read reaches the upstream on no wire at all, on a session the rider opened" + "note": "the streaming door's mint takes no caller body (the locked session is the plane's own), so no root cell on this leg can feed it a body that fails to decode" } }, "authenticate": { @@ -482,8 +481,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_attributes_the_session_to_the_resolved_key", - "note": "on the served session leg: the session answers for the key the door resolved and for nobody else -- its turns land on that key's own ledger, and a caller the door resolved no key for is attributed nothing" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the mint is admitted for the key its token resolves, and the same request with no credential is refused before any dial or charge" } }, "verify": { @@ -493,8 +492,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_opens_the_session_on_the_destination_it_judged", - "note": "on the served session leg: the destination the open-pass gate judged inside the rider is the destination the session is metered under -- every count lands under that one lane" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the mint reaches the provider through the connector at the destination its guard judged, the catalog model's provider on loopback" } }, "approve": { @@ -502,10 +501,9 @@ "status": "mapped", "note": "testing/voice-conformance/legs/session-scope.sh; K3 -- the session scope kind is a gate, not vocabulary", "root": { - "state": "proven", + "state": "none", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_refuses_a_grant_short_of_the_operation", - "note": "on the served session leg: a key valid for the door but granted no session scope is refused with the plane's own answer before the gate and opens nothing; a granted key reaches the governed open" + "note": "no streaming-door cell mints a key short of the mint's grant; the door's two root cells drive an admitted key and a spent one" } }, "admit": { @@ -515,8 +513,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_refuses_an_over_budget_caller_before_it_dials", - "note": "on the served session leg: a key whose chain already reads dry is refused at the open, past the rider's gate and before any durable row or account exists, while a key with room opens" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_spent_key_is_refused_at_the_session_door_before_any_dial", + "note": "on the streaming door's served leg: a key whose group budget is spent is refused 429 at admission, the provider never dialled and nothing charged" } }, "route": { @@ -526,8 +524,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_opens_a_session_that_relays_both_ways", - "note": "on the served session leg: the session the rider opened relays the provider's audio down to the client and the client's audio up to the provider" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the mint is routed DIRECT to the catalog model streams.session.model names, its locked session carrying that model to the provider" } }, "meter": { @@ -537,8 +535,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_meters_each_turn_per_declared_class", - "note": "on the served session leg: one turn is ledgered on the presenting key per class the plane declares, the two text halves on their own classes rather than summed (Q21b: the kernel's session account)" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the served mint is one request on the governance ledger of the key that presented it" } }, "audit": { @@ -548,8 +546,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_audits_each_call_once", - "note": "on the served session leg: a served open lands exactly one admin-audit row naming the mutation, the session, its outcome and its principal" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the served mint writes exactly one line on the node's book, the unit's one record" } }, "encode": { @@ -559,8 +557,8 @@ "root": { "state": "proven", "leg": "root-streaming", - "test": "crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_ends_each_call_once", - "note": "on the served session leg: the session ends once -- the turn still open when the carrier ends is ledgered with the session, and the durable row is settled and evicted" + "test": "crates/busbar/src/root/tests/serve_tests.rs::a_session_door_mint_is_served_under_the_providers_credential", + "note": "on the streaming door's served leg: the mint's money facts and node facts close once at its end, nothing left open on either" } } }, diff --git a/scripts/plane-config-noun-gate.sh b/scripts/plane-config-noun-gate.sh index c788d84834..2c5a8389f3 100755 --- a/scripts/plane-config-noun-gate.sh +++ b/scripts/plane-config-noun-gate.sh @@ -117,6 +117,13 @@ hdr() { printf '\n== %s ==\n' "$*"; } # reached through the surviving root rather than through a path that stopped existing. CORE_ROOTS="crates/busbar-kernel/src" +# SECTION NOUNS KEPT BY NAME (ARCHITECT 2026-10-05, Q7): `streams` is the 1.5.5 customer config key +# of the streaming plane, whose legacy crate (`busbar-voice`, the one that declared it in a PlaneDecl) +# the P3 flip deleted. The plane is a door now (`busbar-plane-streaming`) and declares no PlaneDecl, +# so the noun cannot be read off one; it is kept here by name rather than dropped from the scan. The +# floor below counts it. +NAMED_NOUNS="streams" + # Resolve the four section nouns from each plane crate's DECLARED PlaneDecl.config_section — never a # restated literal. ` -> crates/busbar-/src`; read the `config_section: "",` line. # @@ -154,6 +161,7 @@ section_nouns() { fi [ -n "$noun" ] && out="${out:+$out }$noun" done + for noun in $NAMED_NOUNS; do out="${out:+$out }$noun"; done printf '%s' "$out" } @@ -214,7 +222,7 @@ require_nouns() { # shellcheck disable=SC2086 # the split is the count; `set -f` makes it glob-safe set -f; set -- $nouns; got=$#; set +f # shellcheck disable=SC2086 # same - set -f; set -- $PLANE_KEYS; want=$#; set +f + set -f; set -- $PLANE_KEYS $NAMED_NOUNS; want=$#; set +f [ "$got" -eq "$want" ] && return 0 red "plane-config-noun gate: FAIL — resolved $got section noun(s) for $want plane key(s): [$nouns]" note "Each plane declares its own noun via PlaneDecl.config_section; a plane that resolves to nothing" diff --git a/scripts/plane-delete-test.sh b/scripts/plane-delete-test.sh index dbd660e2e9..4b679259ca 100755 --- a/scripts/plane-delete-test.sh +++ b/scripts/plane-delete-test.sh @@ -197,23 +197,23 @@ command -v cargo >/dev/null 2>&1 || { echo "plane-delete-test: cargo not found" # plane-kind name). The NEUTRAL feature set to keep ON for the neutral-crate check (every default plane # EXCEPT the one being removed) — llm has no neutral-side feature of its own, so removing it keeps both # plane-mcp and plane-a2a. A crate/feature that appears or moves is a one-line edit here. -# `voice` (busbar-voice, Plane 4) is WIRED into the bin and DEFAULT-ON, on both of its features: it has -# a `dep:busbar-voice` optional dependency and the `plane-streaming` bin feature, whose forwards to the -# plane crate are `dep:busbar-voice`, `busbar-voice?/runtime` and `busbar-voice?/openapi-schema` — all -# three stripped by neutralise_bin. Its bin_feature is `plane-streaming`; its neutral_keep is the full -# default plane set (removing voice touches neither mcp nor a2a). A feature that FORWARDS to +# The streaming plane (`plane-streaming`, crates/busbar-plane-streaming since FLIP-STREAMING deleted +# the legacy busbar-voice crate) is WIRED into the bin and DEFAULT-ON: a `dep:busbar-plane-streaming` +# optional dependency behind the `plane-streaming` bin feature, stripped by neutralise_bin. Its +# bin_feature is `plane-streaming`; its neutral_keep is the full default plane set (removing it +# touches neither mcp nor a2a). A feature that FORWARDS to # `plane-streaming` would leave the bin's default build incoherent without the crate until it came out # too — which is what neutralise_bin's forwarding closure is for. # `plane-mcp` (P3 DEL-MCP: the engine crate is deleted; the plane is its one plane-kind crate, # `crates/busbar-plane-mcp`, plane_ondisk_key) names its `dep:` in its DOOR row, `plane-mcp-door`; # `plane-mcp` forwards to that row, so the reaching closure takes both out of `default`. -bin_feature() { case "$1" in llm) echo proto-llm ;; mcp) echo plane-mcp ;; plane-mcp) echo plane-mcp-door ;; a2a) echo plane-a2a ;; voice) echo plane-streaming ;; plane-decisions) echo plane-decisions ;; esac; } +bin_feature() { case "$1" in llm) echo proto-llm ;; mcp) echo plane-mcp ;; plane-mcp) echo plane-mcp-door ;; a2a) echo plane-a2a ;; plane-streaming) echo plane-streaming ;; plane-decisions) echo plane-decisions ;; esac; } neutral_keep() { case "$1" in llm) echo "plane-mcp,plane-a2a" ;; mcp | plane-mcp) echo "plane-a2a" ;; a2a) echo "plane-mcp" ;; - voice) echo "plane-mcp,plane-a2a" ;; + plane-streaming) echo "plane-mcp,plane-a2a" ;; plane-decisions) echo "plane-mcp,plane-a2a" ;; esac } @@ -624,14 +624,14 @@ plane_probe_path() { llm) echo "/v1/chat/completions" ;; mcp | plane-mcp) echo "/.well-known/oauth-protected-resource/mcp" ;; a2a) echo "/a2a" ;; - voice) echo "/v1/realtime/client_secrets" ;; + plane-streaming) echo "/v1/realtime/client_secrets" ;; plane-decisions) echo "/v1/systemone" ;; esac } plane_probe_body() { case "$1" in a2a) printf '{"jsonrpc":"2.0","method":"message/send","id":1}' ;; - voice) printf '{"model":"gpt-realtime"}' ;; + plane-streaming) printf '{"model":"gpt-realtime"}' ;; plane-decisions) printf '{"state":{},"context":{}}' ;; llm | mcp | plane-mcp) printf '' ;; esac @@ -641,7 +641,7 @@ plane_probe_body() { # the SUBJECT boot must not carry. Space-separated; empty for a plane the fixture does not configure. # This is the same fail-closed pairing the product enforces at resolve: the section exists only while # the plane that owns it is compiled in. -plane_config_sections() { case "$1" in mcp | plane-mcp) echo "mcp" ;; a2a) echo "agents" ;; voice) echo "streams" ;; plane-decisions) echo "decisions" ;; *) echo "" ;; esac; } +plane_config_sections() { case "$1" in mcp | plane-mcp) echo "mcp" ;; a2a) echo "agents" ;; plane-streaming) echo "streams" ;; plane-decisions) echo "decisions" ;; *) echo "" ;; esac; } # section_omitted
→ 0 when
is in the space-separated . section_omitted() { case " ${2:-} " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } diff --git a/scripts/plane-grep-gate.sh b/scripts/plane-grep-gate.sh index d2b539730f..693200ce72 100755 --- a/scripts/plane-grep-gate.sh +++ b/scripts/plane-grep-gate.sh @@ -110,7 +110,9 @@ DIALECTS="openai gemini anthropic bedrock cohere responses" # below and nothing else; CI never sets it. NEUTRAL_ROOTS="${PLANE_GREP_NEUTRAL_ROOTS:-$(neutral_src_roots)}" # `$PLANE_KEYS_KIND_ONLY` (mcp since P3 DEL-MCP) is still a plane noun the neutral crates must not name. -NEUTRAL_NEEDLES="$DIALECTS $PLANE_KEYS_PROTOCOL $PLANE_KEYS_KIND_ONLY" +# `voice` stays a NEUTRAL needle by name: FLIP-STREAMING took it off PLANE_KEYS with crates/busbar-voice, +# and the streaming plane's dialect word must still never reach the neutral side. +NEUTRAL_NEEDLES="$DIALECTS $PLANE_KEYS_PROTOCOL $PLANE_KEYS_KIND_ONLY voice" # TWO ROOTS PER PLANE since the codec split: each protocol plugin kept its I/O half under the # historical crate name and shed its pure half into a `-codec` crate a PURE kind may name. The gate # scans sources, not manifests, so both halves are named or the moved files stop being scanned — @@ -129,10 +131,9 @@ NEUTRAL_NEEDLES="$DIALECTS $PLANE_KEYS_PROTOCOL $PLANE_KEYS_KIND_ONLY" # which this needle gate does not read — a plane-kind crate is held to the plugin-kind rules instead. A2A_ROOT="crates/busbar-a2a/src" A2A_NEEDLES="$DIALECTS $(plane_noun_keys_other a2a)" -# VOICE IS ONE ROOT NOW TOO, same reason (owner ruling R7, 2026-09-27, `BUSBAR-1.6.0.md` THE DESIGN, §9/#39): -# `busbar-voice-codec` dissolved into `busbar-plane-streaming`'s own `codec` module, a plane-kind -# crate this needle gate does not read. -VOICE_ROOT="crates/busbar-voice/src" +# VOICE HAS NO ROOT: FLIP-STREAMING deleted crates/busbar-voice, and the streaming plane is +# `busbar-plane-streaming`, a plane-kind crate this needle gate does not read. VOICE_NEEDLES remains +# for the self-test's symmetric case, which plants its own file. VOICE_NEEDLES="$DIALECTS $(plane_noun_keys_other voice)" # The neutral Operation enum — generic op vocabulary, explicitly in-scope-neutral. Excluded whole. @@ -524,31 +525,26 @@ run_report() { require_roots neutral $NEUTRAL_ROOTS # shellcheck disable=SC2086 require_roots a2a $A2A_ROOT - # shellcheck disable=SC2086 - require_roots voice $VOICE_ROOT local tmp; tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' RETURN : >"$tmp/hits" # Prepass: compute the test-support module subtrees to drop (across every scanned root). - compute_mod_excludes $NEUTRAL_ROOTS $A2A_ROOT $VOICE_ROOT + compute_mod_excludes $NEUTRAL_ROOTS $A2A_ROOT - local nf af vf - nf="$(prod_files $NEUTRAL_ROOTS)"; af="$(prod_files $A2A_ROOT)"; vf="$(prod_files $VOICE_ROOT)" - local n_nf n_af n_vf + local nf af + nf="$(prod_files $NEUTRAL_ROOTS)"; af="$(prod_files $A2A_ROOT)" + local n_nf n_af n_nf="$(count_files "$nf")" - n_af="$(count_files "$af")"; n_vf="$(count_files "$vf")" + n_af="$(count_files "$af")" require_files neutral "$n_nf" require_files a2a "$n_af" - require_files voice "$n_vf" : >"$tmp/allowused" # shellcheck disable=SC2086 scan "$NEUTRAL_NEEDLES" "$tmp/allowused" $nf >>"$tmp/hits" # shellcheck disable=SC2086 scan "$A2A_NEEDLES" "$tmp/allowused" $af >>"$tmp/hits" - # shellcheck disable=SC2086 - scan "$VOICE_NEEDLES" "$tmp/allowused" $vf >>"$tmp/hits" # ── THE DEAD-ROW CHECK — an allowlist entry that suppresses nothing is reported, never carried ──── # Every row above exists to say "this specific hit is frozen contract, not dialect leakage". A row @@ -585,7 +581,6 @@ EOF hdr "PLANE-GREP report — dialect-name SUBSTRINGS outside busbar-llm (production .rs, comments/tests/Operation excluded)" note "neutral roots: $NEUTRAL_ROOTS ($n_nf file(s); bans: $NEUTRAL_NEEDLES)" note "a2a root: $A2A_ROOT ($n_af file(s); bans: $A2A_NEEDLES)" - note "voice root: $VOICE_ROOT ($n_vf file(s); bans: $VOICE_NEEDLES)" note "excluded: $OPERATION_EXCLUDE (neutral Operation enum), */tests/*, *_test(s).rs, #[cfg(test)]" hdr "by needle (a clean tree reports zero)" diff --git a/scripts/plane-keys.sh b/scripts/plane-keys.sh index b23da7c73b..bbcfa1626c 100644 --- a/scripts/plane-keys.sh +++ b/scripts/plane-keys.sh @@ -45,6 +45,8 @@ # for their crate/wiring counterpart to land; until then, PLANE_KEYS states what is TRUE ON DISK, and # PLANE_KEYS_LOCKED (below) states what is true IN DOCTRINE, so no caller can mistake one for the # other by reading only this line. +# `voice` is struck: FLIP-STREAMING deleted crates/busbar-voice; the streaming plane is +# crates/busbar-plane-streaming (plane_ondisk_key below), scanned by the plane-kind regime. # # `mcp` IS STRUCK (P3 DEL-MCP, ARCHITECT 2026-10-05) — the named change this header asks for. Every # consumer reads a key here as a literal `crates/busbar-` LEGACY-ENGINE directory, and @@ -52,13 +54,14 @@ # `crates/busbar-plane-mcp`, which the plane-kind regime scans whole. It stays on the doctrine roster # (`PLANE_KEYS_LOCKED`) and reaches the deletion harness as `plane-mcp` (`plane_ondisk_key`), the way # `decisions` does as `plane-decisions`. -PLANE_KEYS="llm a2a voice" +PLANE_KEYS="llm a2a" # PLANE_KEYS_KIND_ONLY — plane keys whose plane has NO legacy-engine crate any more and lives only in -# its plane-kind crate `crates/busbar-plane-` (P3 DEL-MCP, ARCHITECT 2026-10-05: `mcp`). Never a +# its plane-kind crate `crates/busbar-plane-` (P3 DEL-MCP, ARCHITECT 2026-10-05: `mcp`; +# FLIP-STREAMING: `streaming`, whose legacy crate was `crates/busbar-voice`). Never a # `crates/busbar-` root; STILL a plane noun, so a needle gate that bans the other planes' keys # keeps banning it (`plane_noun_keys_other`). Leaving PLANE_KEYS must not let the word in. -PLANE_KEYS_KIND_ONLY="mcp" +PLANE_KEYS_KIND_ONLY="mcp streaming" # The protocol subset: every plane key except `llm`. Derived from PLANE_KEYS so adding a plane in # one place flows here automatically. @@ -90,7 +93,7 @@ PLANE_KEYS_LOCKED="llm mcp a2a streaming decisions" # entry here returns empty: a caller must report that plane as a NAMED GAP, never as a silent pass. plane_ondisk_key() { case "$1" in - streaming) printf 'voice' ;; + streaming) printf 'plane-streaming' ;; decisions) printf 'plane-decisions' ;; # P3 DEL-MCP: the engine is deleted; the plane is its one plane-kind crate. mcp) printf 'plane-mcp' ;; diff --git a/scripts/verify-1.6.0-done.sh b/scripts/verify-1.6.0-done.sh index 9ac63f162d..325adfc71f 100755 --- a/scripts/verify-1.6.0-done.sh +++ b/scripts/verify-1.6.0-done.sh @@ -21,7 +21,7 @@ # every filtered step declares its test count so a filter that selects nothing is # RED rather than vacuously green (see filtered_cargo_test). # config-stability cargo xtask gate config-schema (config-schema.snapshot.json byte-stable). -# test cargo test --workspace + cargo test -p busbar-voice --features runtime. +# test cargo test --workspace. # conformance the conformance rigs' selftests + the voice legs =ready. # teller-steps the H2 matrix holds on BOTH its columns: every rig cell id still resolves to the # scenario/script/leg/suite that owns it, and the rigs behind them RUN and pass @@ -1299,7 +1299,6 @@ end_group # ───────────────────────────────────────────────────────────────────────────────────────────────── begin_group "TEST — full workspace + voice runtime" step "cargo test --workspace" cargo test --workspace --quiet -step "cargo test -p busbar-voice --features runtime" cargo test -p busbar-voice --features runtime --quiet end_group # ───────────────────────────────────────────────────────────────────────────────────────────────── diff --git a/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__mint__client-secrets.json b/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__mint__client-secrets.json index 1b962292c1..1850ea7489 100644 --- a/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__mint__client-secrets.json +++ b/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__mint__client-secrets.json @@ -1 +1 @@ -{"applied":["capture"],"body":{"json":{"steps":[{"body":{"json":{"expires_at_unix":1767225600,"value":"ek_oracle_0001"}},"header_order":["content-type","content-length","connection","date"],"headers":{"connection":"close","content-length":"55","content-type":"application/json"},"request":"POST /v1/realtime/client_secrets","status":200,"status_line":"HTTP/1.1 200 OK"}],"upstream":[{"answered":200,"body":"{\"expires_after\":{\"anchor\":\"created_at\",\"seconds\":600},\"session\":{\"model\":\"m-cap\"}}","headers":[["content-type","application/json"],["authorization","Bearer oracle-cred-streams-0002"],["openai-safety-identifier",""],["host","127.0.0.1:"],["content-length","83"]],"method":"POST","path":"/v1/realtime/client_secrets","proto":"h1","raw":"POST /v1/realtime/client_secrets HTTP/1.1\r\ncontent-type: application/json\r\nauthorization: Bearer oracle-cred-streams-0002\r\nopenai-safety-identifier: \r\nhost: 127.0.0.1:\r\ncontent-length: 83\r\n\r\n{\"expires_after\":{\"anchor\":\"created_at\",\"seconds\":600},\"session\":{\"model\":\"m-cap\"}}","role":"fixed"}]}},"effects":{},"headers":{},"status":0} +{"applied":["capture"],"body":{"json":{"steps":[{"body":{"json":{"expires_at_unix":1767225600,"value":"ek_oracle_0001"}},"header_order":["content-type","content-length","connection","date"],"headers":{"connection":"close","content-length":"55","content-type":"application/json"},"request":"POST /v1/realtime/client_secrets","status":200,"status_line":"HTTP/1.1 200 OK"}],"upstream":[{"answered":200,"body":"{\"expires_after\":{\"anchor\":\"created_at\",\"seconds\":600},\"session\":{\"model\":\"m-cap\"}}","headers":[["authorization","Bearer oracle-cred-streams-0002"],["content-type","application/json"],["openai-safety-identifier",""],["host","127.0.0.1:"],["content-length","83"]],"method":"POST","path":"/v1/realtime/client_secrets","proto":"h1","raw":"POST /v1/realtime/client_secrets HTTP/1.1\r\nauthorization: Bearer oracle-cred-streams-0002\r\ncontent-type: application/json\r\nopenai-safety-identifier: \r\nhost: 127.0.0.1:\r\ncontent-length: 83\r\n\r\n{\"expires_after\":{\"anchor\":\"created_at\",\"seconds\":600},\"session\":{\"model\":\"m-cap\"}}","role":"fixed"}]}},"effects":{},"headers":{},"status":0} diff --git a/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__sdp__calls.json b/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__sdp__calls.json index 252c307d4b..6d16387650 100644 --- a/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__sdp__calls.json +++ b/testing/shadow-oracle/golden/1.6.0-pre/cells/streams__sdp__calls.json @@ -1 +1 @@ -{"applied":["capture"],"body":{"json":{"steps":[{"body":{"text":"v=0\r\no=- 2 2 IN IP4 127.0.0.1\r\ns=oracle-answer\r\nt=0 0\r\n"},"header_order":["content-type","content-length","connection","date"],"headers":{"connection":"close","content-length":"55","content-type":"application/sdp"},"request":"POST /v1/realtime/calls","status":201,"status_line":"HTTP/1.1 201 Created"}],"upstream":[{"answered":201,"body":"v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=oracle-offer\r\nt=0 0\r\n","headers":[["content-type","application/sdp"],["authorization","Bearer oracle-cred-streams-0006"],["host","127.0.0.1:"],["content-length","54"]],"method":"POST","path":"/v1/realtime/calls","proto":"h1","raw":"POST /v1/realtime/calls HTTP/1.1\r\ncontent-type: application/sdp\r\nauthorization: Bearer oracle-cred-streams-0006\r\nhost: 127.0.0.1:\r\ncontent-length: 54\r\n\r\nv=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=oracle-offer\r\nt=0 0\r\n","role":"fixed"}]}},"effects":{},"headers":{},"status":0} +{"applied":["capture"],"body":{"json":{"steps":[{"body":{"text":"v=0\r\no=- 2 2 IN IP4 127.0.0.1\r\ns=oracle-answer\r\nt=0 0\r\n"},"header_order":["content-type","content-length","connection","date"],"headers":{"connection":"close","content-length":"55","content-type":"application/sdp"},"request":"POST /v1/realtime/calls","status":201,"status_line":"HTTP/1.1 201 Created"}],"upstream":[{"answered":201,"body":"v=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=oracle-offer\r\nt=0 0\r\n","headers":[["authorization","Bearer oracle-cred-streams-0006"],["content-type","application/sdp"],["host","127.0.0.1:"],["content-length","54"]],"method":"POST","path":"/v1/realtime/calls","proto":"h1","raw":"POST /v1/realtime/calls HTTP/1.1\r\nauthorization: Bearer oracle-cred-streams-0006\r\ncontent-type: application/sdp\r\nhost: 127.0.0.1:\r\ncontent-length: 54\r\n\r\nv=0\r\no=- 1 1 IN IP4 127.0.0.1\r\ns=oracle-offer\r\nt=0 0\r\n","role":"fixed"}]}},"effects":{},"headers":{},"status":0} diff --git a/testing/shadow-oracle/oracle-rust.pin b/testing/shadow-oracle/oracle-rust.pin index 7ab3540d6e..591fbfdfa7 100644 --- a/testing/shadow-oracle/oracle-rust.pin +++ b/testing/shadow-oracle/oracle-rust.pin @@ -285,6 +285,19 @@ # earlier pin (predev's 4a7a7bba2cef, oracle-strict's edd17921e320, #157's 0ede76aea912, #493's 5b89bdf67326), so # no lane's engine moves backwards when this lands. # +# 2026-10-06 (busbar P3 FLIP-STREAMING / DEL-VOICE): moved 4a7a7bba2cef -> 30d072960aa1 (busbar-release +# `p3-flip-streaming-plan`, PR #168 into dev, cut from 4a7a7bba2cef). One commit, no engine change: the +# build-test plan's `voice runtime` row leaves with the deleted busbar-voice crate (plan-lint refuses a row +# naming a package the tree lacks). The judge's code is 4a7a7bba2cef's byte for byte. promote.yml +# RELEASE_REF moves to the same sha in the same commit. Feature branch head; re-pin to the merge commit +# when it lands. +# +# 2026-10-06 (ARCHITECT RELEASE_REF rule: the pin is always a busbar-release DEV commit that contains +# every earlier pin): moved 30d072960aa1 -> 1869c0032ecf, the merge of busbar-release #168 into dev +# (#168 = the voice-runtime row strike above, merged with dev 5b89bdf67326 = #166). It contains +# 4a7a7bba2cef, 0ede76aea9 (#157) and 5b89bdf67326 (#166). promote.yml RELEASE_REF moves to the same +# sha in the same commit. +# # 2026-10-07 (ARCHITECT ruling on #536, option b; HEADROOM-REMOVE precedent Q-SO2/Q-SO3): moved 1869c0032ecf -> # 4d52f387c4d1 (busbar-release dev, the merge of #177; its tree is #177's head 0ff118af56f5 byte for byte; it descends # 1869c0032ecf, 2119a67a6f and 4a7a7bba2c). diff --git a/testing/shadow-oracle/rigs-ledger.sh b/testing/shadow-oracle/rigs-ledger.sh index 570f1aed29..ec4519d517 100755 --- a/testing/shadow-oracle/rigs-ledger.sh +++ b/testing/shadow-oracle/rigs-ledger.sh @@ -89,7 +89,7 @@ # # ARMING # MCP_SUBJECT_BUSBAR_BIN / A2A_SUBJECT_BUSBAR_BIN are set from --bin for the MCP and A2A legs. The -# voice battery arms itself (it drives its OWN `voice-conform` harness binary built from this tree, +# voice battery arms itself (it drives its OWN `voice_conform` harness (testing/voice-conformance/harness) from this tree, # never busbar's server binary — see testing/voice-conformance/lib/conform-bin.sh), so --bin plays # no part in that leg; it is still required, so a run cannot silently skip the two legs that do need # it by never providing one. @@ -660,7 +660,7 @@ run_voice() { say "== rig: voice conformance battery ==" local log="${WORK}/voice-results.tsv" : >"$log" - # No busbar binary is threaded through: the voice legs drive their OWN `voice-conform` harness + # No busbar binary is threaded through: the voice legs drive their OWN `voice_conform` harness # binary built from this tree (testing/voice-conformance/lib/conform-bin.sh), never a server # busbar binary. --bin is still required at the top of this script so a caller cannot silently # skip the two legs that DO need it by never supplying one. diff --git a/testing/voice-conformance/README.md b/testing/voice-conformance/README.md index 43be0545e9..85c461b212 100644 --- a/testing/voice-conformance/README.md +++ b/testing/voice-conformance/README.md @@ -1,130 +1,93 @@ -# Voice (4th-plane) conformance battery — SCAFFOLD - -A conformance battery for busbar's **voice** plane, at structural parity with the -sibling MCP (`testing/mcp-conformance/`) and A2A (`testing/a2a-*`) batteries. - -**This is a scaffold.** The voice runtime does not exist yet, so no leg asserts -real conformance today. What lives here is the **shape** — the runner, the leg -scaffolds, the verdict emitter, the CI workflow — landed **green** in an honest -"scaffolded, legs pending" state, so that: - -1. the shape is enforced (and its accounting proven) *before* the first real leg - depends on it, and -2. filling a leg later is a **drop-in**, not a rebuild. - -## Why a scaffold is a legitimate green here - -The MCP and A2A batteries make `NOT ARMED, SO NOT RUN` a **RED** state, because -for those planes the runtime exists and a disarmed subject leg renders as the -identical green tick a leg that judged busbar and passed would produce — the -false green those batteries exist to refuse. - -Voice has **nothing to arm against yet**. So the honest report is **PENDING** — -stated per leg, and never dressed up as a conformance pass. Following the MCP -workflow's own guidance, the transition from PENDING to a real armed-or-red leg -is **exercised by `voice-conformance.sh --selftest`** rather than asserted by a -real run that cannot happen. The self-test proves that the moment a leg is marked -`ready` it is held to the same anti-vacuity discipline the other batteries use: a -ready leg that executed nothing is **RED**, not green. - -## The two-leg rule (inherited, enforced the day the legs light up) - -- **CONTROL runs ALWAYS.** A battery that cannot judge a known-good third-party - dialect peer cannot be trusted to judge busbar. (Scaffolded today.) -- **SUBJECT is ARMED OR RED.** Once a leg is `ready`, an armed run that executed - nothing is RED. `--selftest` drives that transition in both directions so the - rule is one somebody has watched work. - -## The legs - -Legs are **discovered from `legs/*.sh`, never enumerated** — the same rule -`testing/verdict-covers-every-leg.py` applies to the workflow one level out. Each -`legs/.sh` declares `LEG_KIND`, `LEG_STATUS`, `LEG_SLICES` and a -`leg_execute` function. - -| leg | kind | slices | what it will judge | -|---|---|---|---| -| `spec-per-dialect` | conformance | `openai`, `gemini` | the voice spec battery, run once **per dialect** (the matrix) | -| `replay` | conformance | `default` | captured-transcript replay: a recorded session must re-derive identically | -| `cross-parity` | conformance | `oo og go gg` | the **4 ordered** OpenAI⟷Gemini pairs must agree where the mapping says they must | -| `governance` | governance | 5 checkpoints | the 5 vision checkpoints — **NOT a conformance result** | - -### The dialect matrix - -`spec-per-dialect` runs once per dialect in `{openai, gemini}`. In CI it is a job -matrix; in the runner it is the leg's `LEG_SLICES`. `cross-parity` drives all -four **ordered** pairs — `oo`, `og`, `go`, `gg` — because a mapping that is not -identity *within* a dialect (`oo`, `gg`) is already broken and only running the -cross pairs would never see it. +# Voice (streaming-plane) conformance battery + +The conformance battery for busbar's **streaming** plane (voice is one dialect inside it, #18), at +structural parity with the sibling MCP (`testing/mcp-conformance/`) and A2A (`testing/a2a-*`) +batteries. It is the streaming plane's conformance MUST-set (DEV-GREEN exit #4). + +## What a leg drives: the plane's DOOR, both ways + +The plane is served only through its memory-ABI door (`busbar_plane_streaming::door`). Every leg +shells out to one dev-only harness, the `voice_conform` binary of the testkit crate +`voice-conformance-harness` (`testing/voice-conformance/harness/`), which loads the door through +`busbar-plugin-loader` **both ways** (ARCHITECT Q6): + +- **linked** — `busbar_plane_streaming::door::door`, through `load_linked`; +- **dropped** — the `streaming_door` example `cdylib`, dlopened through `load_dropped` against the + linked row's own Statement. + +Each assertion drives a fresh instance of each door with one script and is PASS only when + +1. the two doors answered **identically** (a dropped door that is not built or does not load is a + FAIL, never a skip), +2. the answer passes the leg's check, and +3. **the RED arm bit**: the leg's planted wrong answer (the wrong door, restated as the answer it + would give) is refused by the same check. `VOICE_CONFORM_RED=1` makes the planted answers the + subject; every conformance leg must then go RED. + +`lib/conform-bin.sh` builds the pair once (`cargo build -p voice-conformance-harness --bin +voice_conform` and `cargo build -p busbar-plane-streaming --example streaming_door`); +`VOICE_CONFORM_BIN` names a prebuilt harness (checked for staleness against the plane, loader, +contract and harness sources), `VOICE_CONFORM_DOOR` a dropped door that is not in the profile's +`examples/` beside it. + +## The legs — each judges the DOOR'S half of its seam + +Legs are **discovered from `legs/*.sh`, never enumerated**. Each leg's header names what it judges at +the door and which half of the seam is the kernel's (the plane driver's walk, the identity crate, the +money steps, the transport), which a plane cannot see and the harness therefore cannot judge. + +| leg | slices | what it judges at the door | +|---|---|---| +| `spec-per-dialect` | `openai`, `gemini` | every fixture: codec round trip, and the door's relay of it by the session rules | +| `replay` | `default` | each captured transcript through one session: codec skeleton; the door's relay and skeleton | +| `cross-parity` | `oo og go gg` | the codec bridge per the cross-dialect map; the bridged wire through the destination door | +| `provider-credential` | 1 | every far request rides a declared outbound need; none carries a credential | +| `metering-lease` | 1 | a session's units, reported per declared class, once per turn; the fee once answered | +| `session-scope` | 1 | the `session` grant kind; every door asks for a principal; 404 off the guest list | +| `gemini-live-route` | 1 | the Gemini Live door: claim, arrival, and the handshake across it | +| `provider-dial` | 1 | a session's far frames ride the dialect's socket need; the host connector dials | +| `admit-refusal` | 1 | a refusal renders in the dialect's shape and opens nothing, dials nothing | +| `route-failover` | 1 | each ATTEMPT answered afresh on the pass need; nothing reaches the caller early | +| `audit-record` | 1 | the audit kind and operation the kernel's one row is written under; no door row | +| `exit-terminal` | 1 | one session, one end; an ended stream is refused on every side | +| `tool-reply` | 1 | a tool call is relayed to the caller and never answered by busbar (Law 11) | +| `governance` | 5 checkpoints | the 5 vision checkpoints — **NOT a conformance result** | + +**The session rules** the fixture legs hold the door to (`session_pump`): a caller frame reaches the +far end as sent, except that the caller's session configuration is replaced by the locked one; a +far-end frame reaches the caller as sent, except that usage and rate-limit reports are consumed (usage +becomes reported units), a far-end tool result is never relayed, and a barge-in also cancels and +truncates the far end's response at the audio the caller heard. The expectation is computed from the +wire by the codec, never by the door. ### Governance is not a conformance result -The `governance` leg observes voice **product policy**, not the voice -**protocol** — barge-in preemption, turn-budget enforcement, metering-lease -settlement (`cost_reserve`/`cost_settle`), dialect down-scope, and the **D2 -hard-close-on-exhaustion** checkpoint. Exactly as `testing/a2a-governance/` can -never contribute to the A2A verdict, this leg's findings are **observations**: -the runner enforces the separation in code, and `--selftest` proves a governance -FAIL cannot move the conformance verdict. - -## How a leg gets filled (the drop-in) - -Edit `legs/.sh`: - -1. flip `LEG_STATUS=pending` → `LEG_STATUS=ready`; -2. implement `leg_execute ` to print **one `RESULT - ` line per assertion**. - -Nothing in `voice-conformance.sh`, in the verdict emitter, or in -`.github/workflows/voice-conformance.yml` changes. The runner immediately holds -the now-ready leg to the anti-vacuity rule: a `ready` leg that emits no `RESULT` -line for a slice is **RED**. - -The inputs later legs consume are authored by another agent and only -**referenced** here: - -- `testing/voice-conformance/fixtures/{openai,gemini}/` — captured transcripts - and per-dialect spec fixtures; -- `qa/evidence/voice-cross-dialect-mapping.*` — the OpenAI⟷Gemini equivalence - the `cross-parity` leg is judged against. +The `governance` leg observes product policy, not protocol. Exactly as `testing/a2a-governance/` can +never contribute to the A2A verdict, the runner keeps a governance FAIL out of the conformance tally, +and `--selftest` proves it. ## The verdict emitter -The runner's verdict **mirrors `verdict-covers-every-leg.py`** one level in: it -holds the set of legs it *reported on* to equality with the set *discovered*, so -a leg cannot be added to the tree and then silently dropped from the verdict. It -also enforces a **floor** on the leg count (a gutted battery satisfies every -equality) and keeps governance out of the conformance tally. `--selftest` injects -each of those faults through the real emitter and watches the check bite. +The runner holds the set of legs it **reported on** to equality with the set **discovered**, enforces +a floor on the leg count, keeps governance out of the conformance tally, and makes a `ready` leg that +emitted no `RESULT` line RED. `--selftest` injects each of those faults through the real emitter and +watches the check bite, then accounts the shipped legs (which runs them: it builds the harness). ## Usage ```bash -# prove the scaffold's own accounting bites, then exit 0 bash testing/voice-conformance/voice-conformance.sh --selftest - -# run everything and emit the honest verdict (the default) -bash testing/voice-conformance/voice-conformance.sh --verdict - -# one leg (what the workflow's per-leg jobs run) +bash testing/voice-conformance/voice-conformance.sh --verdict # the default bash testing/voice-conformance/voice-conformance.sh --leg spec-per-dialect --slice openai -bash testing/voice-conformance/voice-conformance.sh --leg governance - -# what is declared +VOICE_CONFORM_RED=1 bash testing/voice-conformance/voice-conformance.sh --verdict # must be RED bash testing/voice-conformance/voice-conformance.sh --list ``` ## Layout ``` -voice-conformance.sh the runner: --selftest | --verdict | --leg | --list -legs/spec-per-dialect.sh per-dialect spec leg (matrix: openai, gemini) -legs/replay.sh captured-transcript replay -legs/cross-parity.sh the 4 ordered OpenAI<->Gemini pairs -legs/governance.sh the 5 vision checkpoints (NOT a conformance result) -fixtures/{openai,gemini}/ INPUTS authored elsewhere; referenced, not created here +voice-conformance.sh the runner: --selftest | --verdict | --leg | --list +lib/conform-bin.sh builds/locates the voice_conform harness and the dropped-in door +legs/*.sh one leg each (LEG_KIND, LEG_STATUS, LEG_SLICES, leg_execute) +fixtures/{openai,gemini}/ captured transcripts and per-dialect spec fixtures ``` - -The CI workflow is `.github/workflows/voice-conformance.yml`: `gate-selftest` -proves the accounting and the coverage lint bite, one job per leg runs the -scaffolded leg green, and `verdict` asserts every leg executed. diff --git a/testing/voice-conformance/harness/Cargo.toml b/testing/voice-conformance/harness/Cargo.toml new file mode 100644 index 0000000000..188b523c05 --- /dev/null +++ b/testing/voice-conformance/harness/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "voice-conformance-harness" +version = "0.0.0" +publish = false +edition = "2021" +license = "Apache-2.0" +rust-version = "1.97" +description = "The voice (streaming-plane) conformance harness: drives busbar-plane-streaming's door both ways, linked and dropped in, through the plugin loader." + +# TESTKIT, NOT PRODUCT PATH (ARCHITECT Q6: the streaming plane's conformance MUST-set, built against +# `busbar-plane-streaming`, linked and dlopened, RED arm kept). `testing/voice-conformance/`'s legs +# shell out to this binary. It is not shipped and not built by `cargo build --bin busbar`: it is the +# same kind of member as `testing/ws-conformance/subject`, an input to a conformance leg. It lives +# outside the plane crate so the plane's only user of the loader is its own both-ways witness +# (`tests/conformance.rs`), the one dev-edge on the loader a plugin crate is granted. +[[bin]] +name = "voice_conform" +path = "src/main.rs" + +[dependencies] +busbar-contract = { path = "../../../crates/busbar-contract" } +# THE PLANE UNDER TEST: its linked door (`busbar_plane_streaming::door::door`) and its codecs, which +# compute every expectation from the wire. The dropped-in door is the plane's own `streaming_door` +# example cdylib. +busbar-plane-streaming = { path = "../../../crates/busbar-plane-streaming" } +# THE ONE LOADER both doors are loaded through (`load_linked`, `load_dropped`). +busbar-plugin-loader = { path = "../../../crates/plugin-loader" } +serde_json = { workspace = true } +bytes = { workspace = true } diff --git a/testing/voice-conformance/harness/src/codec_legs.rs b/testing/voice-conformance/harness/src/codec_legs.rs new file mode 100644 index 0000000000..a6cf4cbd16 --- /dev/null +++ b/testing/voice-conformance/harness/src/codec_legs.rs @@ -0,0 +1,1292 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE FIXTURE LEGS — `spec-per-dialect`, `replay`, `cross-parity` — over the plane's own codecs +//! (`busbar_plane_streaming::codec`, the module the door's sessions run) AND through the door's +//! session pieces. +//! +//! Each fixture is judged twice. The CODEC half is the old leg, unchanged: wire → IR → wire → IR +//! must be stable (spec), a captured transcript must re-derive its concept skeleton (replay), a +//! concept bridged A → IR → B → IR must keep its load-bearing fields (cross). The DOOR half pushes +//! the same wire bytes (for cross: the bridged B wire) through a live session on the door that +//! speaks the dialect — the caller's frames as caller pieces, the far end's as far-end pieces — on +//! the linked and the dropped door, and holds what the door relays to the SESSION RULES the plane +//! states (`session_pump`): a caller frame reaches the far end as it arrived, except that the +//! caller's session configuration is replaced by the locked one; a far-end frame reaches the caller +//! as it arrived, except that usage and rate-limit reports are consumed (usage becomes reported +//! units), a far-end tool RESULT is never relayed, and a barge-in also cancels and truncates the +//! far end's response at the audio the caller heard. Compared on the correlation fingerprint, the +//! same rule the codec half uses. The expectation is computed from the wire by the codec, never by +//! the door. + +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use busbar_contract::abi::mechanism::call::Outcome; +use busbar_contract::abi::plane::{FROM_CALLER, FROM_FAR_END, UNITS_REPORTED}; +use busbar_plane_streaming::codec::ir::{ + DecodeState, DuplexReader, DuplexWriter, GeminiLiveCodec, IrClientEvent, IrDuplexControl, + IrDuplexTool, IrServerEvent, OpenAiRealtimeCodec, SessionConfig, WireEvent, +}; +use busbar_plane_streaming::door::{read_settings, RIDES_LIVE_SOCKET, RIDES_REALTIME_SOCKET}; +use busbar_plane_streaming::driven::{GEMINI_SOCKET_PATH, REALTIME_SOCKET_PATH}; +use busbar_plane_streaming::session_door::PER_SESSION_CLASS; +use busbar_plugin_loader::dispatch::kinds::plane::Plane; +use busbar_plugin_loader::dispatch::Plugin; +use bytes::Bytes; +use serde_json::Value; + +use crate::door::{judge, open, result, Log, Rig, Session, PUBLIC, SESSION}; + +// ── the two dialects ──────────────────────────────────────────────────────────────────────────── + +/// A duplex dialect the plane dials, and the session door that speaks it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Dialect { + /// OpenAI Realtime: the browser's sideband socket (claim 2). + OpenAi, + /// Gemini Live: the Gemini socket (claim 3). + Gemini, +} + +impl Dialect { + /// The dialect a leg names. + pub fn of(name: &str) -> Option { + match name { + "openai" => Some(Dialect::OpenAi), + "gemini" => Some(Dialect::Gemini), + _ => None, + } + } + + /// Its name in the legs. + pub const fn name(self) -> &'static str { + match self { + Dialect::OpenAi => "openai", + Dialect::Gemini => "gemini", + } + } + + /// The door claim that opens a session in it. + pub const fn claim(self) -> u32 { + match self { + Dialect::OpenAi => 2, + Dialect::Gemini => 3, + } + } + + /// The declared need its far frames ride. + pub const fn ride(self) -> u32 { + match self { + Dialect::OpenAi => RIDES_REALTIME_SOCKET, + Dialect::Gemini => RIDES_LIVE_SOCKET, + } + } + + /// The far end's socket, under the member's base URL. + pub const fn socket(self) -> &'static str { + match self { + Dialect::OpenAi => REALTIME_SOCKET_PATH, + Dialect::Gemini => GEMINI_SOCKET_PATH, + } + } + + /// Read a caller frame. + pub fn read_up(self, frame: &[u8], st: &mut DecodeState) -> Vec { + let w = WireEvent(Bytes::copy_from_slice(frame)); + match self { + Dialect::OpenAi => OpenAiRealtimeCodec.read_up(w, st), + Dialect::Gemini => GeminiLiveCodec.read_up(w, st), + } + } + + /// Read a far-end frame. + pub fn read_down(self, frame: &[u8], st: &mut DecodeState) -> Vec { + let w = WireEvent(Bytes::copy_from_slice(frame)); + match self { + Dialect::OpenAi => OpenAiRealtimeCodec.read_down(w, st), + Dialect::Gemini => GeminiLiveCodec.read_down(w, st), + } + } + + /// Frame a caller event; `None` when the dialect has no verb for it. + pub fn write_up(self, ev: IrClientEvent, st: &mut DecodeState) -> Option { + match self { + Dialect::OpenAi => OpenAiRealtimeCodec.write_up(ev, st), + Dialect::Gemini => GeminiLiveCodec.write_up(ev, st), + } + } + + /// Frame a far-end event; `None` when it is not a frame on its own. + pub fn write_down(self, ev: IrServerEvent, st: &mut DecodeState) -> Option { + match self { + Dialect::OpenAi => OpenAiRealtimeCodec.write_down(ev, st), + Dialect::Gemini => GeminiLiveCodec.write_down(ev, st), + } + } +} + +fn bytes_of(v: &Value) -> Vec { + serde_json::to_vec(v).expect("serialize wire value") +} + +// ── the shared normal form (the old harness's, verbatim in meaning) ───────────────────────────── + +/// Both direction-split IR enums collapsed onto one list, so concepts compare uniformly. +#[derive(Clone, Debug, PartialEq)] +pub enum Norm { + /// Config essentials only: instructions|voice|tools|max (the survivable fields). + Config(String), + /// The configured modalities. + ConfigModalities(Vec), + /// The session's acknowledgement. + Connect, + /// Uplink audio. + AudioUp(Vec), + /// Downlink audio. + AudioDown(Vec), + /// An item's audio is done. + AudioDone, + /// A conversation item. + Item(Value), + /// Speech began. + SpeechStart, + /// Speech stopped. + SpeechStop, + /// A truncate, at the ms heard. + Truncate(u64), + /// Commit the uplink. + Commit, + /// Clear the uplink. + Clear, + /// Ask for a response. + ResponseCreate, + /// Cancel the response. + ResponseCancel, + /// Delete an item. + ItemDelete, + /// Usage: audio in, audio out, text in, text out. + Usage(u64, u64, u64, u64), + /// A rate-limit report. + RateLimits, + /// An error: code, message. + Error(String, String), + /// A call opened: id, name. + ToolOpen(String, String), + /// A call's arguments: id, parsed. + ToolArgs(String, Value), + /// A call closed: id. + ToolClose(String), + /// A call's result: id, parsed output. + ToolResult(String, Value), +} + +fn cfg_essentials(config: &SessionConfig) -> Norm { + Norm::Config(format!( + "{:?}|{:?}|{}|{:?}", + config.instructions, + config.voice, + config.tools.len(), + config.max_output_tokens + )) +} + +fn norm_tool(t: &IrDuplexTool) -> Norm { + match t { + IrDuplexTool::CallOpen { call_id, name, .. } => { + Norm::ToolOpen(call_id.clone(), name.clone()) + } + IrDuplexTool::CallArgs { + call_id, + json_delta, + .. + } => Norm::ToolArgs( + call_id.clone(), + serde_json::from_slice(json_delta).unwrap_or(Value::Null), + ), + IrDuplexTool::CallClose { call_id, .. } => Norm::ToolClose(call_id.clone()), + IrDuplexTool::CallResult { + call_id, output, .. + } => Norm::ToolResult( + call_id.clone(), + serde_json::from_slice(output).unwrap_or(Value::Null), + ), + } +} + +/// Caller events, normalized. +pub fn norm_up(evs: &[IrClientEvent]) -> Vec { + let mut out = Vec::new(); + for e in evs { + match e { + IrClientEvent::AudioFrame(f) => out.push(Norm::AudioUp(f.media.to_vec())), + IrClientEvent::Tool(t) => out.push(norm_tool(t)), + IrClientEvent::Control(c) => match c { + IrDuplexControl::SessionConfigure { config } => { + out.push(cfg_essentials(config)); + out.push(Norm::ConfigModalities(config.modalities.clone())); + } + IrDuplexControl::ItemCreate { item } => out.push(Norm::Item(item.clone())), + IrDuplexControl::ItemTruncate { + audio_played_ms, .. + } => out.push(Norm::Truncate(*audio_played_ms)), + IrDuplexControl::InputAudioCommit => out.push(Norm::Commit), + IrDuplexControl::InputAudioClear => out.push(Norm::Clear), + IrDuplexControl::ResponseCreate { .. } => out.push(Norm::ResponseCreate), + IrDuplexControl::ResponseCancel => out.push(Norm::ResponseCancel), + IrDuplexControl::ItemDelete { .. } => out.push(Norm::ItemDelete), + }, + } + } + out +} + +/// Far-end events, normalized. +pub fn norm_down(evs: &[IrServerEvent]) -> Vec { + let mut out = Vec::new(); + for e in evs { + match e { + IrServerEvent::SessionCreated { .. } => out.push(Norm::Connect), + IrServerEvent::Tool(t) => out.push(norm_tool(t)), + IrServerEvent::SpeechStarted { .. } => out.push(Norm::SpeechStart), + IrServerEvent::SpeechStopped { .. } => out.push(Norm::SpeechStop), + IrServerEvent::AudioFrame(f) => out.push(Norm::AudioDown(f.media.to_vec())), + IrServerEvent::AudioDone { .. } => out.push(Norm::AudioDone), + IrServerEvent::Usage(u) => { + out.push(Norm::Usage(u.audio_in, u.audio_out, u.text_in, u.text_out)); + } + IrServerEvent::RateLimits => out.push(Norm::RateLimits), + IrServerEvent::Error { code, message } => { + out.push(Norm::Error(code.clone(), message.clone())); + } + } + } + out +} + +/// One correlated tool call, keyed by call id: `(name, args, result)`. +type CallEntry = (Option, Option, Option); + +/// A call-collapsed fingerprint: tool events merged by call id (an atomic Gemini `toolCall` +/// decodes to a streamed triple and the stateless writer re-frames per event, so arity differs +/// legitimately; the correlation must survive). Everything else compared verbatim, in order. +#[derive(Debug, Default, PartialEq)] +pub struct Fingerprint { + calls: BTreeMap, + other: Vec, +} + +/// The fingerprint of `norms`. +pub fn fingerprint(norms: &[Norm]) -> Fingerprint { + let mut fp = Fingerprint::default(); + for n in norms { + match n { + Norm::ToolOpen(id, name) => { + let e = fp.calls.entry(id.clone()).or_default(); + if e.0.is_none() && !name.is_empty() { + e.0 = Some(name.clone()); + } + } + Norm::ToolArgs(id, args) => { + let e = fp.calls.entry(id.clone()).or_default(); + if e.1.is_none() && !args.is_null() { + e.1 = Some(args.clone()); + } + } + Norm::ToolClose(id) => { + fp.calls.entry(id.clone()).or_default(); + } + Norm::ToolResult(id, out) => { + let e = fp.calls.entry(id.clone()).or_default(); + if e.2.is_none() { + e.2 = Some(out.clone()); + } + } + other => fp.other.push(other.clone()), + } + } + fp +} + +/// A concept's tag, for the replay skeleton. +fn tag(n: &Norm) -> &'static str { + match n { + Norm::Config(_) => "config", + Norm::ConfigModalities(_) => "config-modalities", + Norm::Connect => "connect", + Norm::AudioUp(_) => "audio-in", + Norm::AudioDown(_) => "audio-out", + Norm::AudioDone => "audio-done", + Norm::Item(_) => "item", + Norm::SpeechStart => "speech-start", + Norm::SpeechStop => "speech-stop", + Norm::Truncate(_) => "truncate", + Norm::Commit => "commit", + Norm::Clear => "clear", + Norm::ResponseCreate => "response-create", + Norm::ResponseCancel => "cancel", + Norm::ItemDelete => "item-delete", + Norm::Usage(..) => "usage", + Norm::RateLimits => "rate-limits", + Norm::Error(..) => "error", + Norm::ToolOpen(..) => "tool-open", + Norm::ToolArgs(..) => "tool-args", + Norm::ToolClose(..) => "tool-close", + Norm::ToolResult(..) => "tool-result", + } +} + +/// `expected` appears, in order, as a subsequence of `tags`. +fn ordered_subsequence(tags: &[&str], expected: &[&str]) -> Result<(), String> { + let mut i = 0; + for want in expected { + match tags[i..].iter().position(|t| t == want) { + Some(off) => i += off + 1, + None => { + return Err(format!( + "expected concept '{want}' not found after position {i} in {tags:?}" + )) + } + } + } + Ok(()) +} + +// ── wires, and what a session does with them ──────────────────────────────────────────────────── + +/// Which side of the session a frame comes from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Dir { + /// The caller. + Up, + /// The far end. + Down, +} + +/// One frame pushed to a session. +#[derive(Clone, Debug)] +pub struct Wire { + /// Its side. + pub dir: Dir, + /// Its bytes. + pub bytes: Vec, +} + +/// Drive `wires` through ONE session on `p`'s door for `dialect`. +pub fn relay(p: &Plugin, dialect: Dialect, wires: &[Wire]) -> Log { + let opened = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 77, dialect.claim()); + if opened.outcome != Outcome::Ready { + s.log.anomalies.push(format!("open {:?}", opened.outcome)); + } + for w in wires { + let from = match w.dir { + Dir::Up => FROM_CALLER, + Dir::Down => FROM_FAR_END, + }; + s.push(from, 0, &w.bytes); + } + s.log +} + +/// What the session rules say the door must relay for a sequence of wires. +pub struct Expected { + far: Vec, + caller: Vec, + tokens: Vec<(u32, u64)>, + answered: bool, +} + +/// The locked config, as it reads on the dialect's wire. +fn locked_norms(dialect: Dialect, locked: &SessionConfig) -> Vec { + let (mut w, mut r) = (DecodeState::default(), DecodeState::default()); + let configure = IrClientEvent::Control(IrDuplexControl::SessionConfigure { + config: locked.clone(), + }); + match dialect.write_up(configure, &mut w) { + Some(wire) => norm_up(&dialect.read_up(&wire.0, &mut r)), + None => Vec::new(), + } +} + +/// A barge-in's cancel and truncate, at `heard_ms` of `item`, as they read on the dialect's wire +/// (a dialect with no verb for them frames nothing). +fn barge_norms(dialect: Dialect, item: &str, heard_ms: u64) -> Vec { + let (mut w, mut r) = (DecodeState::default(), DecodeState::default()); + let mut out = Vec::new(); + for ev in [ + IrClientEvent::Control(IrDuplexControl::ResponseCancel), + IrClientEvent::Control(IrDuplexControl::ItemTruncate { + item_ref: item.to_string(), + content_index: 0, + audio_played_ms: heard_ms, + }), + ] { + if let Some(wire) = dialect.write_up(ev, &mut w) { + out.extend(norm_up(&dialect.read_up(&wire.0, &mut r))); + } + } + out +} + +/// The session rules over `wires`, read by the codec (never by the door). +pub fn expect(dialect: Dialect, wires: &[Wire]) -> Expected { + let locked = read_settings(SESSION) + .expect("the leg's section reads") + .session; + let locked = locked_norms(dialect, &locked); + let mut st = DecodeState::default(); + let mut x = Expected { + far: Vec::new(), + caller: Vec::new(), + tokens: Vec::new(), + answered: false, + }; + let mut sums = [0_u64; 4]; + for w in wires { + match w.dir { + Dir::Up => { + for ev in dialect.read_up(&w.bytes, &mut st) { + match ev { + IrClientEvent::Control(IrDuplexControl::SessionConfigure { .. }) => { + x.far.extend(locked.iter().cloned()); + } + ev => x.far.extend(norm_up(std::slice::from_ref(&ev))), + } + } + } + Dir::Down => { + x.answered = true; + for ev in dialect.read_down(&w.bytes, &mut st) { + match ev { + IrServerEvent::Usage(u) => { + for (sum, n) in sums.iter_mut().zip([ + u.audio_in, + u.audio_out, + u.text_in, + u.text_out, + ]) { + *sum = sum.saturating_add(n); + } + } + IrServerEvent::RateLimits + | IrServerEvent::Tool(IrDuplexTool::CallResult { .. }) => {} + IrServerEvent::SpeechStarted { item_id, .. } => { + let heard = st.flush_playback(); + x.far.extend(barge_norms(dialect, &item_id, heard)); + x.caller.push(Norm::SpeechStart); + } + ev => x.caller.extend(norm_down(std::slice::from_ref(&ev))), + } + } + } + } + } + x.tokens = (0_u32..).zip(sums).filter(|(_, n)| *n != 0).collect(); + x +} + +/// What the door relayed, read back by the codec: the far end's frames, the caller's, and every +/// concept's tag in the order the door emitted it. +pub struct Relayed { + far: Vec, + caller: Vec, + tags: Vec<&'static str>, +} + +/// Read `log`'s frames back under `dialect`. +pub fn decode_relay(dialect: Dialect, log: &Log) -> Relayed { + let (mut su, mut sd) = (DecodeState::default(), DecodeState::default()); + let mut r = Relayed { + far: Vec::new(), + caller: Vec::new(), + tags: Vec::new(), + }; + for e in &log.emissions { + let n = if e.to_far { + norm_up(&dialect.read_up(&e.frame, &mut su)) + } else { + norm_down(&dialect.read_down(&e.frame, &mut sd)) + }; + r.tags.extend(n.iter().map(tag)); + if e.to_far { + r.far.extend(n); + } else { + r.caller.extend(n); + } + } + r +} + +/// The door's relay of a session against the session rules: every answer READY, no record row, +/// every far frame on the dialect's socket need, both sides' frames on the expected fingerprint, +/// the far end's token reports as reported units (and the session fee once the far end answered). +pub fn relay_check(dialect: Dialect, log: &Log, x: &Expected) -> Result { + if !log.anomalies.is_empty() { + return Err(format!( + "the door did not answer every piece READY: {:?}", + log.anomalies + )); + } + if log.records != 0 { + return Err(format!( + "the door wrote {} record row(s) on a session", + log.records + )); + } + for e in log.emissions.iter().filter(|e| e.to_far) { + if e.need != dialect.ride() || e.verb != "GET" || e.target != dialect.socket() { + return Err(format!( + "a far frame rode need {} to {} {}, not the {} socket (need {})", + e.need, + e.verb, + e.target, + dialect.name(), + dialect.ride() + )); + } + } + let r = decode_relay(dialect, log); + if fingerprint(&r.far) != fingerprint(&x.far) { + return Err(format!( + "the far end was sent {:?}; the session rules say {:?}", + r.far, x.far + )); + } + if fingerprint(&r.caller) != fingerprint(&x.caller) { + return Err(format!( + "the caller was sent {:?}; the session rules say {:?}", + r.caller, x.caller + )); + } + if let Some(u) = log.units.iter().find(|u| u.1 != UNITS_REPORTED) { + return Err(format!("a unit was not reported by the far end: {u:?}")); + } + let tokens: Vec<(u32, u64)> = log + .units + .iter() + .filter(|u| u.0 < 4) + .map(|u| (u.0, u.2)) + .collect(); + if tokens != x.tokens { + return Err(format!( + "the token units reported {tokens:?}; the far end's reports add up to {:?}", + x.tokens + )); + } + let fee = log + .units + .iter() + .any(|u| u.0 == PER_SESSION_CLASS && u.2 == 1); + if fee != x.answered { + return Err(format!( + "the session fee unit reported {fee}, the far end answered {}", + x.answered + )); + } + Ok(format!( + "the {} door relayed {} far / {} caller concept(s) by the session rules, {} token unit(s)", + dialect.name(), + r.far.len(), + r.caller.len(), + tokens.len() + )) +} + +/// THE PLANTED WRONG DOOR for a relay: one that reports a token the far end never sent. +pub fn plant_units(log: &Log) -> Log { + let mut planted = log.clone(); + planted.units.push((1, UNITS_REPORTED, 7)); + planted +} + +/// Judge one relay of `wires` on both doors, with `codec` (the codec half's verdict) folded in. +fn judge_relay( + rig: &Rig, + slice: &str, + what: &str, + dialect: Dialect, + wires: &[Wire], + codec: &Result, +) -> bool { + let x = expect(dialect, wires); + let seen = rig.both(|p| relay(p, dialect, wires)); + judge( + slice, + what, + &seen, + &|log: &Log| { + let codec = codec.clone()?; + let door = relay_check(dialect, log, &x)?; + Ok(format!("codec: {codec}; door: {door}")) + }, + &plant_units, + ) +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════ +// spec-per-dialect +// ══════════════════════════════════════════════════════════════════════════════════════════════ + +/// Fixtures that legitimately decode to NOTHING, with the reason (the codec's documented drops). +fn drop_reason(dialect: Dialect, fixture: &str) -> Option<&'static str> { + match (dialect, fixture) { + (Dialect::Gemini, "goAway.json") => { + Some("gemini_go_away: no OpenAI advance-disconnect twin (drop+warn)") + } + (Dialect::Gemini, "toolCallCancellation.json") => { + Some("gemini_tool_call_cancellation: no OpenAI server-driven cancel (drop+warn)") + } + (Dialect::Gemini, "serverContent.inputTranscription.json") => { + Some("input transcription side-channel: no shared IR home (drop+warn)") + } + (Dialect::Gemini, "serverContent.outputTranscription.json") => { + Some("output transcription side-channel: no shared IR home (drop+warn)") + } + _ => None, + } +} + +/// A decoded fixture: the direction the codec recognized it in, and its IR. +enum Decoded { + Up(Vec), + Down(Vec), + Empty, +} + +fn decode(dialect: Dialect, frame: &[u8]) -> Decoded { + let up = dialect.read_up(frame, &mut DecodeState::default()); + if !up.is_empty() { + return Decoded::Up(up); + } + let down = dialect.read_down(frame, &mut DecodeState::default()); + if !down.is_empty() { + return Decoded::Down(down); + } + Decoded::Empty +} + +fn spec_verdict(n1: &[Norm], n2: &[Norm], arity: usize) -> Result { + if n1 == n2 { + return Ok(format!("IR-fixpoint stable ({arity} IR event(s))")); + } + if fingerprint(n1) == fingerprint(n2) { + return Ok(format!( + "IR-fixpoint stable by correlation fingerprint (atomic expansion, {arity} event(s))" + )); + } + Err(format!("round-trip diverged: {n1:?} != {n2:?}")) +} + +/// The codec half of one fixture, and the direction its frame is pushed from. +fn spec_codec(dialect: Dialect, fixture: &str, frame: &[u8]) -> (Result, Dir) { + match decode(dialect, frame) { + Decoded::Up(ir1) => { + let (mut w, mut r) = (DecodeState::default(), DecodeState::default()); + let mut ir2 = Vec::new(); + for e in &ir1 { + if let Some(wire) = dialect.write_up(e.clone(), &mut w) { + ir2.extend(dialect.read_up(&wire.0, &mut r)); + } + } + ( + spec_verdict(&norm_up(&ir1), &norm_up(&ir2), ir1.len()), + Dir::Up, + ) + } + Decoded::Down(ir1) => { + let (mut w, mut r) = (DecodeState::default(), DecodeState::default()); + let mut ir2 = Vec::new(); + for e in &ir1 { + if let Some(wire) = dialect.write_down(e.clone(), &mut w) { + ir2.extend(dialect.read_down(&wire.0, &mut r)); + } + } + ( + spec_verdict(&norm_down(&ir1), &norm_down(&ir2), ir1.len()), + Dir::Down, + ) + } + // A documented drop is a far-end concept with no shared home: pushed from the far end, the + // door must carry it on no wire at all. + Decoded::Empty => match drop_reason(dialect, fixture) { + Some(reason) => (Ok(format!("documented drop — {reason}")), Dir::Down), + None => ( + Err("decoded to NO IR events and is not a documented drop".to_string()), + Dir::Down, + ), + }, + } +} + +/// `spec `: every fixture, codec half and door half. +pub fn spec(rig: &Rig, dialect_name: &str, dir: &Path) -> i32 { + let Some(dialect) = Dialect::of(dialect_name) else { + result(dialect_name, false, "dialect", "unknown dialect"); + return 1; + }; + let mut fixtures: Vec = match fs::read_dir(dir) { + Ok(rd) => rd + .filter_map(Result::ok) + .map(|e| e.file_name().to_string_lossy().to_string()) + .filter(|n| n.ends_with(".json")) + .collect(), + Err(e) => { + result( + dialect.name(), + false, + "fixtures", + &format!("{}: {e}", dir.display()), + ); + return 1; + } + }; + fixtures.sort(); + if fixtures.is_empty() { + result(dialect.name(), false, "fixtures", "no .json fixtures"); + return 1; + } + let mut fails = 0; + for f in &fixtures { + let frame = match fs::read(dir.join(f)) + .map_err(|e| e.to_string()) + .and_then(|b| { + serde_json::from_slice::(&b) + .map(|v| bytes_of(&v)) + .map_err(|e| e.to_string()) + }) { + Ok(frame) => frame, + Err(e) => { + result( + dialect.name(), + false, + f, + &format!("unreadable fixture: {e}"), + ); + fails += 1; + continue; + } + }; + let (codec, side) = spec_codec(dialect, f, &frame); + let wires = [Wire { + dir: side, + bytes: frame, + }]; + if !judge_relay(rig, dialect.name(), f, dialect, &wires, &codec) { + fails += 1; + } + } + i32::from(fails > 0) +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════ +// replay +// ══════════════════════════════════════════════════════════════════════════════════════════════ + +fn read_jsonl(path: &Path) -> Result, String> { + let text = fs::read_to_string(path).map_err(|e| format!("{}: {e}", path.display()))?; + text.lines() + .filter(|l| !l.trim().is_empty()) + .map(|l| serde_json::from_str(l).map_err(|e| format!("{}: {e}", path.display()))) + .collect() +} + +/// A transcript's frames, as the session saw them (client → caller, server → far end). +fn transcript_wires(lines: &[Value]) -> Vec { + lines + .iter() + .filter_map(|line| { + let ev = line.get("event")?; + let dir = match line.get("dir").and_then(Value::as_str) { + Some("client") => Dir::Up, + Some("server") => Dir::Down, + _ => return None, + }; + Some(Wire { + dir, + bytes: bytes_of(ev), + }) + }) + .collect() +} + +/// The codec half of a replay: one session state, every frame decoded in order, every decoded +/// event re-framed to valid wire JSON, and the skeleton in order. +fn replay_codec(dialect: Dialect, wires: &[Wire], skeleton: &[&str]) -> Result { + let (mut st, mut wst) = (DecodeState::default(), DecodeState::default()); + let (mut tags, mut decoded, mut reencoded) = (Vec::new(), 0_usize, 0_usize); + let valid = |w: Option| { + w.is_some_and(|w| serde_json::from_slice::(&w.0).is_ok_and(|v| !v.is_null())) + }; + for w in wires { + match w.dir { + Dir::Up => { + let irs = dialect.read_up(&w.bytes, &mut st); + for ir in &irs { + decoded += 1; + reencoded += usize::from(valid(dialect.write_up(ir.clone(), &mut wst))); + } + tags.extend(norm_up(&irs).iter().map(tag)); + } + Dir::Down => { + let irs = dialect.read_down(&w.bytes, &mut st); + for ir in &irs { + decoded += 1; + reencoded += usize::from(valid(dialect.write_down(ir.clone(), &mut wst))); + } + tags.extend(norm_down(&irs).iter().map(tag)); + } + } + } + ordered_subsequence(&tags, skeleton)?; + Ok(format!( + "{decoded} IR events, {reencoded} re-framed, skeleton {skeleton:?} in order" + )) +} + +/// `replay `: each dialect's captured transcript, codec half and door half. +pub fn replay(rig: &Rig, root: &Path) -> i32 { + // The skeletons the codec re-derives; the door's relay must show the same concepts in the + // order it emitted them, less the far end's usage, which a session consumes into units. + let cases: [(Dialect, &[&str], &[&str]); 2] = [ + ( + Dialect::OpenAi, + &[ + "config", + "connect", + "audio-in", + "speech-start", + "tool-args", + "tool-close", + "tool-result", + "audio-out", + "speech-start", + "cancel", + ], + &[ + "config", + "connect", + "audio-in", + "speech-start", + "tool-args", + "tool-close", + "tool-result", + "audio-out", + "speech-start", + "cancel", + ], + ), + ( + Dialect::Gemini, + &[ + "config", + "connect", + "audio-in", + "tool-open", + "tool-args", + "tool-close", + "tool-result", + "audio-out", + "speech-start", + "audio-done", + "usage", + ], + &[ + "config", + "connect", + "audio-in", + "tool-open", + "tool-args", + "tool-close", + "tool-result", + "audio-out", + "speech-start", + "audio-done", + ], + ), + ]; + let mut fails = 0; + for (dialect, codec_skeleton, door_skeleton) in cases { + let what = format!("{}-transcript", dialect.name()); + let lines = match read_jsonl(&root.join(dialect.name()).join("transcript.jsonl")) { + Ok(lines) => lines, + Err(e) => { + result("default", false, &what, &e); + fails += 1; + continue; + } + }; + let wires = transcript_wires(&lines); + let codec = replay_codec(dialect, &wires, codec_skeleton); + let x = expect(dialect, &wires); + let seen = rig.both(|p| relay(p, dialect, &wires)); + let pass = judge( + "default", + &what, + &seen, + &|log: &Log| { + let codec = codec.clone()?; + let door = relay_check(dialect, log, &x)?; + ordered_subsequence(&decode_relay(dialect, log).tags, door_skeleton)?; + Ok(format!( + "codec: {codec}; door: {door}, skeleton {door_skeleton:?} in emitted order" + )) + }, + &plant_units, + ); + if !pass { + fails += 1; + } + } + i32::from(fails > 0) +} + +// ══════════════════════════════════════════════════════════════════════════════════════════════ +// cross-parity +// ══════════════════════════════════════════════════════════════════════════════════════════════ + +/// One bridge: the source's norms, the bridged-and-re-read norms, and the bridged wires (the B +/// dialect's frames, pushed through B's door). +struct Bridged { + n1: Vec, + n2: Vec, + wires: Vec, +} + +/// Bridge the frames `vals` FROM `a` TO `b` as one exchange (one source session, one destination). +fn bridge(a: Dialect, b: Dialect, vals: &[Vec]) -> Bridged { + let (mut rst, mut wst, mut bst) = ( + DecodeState::default(), + DecodeState::default(), + DecodeState::default(), + ); + let mut out = Bridged { + n1: Vec::new(), + n2: Vec::new(), + wires: Vec::new(), + }; + for v in vals { + let up = a.read_up(v, &mut rst); + if !up.is_empty() { + out.n1.extend(norm_up(&up)); + for e in up { + if let Some(w) = b.write_up(e, &mut wst) { + out.n2.extend(norm_up(&b.read_up(&w.0, &mut bst))); + out.wires.push(Wire { + dir: Dir::Up, + bytes: w.0.to_vec(), + }); + } + } + continue; + } + let down = a.read_down(v, &mut rst); + out.n1.extend(norm_down(&down)); + for e in down { + if let Some(w) = b.write_down(e, &mut wst) { + out.n2.extend(norm_down(&b.read_down(&w.0, &mut bst))); + out.wires.push(Wire { + dir: Dir::Down, + bytes: w.0.to_vec(), + }); + } + } + } + out +} + +fn pair_slice(a: Dialect, b: Dialect) -> &'static str { + match (a, b) { + (Dialect::OpenAi, Dialect::OpenAi) => "oo", + (Dialect::OpenAi, Dialect::Gemini) => "og", + (Dialect::Gemini, Dialect::OpenAi) => "go", + (Dialect::Gemini, Dialect::Gemini) => "gg", + } +} + +/// Why a concept has no decoding source fixture (never faked as a pass). +fn none_reason(concept: &str, from: Dialect) -> &'static str { + match (concept, from) { + ("input turn commit / end-of-audio", Dialect::Gemini) => { + "documented drop — gemini audioStreamEnd is dropped by the codec" + } + ("server-side VAD speech boundary", Dialect::Gemini) => { + "dialect-only concept — Gemini has no explicit server-VAD boundary fixture; exercised \ + as the openai_speech_boundary asymmetry in og" + } + ("input transcription" | "output transcription", _) => { + "documented side-channel drop+warn — no shared IR home; the same-dialect round-trip \ + is exercised in spec-per-dialect" + } + _ => "no decoding fixture on the source side (exercised in the reverse pair)", + } +} + +/// Concepts the map documents as dropping toward `to` (implicit or absent there). +fn directional_drop(concept: &str, to: Dialect) -> bool { + matches!( + (concept, to), + ("input turn commit / end-of-audio", Dialect::Gemini) + | ("barge-in / truncate / cancel", Dialect::Gemini) + ) +} + +fn cfg_from(norms: &[Norm]) -> Vec { + norms + .iter() + .filter_map(|n| match n { + Norm::Config(s) => Some(s.clone()), + _ => None, + }) + .collect() +} + +/// Do the shared concept's load-bearing fields survive `a` → IR → `b` → IR? +fn survives( + concept: &str, + n1: &[Norm], + n2: &[Norm], + a: Dialect, + b: Dialect, +) -> Result { + let (f1, f2) = (fingerprint(n1), fingerprint(n2)); + let (an, bn) = (a.name(), b.name()); + if f1 == f2 { + return Ok(format!("shared fields survive {an}→{bn} ({concept})")); + } + if concept.contains("session") && cfg_from(n1) == cfg_from(n2) && !cfg_from(n1).is_empty() { + return Ok(format!( + "session config essentials survive {an}→{bn} (modalities-text/VAD/format dropped per map)" + )); + } + if f2.calls.is_empty() && f2.other.is_empty() && directional_drop(concept, b) { + return Ok(format!( + "{concept}: documented directional drop {an}→{bn} (implicit / no counterpart per map)" + )); + } + Err(format!( + "{concept}: shared fields did not survive {an}→{bn}: {f1:?} != {f2:?}" + )) +} + +fn drop_if(cond: bool, msg: &str) -> Result { + if cond { + Ok(format!("accounted for — {msg}")) + } else { + Err(format!("NOT accounted for — {msg}")) + } +} + +/// A one-dialect-only concept bridged toward the other must be ACCOUNTED FOR. +fn asym_drop(id: &str, n1: &[Norm], n2: &[Norm]) -> Result { + let has = |ns: &[Norm], pred: &dyn Fn(&Norm) -> bool| ns.iter().any(pred); + match id { + "openai_buffer_clear" => drop_if( + !has(n2, &|n| matches!(n, Norm::Clear)), + "clear has no Gemini twin — dropped", + ), + "openai_response_overrides" => drop_if( + !has(n2, &|n| matches!(n, Norm::ResponseCreate)), + "per-response overrides dropped (Gemini is setup-time only)", + ), + "openai_truncate_precision" => drop_if( + !has(n2, &|n| matches!(n, Norm::Truncate(_))), + "sample-accurate truncate dropped (Gemini interrupted carries no ms)", + ), + "openai_structured_error" => drop_if( + !has(n2, &|n| matches!(n, Norm::Error(..))), + "structured error dropped toward Gemini (WS close codes instead)", + ), + "openai_event_id" | "openai_noise_reduction" => drop_if( + true, + "field never enters the IR (dropped at decode); bridged config omits it", + ), + "openai_semantic_vad" | "openai_g711" => drop_if( + has(n2, &|n| matches!(n, Norm::Config(_))), + "config bridges; semantic_vad/g711 specifics dropped per map", + ), + "openai_speech_boundary" => drop_if( + has(n2, &|n| matches!(n, Norm::SpeechStart)) || n1.is_empty(), + "boundary maps to interrupted; ms offset dropped", + ), + "openai_uplink_rate" => drop_if( + !n2.is_empty() + && n1 + .iter() + .zip(n2.iter()) + .all(|(a, b)| matches!((a, b), (Norm::AudioUp(x), Norm::AudioUp(y)) if x == y)), + "uplink audio bridges verbatim at its true rate; the resample toward Gemini's 16 kHz \ + input is not performed", + ), + "gemini_go_away" | "gemini_tool_call_cancellation" => drop_if( + n1.is_empty() && n2.is_empty(), + "no OpenAI twin — dropped at decode (drop+warn)", + ), + "gemini_generation_complete" => drop_if( + has(n2, &|n| matches!(n, Norm::AudioDone)), + "turnComplete→AudioDone survives; generationComplete collapsed", + ), + "gemini_audio_stream_end" => drop_if( + has(n1, &|n| matches!(n, Norm::Commit)) && has(n2, &|n| matches!(n, Norm::Commit)), + "audioStreamEnd ↔ input_audio_buffer.commit: the end-of-uplink turn survives", + ), + "gemini_setup_complete" => drop_if( + has(n2, &|n| matches!(n, Norm::Connect)), + "ack maps to session.created; gate semantics are the session's, not IR", + ), + other => Err(format!("no asymmetry handler for id '{other}'")), + } +} + +fn read_fixture(path: &Path) -> Option> { + let v: Value = serde_json::from_slice(&fs::read(path).ok()?).ok()?; + Some(bytes_of(&v)) +} + +/// `cross `: every shared concept, codec half and door half +/// (the bridged wire through the destination dialect's door), then every asymmetry row. +pub fn cross(rig: &Rig, pair: &str, oa: &Path, ge: &Path, map: &Value) -> i32 { + let (a, b) = match pair { + "oo" => (Dialect::OpenAi, Dialect::OpenAi), + "og" => (Dialect::OpenAi, Dialect::Gemini), + "go" => (Dialect::Gemini, Dialect::OpenAi), + "gg" => (Dialect::Gemini, Dialect::Gemini), + other => { + result(other, false, "pair", "unknown ordered pair"); + return 1; + } + }; + let slice = pair_slice(a, b); + let dir_for = |d: Dialect| if d == Dialect::OpenAi { oa } else { ge }; + let mut fails = 0; + let Some(concepts) = map["concepts"].as_array() else { + result( + slice, + false, + "map", + "the cross-dialect map has no concepts array", + ); + return 1; + }; + for c in concepts { + let concept = c["concept"].as_str().unwrap_or("?"); + let present: Vec<(String, Vec)> = c[a.name()]["fixtures"] + .as_array() + .map(Vec::as_slice) + .unwrap_or_default() + .iter() + .filter_map(|fx| { + let name = fx + .as_str() + .filter(|n| !n.is_empty() && !n.ends_with(".jsonl"))?; + Some((name.to_string(), read_fixture(&dir_for(a).join(name))?)) + }) + .collect(); + let mut chosen = present.iter().find_map(|(name, v)| { + let br = bridge(a, b, std::slice::from_ref(v)); + (!br.n1.is_empty()).then(|| (name.clone(), br)) + }); + // A concept whose transform is stated ACROSS events (the streamed⟷atomic tool call) can + // bridge one frame to nothing alone; judge its fixtures as the one exchange the map states. + if chosen.as_ref().is_some_and(|(_, br)| br.n2.is_empty()) && present.len() > 1 { + let vals: Vec> = present.iter().map(|(_, v)| v.clone()).collect(); + let names: Vec<&str> = present.iter().map(|(n, _)| n.as_str()).collect(); + chosen = Some((names.join("+"), bridge(a, b, &vals))); + } + match chosen { + None => println!( + "SUBITEM {slice}:{concept} PENDING — {}", + none_reason(concept, a) + ), + Some((name, br)) => { + let codec = survives(concept, &br.n1, &br.n2, a, b); + let what = format!("shared:{name}"); + if !judge_relay(rig, slice, &what, b, &br.wires, &codec) { + fails += 1; + } + } + } + } + let Some(asym) = map["asymmetry"].as_array() else { + result( + slice, + false, + "map", + "the cross-dialect map has no asymmetry array", + ); + return 1; + }; + for row in asym { + let id = row["id"].as_str().unwrap_or("?"); + let origin = row["dialect"].as_str().unwrap_or("?"); + let fixture = row["fixture"].as_str().unwrap_or_default(); + let what = format!("asym:{id}"); + // A row is EXERCISED in its origin → other direction only. + if a.name() != origin || a == b { + result( + slice, + true, + &what, + &format!( + "not this pair's drop direction (origin={origin}); exercised in the \ + {origin}→other pair" + ), + ); + continue; + } + let name = fixture + .strip_prefix(&format!("{origin}/")) + .unwrap_or(fixture); + let Some(v) = read_fixture(&dir_for(a).join(name)) else { + result(slice, false, &what, &format!("fixture {fixture} missing")); + fails += 1; + continue; + }; + let br = bridge(a, b, std::slice::from_ref(&v)); + match asym_drop(id, &br.n1, &br.n2) { + Ok(d) => result(slice, true, &what, &d), + Err(d) => { + result(slice, false, &what, &d); + fails += 1; + } + } + } + i32::from(fails > 0) +} + +/// The codec half of the V4 governance checkpoint: an OpenAI-only concept bridged toward Gemini +/// is down-scoped, never widened. Answers the bridged norms' text. +pub fn downscope_bridge(update: &Value) -> String { + format!( + "{:?}", + bridge(Dialect::OpenAi, Dialect::Gemini, &[bytes_of(update)]).n2 + ) +} + +/// The frame bytes of a JSON value. +pub fn frame(v: &Value) -> Vec { + bytes_of(v) +} + +/// The far-end concepts a log's frames carry toward the caller, decoded under `dialect`. +pub fn caller_norms(dialect: Dialect, log: &Log) -> Vec { + decode_relay(dialect, log).caller +} + +/// The concepts a log's frames carry toward the far end, decoded under `dialect`. +pub fn far_norms(dialect: Dialect, log: &Log) -> Vec { + decode_relay(dialect, log).far +} + +/// The token units a usage frame reports, read by the codec: `(class, amount)`, zeros left out. +pub fn usage_tokens(dialect: Dialect, usage_frame: &[u8]) -> Vec<(u32, u64)> { + expect( + dialect, + &[Wire { + dir: Dir::Down, + bytes: usage_frame.to_vec(), + }], + ) + .tokens +} diff --git a/testing/voice-conformance/harness/src/composition.rs b/testing/voice-conformance/harness/src/composition.rs new file mode 100644 index 0000000000..5fb080afba --- /dev/null +++ b/testing/voice-conformance/harness/src/composition.rs @@ -0,0 +1,1344 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE COMPOSITION LEGS, AT THE DOOR. Each leg judged one seam of a served session; the plane is +//! now served only through its door, so each leg judges the DOOR'S HALF of that seam — what the +//! door states, answers and refuses to do — and names the half that is the kernel's (the plane +//! driver's walk, the identity crate, the money steps, the transport), which the plane cannot see +//! and this harness therefore cannot judge. + +use busbar_contract::abi::host::conn::connector::DIRECTION_OUTBOUND; +use busbar_contract::abi::mechanism::call::Outcome; +use busbar_contract::abi::plane::{ + FROM_CALLER, FROM_FAR_END, FROM_KERNEL, PIECE_HAS_STATUS, PIECE_LAST, PRINCIPAL_NONE, + PRINCIPAL_REQUIRED, ROUTE_DIRECT, ROUTE_POOL, +}; +use busbar_contract::media::base64_encode; +use busbar_plane_streaming::claims::{HTTP_TRANSPORT, WS_TRANSPORT}; +use busbar_plane_streaming::door::{ + refusal_body, AUDIT_KIND, LIVE_STYLE, REALTIME_STYLE, REFUSAL_NO_DOOR, RIDES_LIVE_SOCKET, + RIDES_REALTIME_PASS, RIDES_REALTIME_SOCKET, +}; +use busbar_plane_streaming::driven::{ + mint_reply, GEMINI_SOCKET_PATH, MINT_FAILED_STATUS, REALTIME_SOCKET_PATH, SESSION_OPEN_ACTION, +}; +use busbar_plane_streaming::provider::{GEMINI_LIVE, OPENAI_REALTIME}; +use busbar_plugin_loader::dispatch::kinds::plane::Plane; +use busbar_plugin_loader::dispatch::Plugin; +use serde_json::{json, Value}; + +use crate::codec_legs::{caller_norms, far_norms, fingerprint, frame, usage_tokens, Dialect, Norm}; +use crate::door::{ + arrive, attempt, drive, judge, open, piece, refusal, request, result, session, Answer, Arrived, + Emission, Log, Opened, Piece, Rendered, Rig, Session, CALLER_REF, MODEL, PUBLIC, SESSION, +}; + +// ── the frames the legs push ──────────────────────────────────────────────────────────────────── + +/// A far end's OpenAI Realtime usage report. +pub const USAGE_DONE: &[u8] = br#"{"type":"response.done","response":{"usage":{"input_token_details":{"audio_tokens":10,"text_tokens":3},"output_token_details":{"audio_tokens":20,"text_tokens":4}}}}"#; + +/// A far end's Gemini Live usage report. +pub const GEMINI_USAGE: &[u8] = br#"{"usageMetadata":{"promptTokenCount":140,"responseTokenCount":80,"totalTokenCount":220,"promptTokensDetails":[{"modality":"TEXT","tokenCount":40},{"modality":"AUDIO","tokenCount":100}],"responseTokensDetails":[{"modality":"AUDIO","tokenCount":80}]}}"#; + +/// The cumulative units a session reports once `USAGE_DONE` closed a turn holding `secs` seconds +/// of the caller's audio: the four token classes, the audio seconds and the session fee. +pub fn usage_units(secs: u64) -> String { + format!("0:1=10 1:1=20 2:1=3 3:1=4 4:1={secs} 6:1=1") +} + +/// `ms` milliseconds of the caller's 24 kHz PCM16 audio, as an OpenAI Realtime frame. +pub fn uplink(ms: usize) -> Vec { + frame(&json!({"type": "input_audio_buffer.append", + "audio": base64_encode(&vec![0_u8; ms * 48])})) +} + +/// `ms` milliseconds of the caller's 16 kHz PCM16 audio, as a Gemini Live frame. +pub fn gemini_uplink(ms: usize) -> Vec { + frame( + &json!({"realtimeInput": {"audio": {"mimeType": "audio/pcm;rate=16000", + "data": base64_encode(&vec![0_u8; ms * 32])}}}), + ) +} + +/// `bytes` bytes of the far end's OpenAI Realtime audio. +pub fn downlink(bytes: usize) -> Vec { + frame(&json!({"type": "response.output_audio.delta", + "delta": base64_encode(&vec![0_u8; bytes])})) +} + +/// The OpenAI Realtime mint's far-end answer. +const MINTED: &[u8] = br#"{"value":"ek_oracle_0001","expires_at":1767225600}"#; + +/// One `answer` as a line: the step, the outcome and the cumulative units. +fn units_of(what: &str, a: &Answer) -> String { + format!("{what} {:?} units=[{}]", a.outcome, a.units_line()) +} + +/// `composition `. +pub fn run(rig: &Rig, slice: &str) -> i32 { + let pass = match slice { + "provider-credential" => provider_credential(rig, slice), + "metering-lease" => metering_lease(rig, slice), + "session-scope" => session_scope(rig, slice), + "gemini-live-route" => gemini_live_route(rig, slice), + "provider-dial" => provider_dial(rig, slice), + "admit-refusal" => admit_refusal(rig, slice), + "route-failover" => route_failover(rig, slice), + "audit-record" => audit_record(rig, slice), + "exit-terminal" => exit_terminal(rig, slice), + "tool-reply" => tool_reply(rig, slice), + other => { + result(other, false, "slice", "unknown composition slice"); + false + } + }; + i32::from(!pass) +} + +// ── provider-credential ───────────────────────────────────────────────────────────────────────── + +/// One need the Statement declares. +#[derive(Clone, Debug, PartialEq, Eq)] +struct NeedLine { + direction: u32, + transport: String, + auth: String, + target: String, +} + +/// One far request a door answered. +#[derive(Clone, Debug, PartialEq, Eq)] +struct Ride { + door: &'static str, + verb: String, + target: String, + need: u32, + fields: Vec<(String, String)>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct CredObs { + needs: Vec, + dialect_auth: Vec, + rides: Vec, +} + +/// Head fields that carry a credential: none may ride a request the door answers. +const CREDENTIAL_FIELDS: &[&str] = &[ + "authorization", + "proxy-authorization", + "x-goog-api-key", + "x-api-key", + "api-key", + "openai-api-key", + "cookie", +]; + +fn ride(door: &'static str, a: &Answer) -> Ride { + Ride { + door, + verb: a.verb.clone(), + target: a.target.clone(), + need: a.need, + fields: a.fields.clone(), + } +} + +fn cred_script(p: &Plugin) -> CredObs { + let served = p.served(); + let needs = served + .need_auths + .iter() + .zip(&served.need_transports) + .zip(&served.need_targets) + .map(|((da, t), g)| NeedLine { + direction: da.0, + transport: (*t).to_string(), + auth: da.1.to_string(), + target: (*g).to_string(), + }) + .collect(); + let dialect_auth = served + .dialect_auth + .iter() + .map(|(d, style, _)| { + let name = served.dialects.get(*d as usize).copied().unwrap_or("?"); + format!("{name}={style}") + }) + .collect(); + let _ = open(p, SESSION, Some(PUBLIC)); + let mut rides = vec![ + ride("mint", &piece(p, &attempt(7, 0, 1))), + ride("sdp", &piece(p, &attempt(8, 1, 1))), + ]; + let mut s = Session::open(p, 20, 2); + rides.push(ride("sideband", &s.push(FROM_CALLER, 0, &uplink(100)))); + let mut g = Session::open(p, 21, 3); + rides.push(ride("gemini", &g.push(FROM_CALLER, 0, &gemini_uplink(100)))); + CredObs { + needs, + dialect_auth, + rides, + } +} + +fn cred_check(o: &CredObs) -> Result { + if o.rides.len() != 4 { + return Err(format!("expected four far requests, saw {:?}", o.rides)); + } + for r in &o.rides { + let (transport, style) = match r.door { + "mint" | "sdp" => (HTTP_TRANSPORT, REALTIME_STYLE), + "sideband" => (WS_TRANSPORT, REALTIME_STYLE), + _ => (WS_TRANSPORT, LIVE_STYLE), + }; + let Some(need) = (r.need as usize) + .checked_sub(1) + .and_then(|i| o.needs.get(i)) + else { + return Err(format!( + "the {} request rides need {}, which the Statement does not declare", + r.door, r.need + )); + }; + if need.direction != DIRECTION_OUTBOUND + || need.transport != transport + || need.auth != style + || !need.target.is_empty() + { + return Err(format!( + "the {} request rides {need:?}, not an outbound {transport} need under the \ + {style} style whose address is the member's", + r.door + )); + } + if let Some((name, _)) = r.fields.iter().find(|(n, v)| { + CREDENTIAL_FIELDS.contains(&n.to_ascii_lowercase().as_str()) + || v.to_ascii_lowercase().starts_with("bearer ") + }) { + return Err(format!( + "the {} request carries a credential field `{name}`", + r.door + )); + } + if r.verb.is_empty() || r.target.is_empty() { + return Err(format!( + "the {} request names no verb/target: {r:?}", + r.door + )); + } + } + let mint = &o.rides[0]; + if !mint + .fields + .iter() + .any(|(n, v)| n.eq_ignore_ascii_case("openai-safety-identifier") && v == CALLER_REF) + { + return Err(format!( + "the mint does not name the caller by the kernel's reference alone: {:?}", + mint.fields + )); + } + for want in [ + format!("{OPENAI_REALTIME}={REALTIME_STYLE}"), + format!("{GEMINI_LIVE}={LIVE_STYLE}"), + ] { + if !o.dialect_auth.contains(&want) { + return Err(format!( + "the tail's dialect auth lacks {want}: {:?}", + o.dialect_auth + )); + } + } + Ok( + "every far request (mint, SDP, the sideband and Gemini sockets) rides a declared outbound \ + need whose auth style the kernel binds the provider credential to; none carries a \ + credential field, the caller is named only by the kernel's reference, and each dialect \ + states its far end's credential style. Resolving the catalog's secret reference, the \ + set-once composition and an unresolvable reference composing nothing are the \ + composition root's (busbar), not the door's" + .to_string(), + ) +} + +fn provider_credential(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "the door's far requests ride declared needs and carry no credential", + &rig.both(cred_script), + &cred_check, + &|o: &CredObs| { + let mut planted = o.clone(); + planted.rides[0] + .fields + .push(("authorization".to_string(), "Bearer sk-caller".to_string())); + planted + }, + ) +} + +// ── metering-lease ────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct MeterObs { + classes: Vec, + fees: Vec, + admitted: Vec, + lines: Vec, +} + +fn meter_script(p: &Plugin) -> MeterObs { + let served = p.served(); + let classes = served + .billable_classes + .iter() + .zip(&served.billable_families) + .map(|(c, f)| format!("{c}/{f}")) + .collect(); + let fees = served.fee_units.iter().map(|f| (*f).to_string()).collect(); + let _ = open(p, SESSION, Some(PUBLIC)); + let admitted = (0..5).map(|claim| arrive(p, claim).units).collect(); + let audio = uplink(2000); + let mut lines = Vec::new(); + let a = piece(p, &session(20, 2, FROM_CALLER, 0, &audio)); + lines.push(units_of("caller audio", &a)); + let turn = Piece { + attempt_no: 1, + ..session(20, 2, FROM_KERNEL, 0, &[]) + }; + lines.push(units_of("turn attempt", &piece(p, &turn))); + let far = Piece { + status: 101, + ..session(20, 2, FROM_FAR_END, PIECE_HAS_STATUS, USAGE_DONE) + }; + lines.push(units_of("far usage", &piece(p, &far))); + lines.push(units_of( + "collect", + &piece(p, &session(20, 2, FROM_KERNEL, 0, &[])), + )); + let short = Piece { + units_cap: 2, + ..session(20, 2, FROM_CALLER, 0, &audio) + }; + lines.push(units_of("short", &piece(p, &short))); + lines.push(units_of( + "short re-call", + &piece(p, &session(20, 2, FROM_CALLER, 0, &audio)), + )); + lines.push(units_of( + "caller end", + &piece(p, &session(20, 2, FROM_CALLER, PIECE_LAST, &[])), + )); + lines.push(units_of( + "unanswered audio", + &piece(p, &session(30, 2, FROM_CALLER, 0, &audio)), + )); + lines.push(units_of( + "unanswered end", + &piece(p, &session(30, 2, FROM_CALLER, PIECE_LAST, &[])), + )); + MeterObs { + classes, + fees, + admitted, + lines, + } +} + +fn meter_expected() -> Vec { + vec![ + "caller audio Ready units=[]".to_string(), + "turn attempt Ready units=[]".to_string(), + format!("far usage Ready units=[{}]", usage_units(2)), + format!("collect Ready units=[{}]", usage_units(2)), + "short Failed units=[needed 6]".to_string(), + format!("short re-call Ready units=[{}]", usage_units(2)), + format!("caller end Ready units=[{}]", usage_units(4)), + "unanswered audio Ready units=[]".to_string(), + "unanswered end Ready units=[4:1=2]".to_string(), + ] +} + +fn meter_check(o: &MeterObs) -> Result { + let classes = [ + "audio_tokens_in/token", + "audio_tokens_out/token", + "text_tokens_in/token", + "text_tokens_out/token", + "audio_seconds_in/duration", + "tool_calls/count", + "per_session/count", + ]; + if o.classes != classes { + return Err(format!("the tail's billable classes are {:?}", o.classes)); + } + if o.fees != ["per_session"] { + return Err(format!("the tail's fee units are {:?}", o.fees)); + } + if o.admitted.iter().any(|n| *n != 0) { + return Err(format!( + "an arrival estimated units before the far end reported any: {:?}", + o.admitted + )); + } + let want = meter_expected(); + if o.lines != want { + return Err(format!("the session reported {:?}; want {want:?}", o.lines)); + } + Ok( + "the door counts in its six declared classes plus the per-session fee, admits on no \ + estimate, and reports every turn's far-end tokens and audio seconds as cumulative \ + REPORTED units: once per turn (a short answer's re-call does not count its audio \ + twice), the open turn settled once at the end, and no fee unless the far end answered. \ + Reserving against the caller's budget chain, capping at its remaining bucket and \ + ledgering are the kernel's session account" + .to_string(), + ) +} + +fn metering_lease(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "a session's units, reported per declared class", + &rig.both(meter_script), + &meter_check, + &|o: &MeterObs| { + let mut planted = o.clone(); + planted.lines[2] = planted.lines[2].replace("1:1=20", "1:1=21"); + planted + }, + ) +} + +// ── session-scope ─────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ScopeObs { + scope_kinds: Vec, + arrivals: Vec, + refused: Rendered, +} + +const SCOPE_CLAIMS: [u32; 7] = [0, 1, 2, 3, 4, 5, 9]; + +fn scope_script(p: &Plugin) -> ScopeObs { + let scope_kinds = p + .declared() + .scope_kinds + .iter() + .map(|k| (*k).to_string()) + .collect(); + let _ = open(p, SESSION, Some(PUBLIC)); + ScopeObs { + scope_kinds, + arrivals: SCOPE_CLAIMS.iter().map(|c| arrive(p, *c)).collect(), + refused: refusal(p, 0, 0, 403, "the key holds no session grant for this door"), + } +} + +/// A rendered refusal's error type and message, read back. +fn error_of(r: &Rendered) -> (String, String) { + let v: Value = serde_json::from_str(&r.body).unwrap_or(Value::Null); + ( + v["error"]["type"].as_str().unwrap_or_default().to_string(), + v["error"]["message"] + .as_str() + .unwrap_or_default() + .to_string(), + ) +} + +fn scope_check(o: &ScopeObs) -> Result { + if o.scope_kinds != ["session"] { + return Err(format!("the tail's grant kinds are {:?}", o.scope_kinds)); + } + for (claim, a) in SCOPE_CLAIMS.iter().zip(&o.arrivals) { + let door = *claim <= 4; + let ok = if door { + a.outcome == Outcome::Ready && a.principal == PRINCIPAL_REQUIRED && a.op_class == 0 + } else { + a.outcome == Outcome::Refused && a.refusal == (REFUSAL_NO_DOOR, 404) + }; + if !ok { + return Err(format!("claim {claim} arrived as {a:?}")); + } + } + let (kind, words) = error_of(&o.refused); + if o.refused.outcome != Outcome::Ready + || kind != "permission_error" + || words != "the key holds no session grant for this door" + || o.refused.status != 0 + { + return Err(format!("the grant refusal rendered as {:?}", o.refused)); + } + Ok( + "the door declares the `session` grant kind, every one of its five doors asks for a \ + principal (none admits anonymously) under the one session-open operation, an \ + unpublished claim is refused 404 with the plane's own code, and a grant refusal renders \ + as the dialect's permission_error. Matching the key's grants (wildcard, explicit, \ + another pool, an empty list) is the kernel's approve step" + .to_string(), + ) +} + +fn session_scope(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "the door's grant kind and principal need", + &rig.both(scope_script), + &scope_check, + &|o: &ScopeObs| { + let mut planted = o.clone(); + planted.arrivals[2].principal = PRINCIPAL_NONE; + planted + }, + ) +} + +// ── gemini-live-route ─────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RouteObs { + opened: Opened, + arrived: Arrived, + dialect: String, + handshake: Log, +} + +const GEMINI_SETUP: &[u8] = br#"{"setup":{"model":"models/gemini-2.0-flash-exp","generationConfig":{"responseModalities":["AUDIO"]}}}"#; +const SETUP_COMPLETE: &[u8] = br#"{"setupComplete":{}}"#; + +fn route_script(p: &Plugin) -> RouteObs { + let opened = open(p, SESSION, Some(PUBLIC)); + let arrived = arrive(p, 3); + let dialect = p + .served() + .dialects + .get(arrived.dialect as usize) + .copied() + .unwrap_or("?") + .to_string(); + let mut s = Session::open(p, 40, 3); + s.push(FROM_CALLER, 0, GEMINI_SETUP); + s.push(FROM_FAR_END, 0, SETUP_COMPLETE); + RouteObs { + opened, + arrived, + dialect, + handshake: s.log, + } +} + +fn route_check(o: &RouteObs) -> Result { + let claimed = |line: &str, dialect: u16| { + o.opened + .claims + .iter() + .any(|c| c.line == line && c.refusal_dialect == dialect) + }; + if o.opened.outcome != Outcome::Ready + || !claimed("GET /v1/realtime/gemini/{call_id} ws", 1) + || !claimed("GET /v1/realtime/sideband/{call_id} ws", 0) + { + return Err(format!( + "the snapshot does not claim the Gemini socket (refused in Gemini's dialect) beside \ + the sideband: {:?}", + o.opened + )); + } + if o.opened.audience != "https://gw.example.com/v1/realtime" { + return Err(format!("the one audience is {}", o.opened.audience)); + } + let a = &o.arrived; + if a.outcome != Outcome::Ready + || a.principal != PRINCIPAL_REQUIRED + || a.route != (ROUTE_DIRECT, MODEL.to_string()) + || o.dialect != GEMINI_LIVE + { + return Err(format!( + "the Gemini door arrived as {a:?} in dialect {}", + o.dialect + )); + } + let h = &o.handshake; + if !h.anomalies.is_empty() { + return Err(format!( + "the handshake was not answered READY: {:?}", + h.anomalies + )); + } + let far: Vec<&Emission> = h.emissions.iter().filter(|e| e.to_far).collect(); + let near: Vec<&Emission> = h.emissions.iter().filter(|e| !e.to_far).collect(); + if far.len() != 1 + || far[0].need != RIDES_LIVE_SOCKET + || far[0].verb != "GET" + || far[0].target != GEMINI_SOCKET_PATH + { + return Err(format!("the caller's setup reached the far end as {far:?}")); + } + if !far_norms(Dialect::Gemini, h) + .iter() + .any(|n| matches!(n, Norm::Config(_))) + { + return Err("the far end was not sent a Gemini setup".to_string()); + } + let relayed: Value = near + .first() + .and_then(|e| serde_json::from_slice(&e.frame).ok()) + .unwrap_or(Value::Null); + let sent: Value = serde_json::from_slice(SETUP_COMPLETE).unwrap_or(Value::Null); + if near.len() != 1 || relayed != sent || caller_norms(Dialect::Gemini, h) != [Norm::Connect] { + return Err(format!( + "the far end's setupComplete reached the caller as {near:?}" + )); + } + Ok( + "the Gemini Live socket is claimed under its own path (its refusals in Gemini's dialect) \ + beside the OpenAI doors under the one audience, arrives as gemini_live for a principal \ + on the session model's DIRECT route, and the handshake crosses the door: the caller's \ + setup reaches the far end as a Gemini setup on the Live socket need, the far end's \ + setupComplete reaches the caller verbatim" + .to_string(), + ) +} + +fn gemini_live_route(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "the Gemini Live door: claim, arrival and handshake", + &rig.both(route_script), + &route_check, + &|o: &RouteObs| { + let mut planted = o.clone(); + for c in &mut planted.opened.claims { + c.refusal_dialect = 0; + } + planted + }, + ) +} + +// ── provider-dial ─────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct DialObs { + openai: Log, + gemini: Log, +} + +fn dial_script(p: &Plugin) -> DialObs { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 50, 2); + s.push(FROM_CALLER, 0, &uplink(100)); + s.push(FROM_FAR_END, 0, USAGE_DONE); + let mut g = Session::open(p, 51, 3); + g.push(FROM_CALLER, 0, &gemini_uplink(100)); + g.push(FROM_FAR_END, 0, GEMINI_USAGE); + DialObs { + openai: s.log, + gemini: g.log, + } +} + +fn dial_one( + dialect: Dialect, + log: &Log, + usage: &[u8], + need: u32, + socket: &str, +) -> Result { + if !log.anomalies.is_empty() { + return Err(format!( + "{}: not every piece READY: {:?}", + dialect.name(), + log.anomalies + )); + } + let far: Vec<&Emission> = log.emissions.iter().filter(|e| e.to_far).collect(); + if far.is_empty() + || far + .iter() + .any(|e| e.verb != "GET" || e.target != socket || e.need != need) + { + return Err(format!( + "{}: the session's far frames are not GET {socket} on need {need}: {far:?}", + dialect.name() + )); + } + let want = usage_tokens(dialect, usage); + let got: Vec<(u32, u64)> = log + .units + .iter() + .filter(|u| u.0 < 4) + .map(|u| (u.0, u.2)) + .collect(); + if want.is_empty() || got != want || !log.units.iter().any(|u| u.0 == 6 && u.2 == 1) { + return Err(format!( + "{}: the far end's usage {want:?} was reported as {:?}", + dialect.name(), + log.units + )); + } + Ok(format!( + "{}: GET {socket} on need {need}, the far end's usage reported as {got:?} plus the fee", + dialect.name() + )) +} + +fn dial_check(o: &DialObs) -> Result { + let a = dial_one( + Dialect::OpenAi, + &o.openai, + USAGE_DONE, + RIDES_REALTIME_SOCKET, + REALTIME_SOCKET_PATH, + )?; + let b = dial_one( + Dialect::Gemini, + &o.gemini, + GEMINI_USAGE, + RIDES_LIVE_SOCKET, + GEMINI_SOCKET_PATH, + )?; + Ok(format!( + "a session names the dialect's socket under the member's base URL on its declared need \ + and reports what arrives over it ({a}; {b}). Opening the socket (net guard, TLS, the \ + upgrade) is the kernel transport's" + )) +} + +fn provider_dial(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "a session's far frames ride the dialect's socket and its usage is reported", + &rig.both(dial_script), + &dial_check, + &|o: &DialObs| { + let mut planted = o.clone(); + if let Some(e) = planted.openai.emissions.iter_mut().find(|e| e.to_far) { + e.need = RIDES_REALTIME_PASS; + } + planted + }, + ) +} + +// ── admit-refusal ─────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct AdmitObs { + rendered: Vec<(u16, Rendered)>, + after: Vec, + drive: (Outcome, Vec), + control: Answer, +} + +const REFUSALS: [(u16, &str, &str); 3] = [ + (429, "the caller's budget is exhausted", "rate_limit_error"), + (403, "the key holds no session grant", "permission_error"), + (401, "no credential was presented", "authentication_error"), +]; + +fn admit_script(p: &Plugin) -> AdmitObs { + let _ = open(p, SESSION, Some(PUBLIC)); + let rendered = REFUSALS + .iter() + .map(|(status, words, _)| (*status, refusal(p, 60, 0, u32::from(*status), words))) + .collect(); + let far = piece(p, &session(60, 2, FROM_FAR_END, 0, &downlink(96))); + let collect = piece(p, &session(60, 2, FROM_KERNEL, 0, &[])); + let after = vec![ + format!("far {:?} emitted={}", far.outcome, far.emitted.len()), + format!( + "collect {:?} emitted={}", + collect.outcome, + collect.emitted.len() + ), + ]; + let drive = drive(p); + let mut s = Session::open(p, 61, 2); + let control = s.push(FROM_CALLER, 0, &uplink(100)); + AdmitObs { + rendered, + after, + drive, + control, + } +} + +fn admit_check(o: &AdmitObs) -> Result { + for ((status, r), (_, words, kind)) in o.rendered.iter().zip(REFUSALS) { + let body: Value = + serde_json::from_slice(&refusal_body(*status, words)).unwrap_or(Value::Null); + let (k, w) = error_of(r); + if r.outcome != Outcome::Ready + || k != kind + || w != words + || serde_json::from_str::(&r.body).ok() != Some(body) + || r.fields != [("content-type".to_string(), "application/json".to_string())] + || r.status != 0 + || r.records != 0 + { + return Err(format!("the kernel's {status} refusal rendered as {r:?}")); + } + } + if o.after != ["far Refused emitted=0", "collect Refused emitted=0"] { + return Err(format!( + "a refused unit's stream was answered as though a session were open: {:?}", + o.after + )); + } + if o.drive != (Outcome::Ready, Vec::new()) { + return Err(format!( + "drive named a session after the refusal: {:?}", + o.drive + )); + } + let c = &o.control; + if c.outcome != Outcome::Ready || !c.to_far || c.need != RIDES_REALTIME_SOCKET { + return Err(format!( + "the admitted control did not reach the far end: {c:?}" + )); + } + Ok( + "the kernel's refusal (budget 429, grant 403, credential 401) is rendered in the \ + dialect's error envelope with the kernel's status kept and no record written; a refused \ + unit opens no session and dials nothing (its stream's far piece and collection are \ + refused, drive names nothing), while an admitted control dials. Reading the caller's \ + chain dry and refusing before the hold is the kernel's admit step" + .to_string(), + ) +} + +fn admit_refusal(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "a refusal opens nothing and is rendered in the dialect's shape", + &rig.both(admit_script), + &admit_check, + &|o: &AdmitObs| { + let mut planted = o.clone(); + planted.rendered[0].1.body = + String::from_utf8_lossy(&refusal_body(500, "x")).into_owned(); + planted + }, + ) +} + +// ── route-failover ────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct FailObs { + arrived: Arrived, + first: Answer, + failed: Answer, + second: Answer, + caller: Answer, + answered: Answer, + terminal_attempt: Answer, + terminal: Answer, +} + +fn fail_script(p: &Plugin) -> FailObs { + let _ = open(p, SESSION, Some(PUBLIC)); + let arrived = arrive(p, 0); + let first = piece(p, &attempt(70, 0, 1)); + let failed = piece( + p, + &Piece { + status: 503, + ..request( + 70, + 0, + FROM_FAR_END, + PIECE_HAS_STATUS, + b"upstream unavailable", + ) + }, + ); + let second = piece(p, &attempt(70, 0, 2)); + let caller = piece(p, &request(70, 0, FROM_CALLER, PIECE_LAST, &[])); + let answered = piece( + p, + &Piece { + status: 200, + ..request(70, 0, FROM_FAR_END, PIECE_HAS_STATUS | PIECE_LAST, MINTED) + }, + ); + let terminal_attempt = piece(p, &attempt(71, 0, 1)); + let terminal = piece( + p, + &Piece { + status: 503, + ..request(71, 0, FROM_FAR_END, PIECE_HAS_STATUS | PIECE_LAST, b"down") + }, + ); + FailObs { + arrived, + first, + failed, + second, + caller, + answered, + terminal_attempt, + terminal, + } +} + +/// Whether two ATTEMPTs answered the same far request: verb, target, fields, body and need. +fn same_request(a: &Answer, b: &Answer) -> bool { + a.verb == b.verb + && a.target == b.target + && a.fields == b.fields + && a.emitted == b.emitted + && a.need == b.need +} + +fn fail_check(o: &FailObs) -> Result { + if o.arrived.outcome != Outcome::Ready || o.arrived.route != (ROUTE_DIRECT, MODEL.to_string()) { + return Err(format!("the mint arrived as {:?}", o.arrived)); + } + let f = &o.first; + if f.outcome != Outcome::Ready + || !f.to_far + || f.verb != "POST" + || f.target != "/v1/realtime/client_secrets" + || f.need != RIDES_REALTIME_PASS + || f.emitted.is_empty() + { + return Err(format!("attempt 1 sent {f:?}")); + } + if o.failed.outcome != Outcome::Ready || !o.failed.emitted.is_empty() || o.failed.done { + return Err(format!( + "a failing far end's first bytes reached the caller (the walk could no longer fail \ + over): {:?}", + o.failed + )); + } + if !same_request(&o.second, f) || !same_request(&o.terminal_attempt, f) { + return Err(format!( + "a later attempt sent a different request: {:?} vs {:?}", + o.second, f + )); + } + if o.caller.outcome != Outcome::Ready || !o.caller.emitted.is_empty() { + return Err(format!( + "the re-pushed caller body was answered {:?}", + o.caller + )); + } + let a = &o.answered; + if a.outcome != Outcome::Ready + || !a.done + || a.status != 200 + || a.emitted != mint_reply(200, MINTED).body + { + return Err(format!( + "attempt 2's answer was not the caller's whole answer, free of attempt 1: {a:?}" + )); + } + let t = &o.terminal; + if t.outcome != Outcome::Ready + || !t.done + || t.status != u32::from(MINT_FAILED_STATUS) + || !String::from_utf8_lossy(&t.emitted) + .contains("client-secret endpoint returned 503 Service Unavailable") + { + return Err(format!("the last attempt's failure was rendered {t:?}")); + } + Ok( + "the mint names the session model's DIRECT route; a failing far end reaches the caller \ + with nothing before its last piece, so the walk may still fail over; each ATTEMPT is \ + answered with the same request afresh, and the next far end's answer is the caller's \ + whole answer; a last attempt that fails is the served plane's 502. Tripping the \ + breaker cell and refusing further dials are the kernel walk's" + .to_string(), + ) +} + +fn route_failover(rig: &Rig, slice: &str) -> bool { + let failover = judge( + slice, + "each attempt is answered afresh and nothing reaches the caller before the answer", + &rig.both(fail_script), + &fail_check, + &|o: &FailObs| { + let mut planted = o.clone(); + planted.second.emitted = b"planted".to_vec(); + planted + }, + ); + let unrouted = judge( + slice, + "a section with no session model names no route", + &rig.both(|p| { + let _ = open(p, b"{}", Some(PUBLIC)); + arrive(p, 0) + }), + &|a: &Arrived| { + if a.outcome == Outcome::Ready && a.route == (ROUTE_POOL, String::new()) { + Ok( + "with no `streams.session.model` the door names no route for the walk to \ + resolve (the door steps refuse an unnamed keyed route)" + .to_string(), + ) + } else { + Err(format!("an unrouted mint arrived as {a:?}")) + } + }, + &|a: &Arrived| Arrived { + route: (ROUTE_DIRECT, MODEL.to_string()), + ..a.clone() + }, + ); + failover && unrouted +} + +// ── audit-record ──────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct AuditObs { + audit_kind: String, + op_classes: Vec, + record_kinds: Vec, + op_of: Vec<(Outcome, u32)>, + records: u32, +} + +fn audit_script(p: &Plugin) -> AuditObs { + let served = p.served(); + let record_kinds = p + .declared() + .record_kinds + .iter() + .map(|k| (*k).to_string()) + .collect(); + let _ = open(p, SESSION, Some(PUBLIC)); + let op_of = (0..5) + .map(|c| { + let a = arrive(p, c); + (a.outcome, a.op_class) + }) + .collect(); + let mut records = 0; + for stream in [80, 81] { + let mut s = Session::open(p, stream, 2); + s.push(FROM_CALLER, 0, &uplink(100)); + s.push(FROM_FAR_END, 0, USAGE_DONE); + s.push(FROM_CALLER, PIECE_LAST, &[]); + records += s.log.records; + } + for x in [ + attempt(82, 0, 1), + request(82, 0, FROM_CALLER, PIECE_LAST, &[]), + Piece { + status: 200, + ..request(82, 0, FROM_FAR_END, PIECE_HAS_STATUS | PIECE_LAST, MINTED) + }, + ] { + records += piece(p, &x).records; + } + records += refusal(p, 83, 0, 403, "refused").records; + AuditObs { + audit_kind: served.audit_kind.to_string(), + op_classes: served.op_classes.iter().map(|c| (*c).to_string()).collect(), + record_kinds, + op_of, + records, + } +} + +fn audit_check(o: &AuditObs) -> Result { + if o.audit_kind != AUDIT_KIND || o.op_classes != [SESSION_OPEN_ACTION] { + return Err(format!( + "the tail states audit kind {} and op classes {:?}", + o.audit_kind, o.op_classes + )); + } + if !o.record_kinds.is_empty() { + return Err(format!( + "the tail declares record kinds {:?}", + o.record_kinds + )); + } + if o.op_of + .iter() + .any(|(outcome, op)| *outcome != Outcome::Ready || *op != 0) + { + return Err(format!( + "a door's unit is not the one session open: {:?}", + o.op_of + )); + } + if o.records != 0 { + return Err(format!( + "the door wrote {} record row(s) of its own beside the kernel's one fixed record", + o.records + )); + } + Ok(format!( + "every door's unit is the one `{SESSION_OPEN_ACTION}` operation under the `{AUDIT_KIND}` \ + audit kind, and the door writes NO row of its own across two whole sessions, a mint and \ + a refusal (it declares no record kind), so the kernel's one fixed record per unit is the \ + only row; writing that row is the kernel's audit step" + )) +} + +fn audit_record(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "the audit kind and operation the kernel's one row is written under", + &rig.both(audit_script), + &audit_check, + &|o: &AuditObs| AuditObs { + records: o.records + 1, + ..o.clone() + }, + ) +} + +// ── exit-terminal ─────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ExitObs { + end: Answer, + after: Vec, + drive: (Outcome, Vec), + done: u32, + interrupted: Answer, +} + +fn exit_script(p: &Plugin) -> ExitObs { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 90, 2); + s.push(FROM_CALLER, 0, &uplink(1000)); + s.push(FROM_FAR_END, 0, USAGE_DONE); + s.push(FROM_CALLER, 0, &uplink(1000)); + let end = s.push(FROM_CALLER, PIECE_LAST, &[]); + let collect = piece(p, &session(90, 2, FROM_KERNEL, 0, &[])); + let far = piece(p, &session(90, 2, FROM_FAR_END, 0, &downlink(96))); + let after = vec![ + format!( + "collect {:?} emitted={}", + collect.outcome, + collect.emitted.len() + ), + format!("far {:?} emitted={}", far.outcome, far.emitted.len()), + ]; + let drive = drive(p); + let mut t = Session::open(p, 91, 2); + let interrupted = t.push(FROM_CALLER, PIECE_LAST, &[]); + ExitObs { + end, + after, + drive, + done: s.log.done + t.log.done, + interrupted, + } +} + +fn exit_check(o: &ExitObs) -> Result { + if o.end.outcome != Outcome::Ready || !o.end.done || o.end.units_line() != usage_units(2) { + return Err(format!( + "the caller's end did not settle the open turn once and end the session: {:?}", + o.end + )); + } + if o.after != ["collect Refused emitted=0", "far Refused emitted=0"] { + return Err(format!("an ended session still answered: {:?}", o.after)); + } + if o.drive != (Outcome::Ready, Vec::new()) { + return Err(format!("drive named an ended session: {:?}", o.drive)); + } + if o.done != 2 { + return Err(format!("{} end(s) across two sessions", o.done)); + } + let i = &o.interrupted; + if i.outcome != Outcome::Ready || !i.done || !i.units.is_empty() { + return Err(format!( + "a session ended before its first frame answered {i:?}" + )); + } + Ok( + "a session ends ONCE: the caller's end settles the open turn's audio seconds once \ + (cumulative units final), carries the end exactly once, and afterwards the stream is \ + refused on every side and drive names nothing; a session torn down before its first \ + frame ends with no units and no fee. Sealing the session's one line is the kernel's \ + exit step" + .to_string(), + ) +} + +fn exit_terminal(rig: &Rig, slice: &str) -> bool { + judge( + slice, + "one session, one end", + &rig.both(exit_script), + &exit_check, + &|o: &ExitObs| ExitObs { + done: o.done + 1, + ..o.clone() + }, + ) +} + +// ── tool-reply ────────────────────────────────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct ToolObs { + log: Log, + /// Frames emitted once the call was announced. + announced: usize, + /// Frames emitted once the turn's usage arrived. + usage_at: usize, + /// The cumulative units once the turn closed. + units: Vec<(u32, u32, u64)>, +} + +struct ToolWire { + dialect: Dialect, + announce: Vec>, + usage: &'static [u8], + reply: Vec, +} + +fn tool_wire(dialect: Dialect) -> ToolWire { + match dialect { + Dialect::OpenAi => ToolWire { + dialect, + announce: vec![ + frame(&json!({"type": "response.output_item.added", + "item": {"type": "function_call", "call_id": "call_x", "name": "lookup"}})), + frame(&json!({"type": "response.function_call_arguments.delta", + "call_id": "call_x", "delta": "{\"q\":1}"})), + frame(&json!({"type": "response.function_call_arguments.done", + "call_id": "call_x", "name": "lookup", "arguments": "{\"q\":1}"})), + ], + usage: USAGE_DONE, + reply: frame(&json!({"type": "conversation.item.create", + "item": {"type": "function_call_output", "call_id": "call_x", + "output": "{\"ok\":true}"}})), + }, + Dialect::Gemini => ToolWire { + dialect, + announce: vec![frame(&json!({"toolCall": {"functionCalls": [ + {"id": "call_x", "name": "lookup", "args": {"q": 1}}]}}))], + usage: GEMINI_USAGE, + reply: frame(&json!({"toolResponse": {"functionResponses": [ + {"id": "call_x", "name": "lookup", "response": {"ok": true}}]}})), + }, + } +} + +fn tool_script(p: &Plugin, w: &ToolWire) -> ToolObs { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 95, w.dialect.claim()); + for f in &w.announce { + s.push(FROM_FAR_END, 0, f); + } + let announced = s.emitted(); + s.push(FROM_FAR_END, 0, w.usage); + let usage_at = s.emitted(); + let units = s.log.units.clone(); + s.push(FROM_CALLER, 0, &w.reply); + ToolObs { + log: s.log, + announced, + usage_at, + units, + } +} + +fn phase(log: &Log, from: usize, to: usize) -> Log { + Log { + emissions: log.emissions.get(from..to).unwrap_or_default().to_vec(), + ..Log::default() + } +} + +fn tool_check(dialect: Dialect, o: &ToolObs) -> Result { + let name = dialect.name(); + if !o.log.anomalies.is_empty() { + return Err(format!( + "{name}: not every piece READY: {:?}", + o.log.anomalies + )); + } + let announce = phase(&o.log, 0, o.announced); + if announce.emissions.iter().any(|e| e.to_far) { + return Err(format!( + "{name}: the door sent the far end something for a call it was only to relay (the \ + gateway authors no result): {:?}", + announce.emissions + )); + } + let call = vec![ + Norm::ToolOpen("call_x".to_string(), "lookup".to_string()), + Norm::ToolArgs("call_x".to_string(), json!({"q": 1})), + Norm::ToolClose("call_x".to_string()), + ]; + let relayed = caller_norms(dialect, &announce); + if fingerprint(&relayed) != fingerprint(&call) { + return Err(format!( + "{name}: the call reached the caller as {relayed:?}" + )); + } + if o.usage_at != o.announced { + return Err(format!( + "{name}: the turn's usage was relayed instead of reported" + )); + } + if !o.units.contains(&(5, 1, 1)) { + return Err(format!( + "{name}: the call was not counted once: {:?}", + o.units + )); + } + let reply = phase(&o.log, o.usage_at, o.log.emissions.len()); + let carried = far_norms(dialect, &reply); + let ok = reply.emissions.len() == 1 + && reply.emissions[0].to_far + && matches!(carried.as_slice(), [Norm::ToolResult(id, out)] if id == "call_x" && *out == json!({"ok": true})); + if !ok { + return Err(format!( + "{name}: the caller's own result reached the far end as {carried:?} ({} frame(s))", + reply.emissions.len() + )); + } + Ok(format!( + "{name}: the far end's call reached the caller as it was made and the door sent the far \ + end nothing for it; the call counted once; the caller's own result reached the far end \ + alone (no door-authored response)" + )) +} + +fn tool_reply(rig: &Rig, slice: &str) -> bool { + let mut pass = true; + for dialect in [Dialect::OpenAi, Dialect::Gemini] { + let w = tool_wire(dialect); + pass &= judge( + slice, + &format!("{} tool call relayed, never answered", dialect.name()), + &rig.both(|p| tool_script(p, &w)), + &|o: &ToolObs| tool_check(dialect, o), + &|o: &ToolObs| { + let mut planted = o.clone(); + planted.log.emissions.insert( + 0, + Emission { + to_far: true, + frame: w.reply.clone(), + need: dialect.ride(), + verb: "GET".to_string(), + target: dialect.socket().to_string(), + }, + ); + planted.announced += 1; + planted.usage_at += 1; + planted + }, + ); + } + pass +} diff --git a/testing/voice-conformance/harness/src/door.rs b/testing/voice-conformance/harness/src/door.rs new file mode 100644 index 0000000000..8666f486ef --- /dev/null +++ b/testing/voice-conformance/harness/src/door.rs @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE DOOR, BOTH WAYS: the plumbing every leg drives the streaming plane through. +//! +//! The same door is loaded twice through the ONE loader: compiled in +//! (`busbar_plane_streaming::door::door`, [`load_linked`]) and dropped in (the `streaming_door` +//! example `cdylib` beside this harness, [`load_dropped`] against the linked row's own Statement). +//! Every leg builds a fresh instance of each, drives both with one script, and [`judge`]s the pair: +//! the two observations must be equal, the observation must pass the leg's check, and the leg's +//! PLANTED WRONG ANSWER must be refused by the same check (the RED arm, run in the same process on +//! every assertion). A dropped door that is not built or does not load is a FAIL, never a skip. + +use std::cell::Cell; +use std::fmt::Debug; +use std::path::PathBuf; +use std::sync::Arc; + +use busbar_contract::abi::mechanism::call::{AbiStr, Blob, Outcome, Span, BLOB_JSON, BLOB_OCTETS}; +use busbar_contract::abi::mechanism::lifecycle::{slot as life, TickIn, TickOut}; +use busbar_contract::abi::plane::{ + slot, ArriveIn, ArriveOut, OnPieceIn, OnPieceOut, OutField, PlaneDriveIn, PlaneDriveOut, + PlaneOpenIn, PlaneOpenOut, RecordWrite, RefusalIn, RefusalOut, UnitCount, CLAIM_EXACT, + CLAIM_OPEN, EMIT_DONE, EMIT_TO_FAR_END, FROM_CALLER, FROM_KERNEL, REFUSAL_KERNEL, +}; +use busbar_plane_streaming::door; +use busbar_plugin_loader::dispatch::kinds::plane::Plane; +use busbar_plugin_loader::dispatch::{ + in_head, load_dropped, load_linked, out_head, Bind, DispatchConfig, Dispatcher, Frame, + LinkedRow, NoSink, Plugin, +}; + +/// The environment variable naming the dropped-in door's library, when it is not beside the +/// harness. +pub const DOOR_ENV: &str = "VOICE_CONFORM_DOOR"; + +/// The environment variable that turns every planted wrong answer ON as the subject: each leg must +/// then go RED (the RED arm, observable from the runner). +pub const RED_ENV: &str = "VOICE_CONFORM_RED"; + +/// The deployment's public base URL every leg opens the door under. +pub const PUBLIC: &str = "https://gw.example.com/ignored?q=1#f"; + +/// The `streams:` section every leg opens under: the session params a session locks. +pub const SESSION: &[u8] = br#"{"session":{"model":"m-cap"}}"#; + +/// The model the section names: the DIRECT route a reaching door names. +pub const MODEL: &str = "m-cap"; + +/// The kernel's opaque reference for the caller (never the principal, never a credential). +pub const CALLER_REF: &str = "c0ffee"; + +/// A value all of whose bytes are zero. +pub fn z() -> T { + // SAFETY: called only for the plane ABI's plain C structs (`in`/`out` frames, `UnitCount`, + // `OutField`, `RecordWrite`): integers, raw pointers and spans, for which all-zero is valid. + unsafe { std::mem::zeroed() } +} + +/// `n` zeroed buffer elements. +fn zeros(n: usize) -> Vec { + (0..n).map(|_| z()).collect() +} + +/// A JSON blob over static bytes. +pub fn json(b: &'static [u8]) -> Blob { + Blob { + ptr: b.as_ptr(), + len: b.len(), + fmt: BLOB_JSON, + flags: 0, + } +} + +/// An octet blob over `b`. +fn octets(b: &[u8]) -> Blob { + Blob { + ptr: b.as_ptr(), + len: b.len(), + fmt: BLOB_OCTETS, + flags: 0, + } +} + +/// A static string, as the ABI lends it. +pub fn text(s: &'static str) -> AbiStr { + AbiStr { + ptr: s.as_ptr(), + len: s.len(), + } +} + +/// A string the plugin lent, copied. +pub fn read(s: AbiStr) -> String { + if s.ptr.is_null() || s.len == 0 { + return String::new(); + } + // SAFETY: the plugin's text, valid while its generation is live (read before the next call). + String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(s.ptr, s.len) }).into_owned() +} + +/// The bytes a span names in `buf`; `` for a span past it. +fn at(buf: &[u8], s: Span) -> String { + let from = s.offset as usize; + buf.get(from..from + s.len as usize).map_or_else( + || "".to_string(), + |b| String::from_utf8_lossy(b).into_owned(), + ) +} + +/// Keep a line printable: one line, bounded. +pub fn clip(s: &str) -> String { + let one: String = s.chars().map(|c| if c == '\n' { ' ' } else { c }).collect(); + if one.chars().count() > 360 { + let head: String = one.chars().take(360).collect(); + format!("{head}…") + } else { + one + } +} + +/// Whether the planted wrong answers are the subject (`VOICE_CONFORM_RED` set). +pub fn red_mode() -> bool { + std::env::var_os(RED_ENV).is_some() +} + +// ── the two doors ─────────────────────────────────────────────────────────────────────────────── + +/// The harness's one dispatcher and the two ways it reaches the door. +pub struct Rig { + dispatcher: Dispatcher, + count: Cell, + dropped_at: Result, + stated: Vec, +} + +/// Where the dropped-in door is: `VOICE_CONFORM_DOOR`, else the plane's `streaming_door` example +/// library in the profile's `examples/` beside this harness's binary (`cargo build -p +/// busbar-plane-streaming --example streaming_door` puts it there). +fn dropped_path() -> Result { + if let Some(p) = std::env::var_os(DOOR_ENV) { + let p = PathBuf::from(p); + return if p.is_file() { + Ok(p) + } else { + Err(format!("{DOOR_ENV}={} names no file", p.display())) + }; + } + let exe = std::env::current_exe().map_err(|e| format!("the harness has no path: {e}"))?; + let Some(dir) = exe.parent() else { + return Err("the harness's path has no directory".to_string()); + }; + let name = format!( + "{}streaming_door{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + ); + let path = dir.join("examples").join(name); + if path.is_file() { + Ok(path) + } else { + Err(format!( + "the dropped-in door is not built beside the harness ({}): build the \ + `streaming_door` example", + path.display() + )) + } +} + +impl Rig { + /// The dispatcher, the linked row's Statement, and where the dropped-in door lives. + pub fn boot() -> Self { + let row = LinkedRow::of(door::door).expect("the streaming door states itself"); + Rig { + dispatcher: Dispatcher::new(DispatchConfig::default()), + count: Cell::new(0), + dropped_at: dropped_path(), + stated: row.statement, + } + } + + /// The bind for one fresh instance (two instances never share a label). + fn bind(&self, leg: &str) -> Bind { + let n = self.count.get() + 1; + self.count.set(n); + Bind { + instance: Arc::from(format!("{leg}-{n}")), + max_inflight_cap: 8, + sink: Arc::new(NoSink), + dispatcher: self.dispatcher.adopter(), + // These rows drive the door's slots and never dial: its needs are not declared, bound + // as a probe. + conns: busbar_plugin_loader::dispatch::ConnTable::Probe, + } + } + + /// A fresh instance of the LINKED door. + pub fn linked(&self) -> Plugin { + let row = LinkedRow::of(door::door).expect("the streaming door states itself"); + load_linked::(&row, self.bind("linked")).expect("the linked streaming door loads") + } + + /// A fresh instance of the DROPPED door, admitted against the Statement the linked row states. + pub fn dropped(&self) -> Result, String> { + let path = match &self.dropped_at { + Ok(path) => path, + Err(e) => return Err(e.clone()), + }; + load_dropped::(path, &self.stated, self.bind("dropped")).map_err(|e| { + format!( + "the dropped-in door at {} did not load: {e:?}", + path.display() + ) + }) + } + + /// `script` run on a fresh instance of each door. + pub fn both(&self, script: impl Fn(&Plugin) -> T) -> (T, Result) { + let linked = script(&self.linked()); + let dropped = self.dropped().map(|p| script(&p)); + (linked, dropped) + } +} + +// ── the verdict ───────────────────────────────────────────────────────────────────────────────── + +/// One `RESULT` line. +pub fn result(slice: &str, pass: bool, what: &str, detail: &str) { + let verdict = if pass { "PASS" } else { "FAIL" }; + println!("RESULT {slice} {verdict} {what} — {}", clip(detail)); +} + +/// A leg's check over one observation: `Ok(what it proved)` or `Err(what is wrong)`. +pub type Check<'a, T> = &'a dyn Fn(&T) -> Result; + +/// JUDGE ONE ASSERTION over both doors. PASS only when (1) the dropped door loaded and answered +/// exactly as the linked door did, (2) the linked door's observation passes `check`, and (3) the +/// RED ARM bit: `plant` (the leg's wrong door, restated as the wrong answer it would give) changes +/// the observation and `check` refuses it. Under [`RED_ENV`] the planted answer IS the subject, so +/// the line must come out FAIL. Prints the line; answers whether it passed. +pub fn judge( + slice: &str, + what: &str, + seen: &(T, Result), + check: Check<'_, T>, + plant: &dyn Fn(&T) -> T, +) -> bool { + let (linked, dropped) = seen; + let subject = if red_mode() { + plant(linked) + } else { + linked.clone() + }; + let mut wrong = Vec::new(); + match dropped { + Ok(dropped) if *dropped == subject => {} + Ok(dropped) => wrong.push(format!( + "the linked and dropped doors answered differently: {} vs {}", + clip(&format!("{subject:?}")), + clip(&format!("{dropped:?}")) + )), + Err(e) => wrong.push(e.clone()), + } + let proved = match check(&subject) { + Ok(proved) => proved, + Err(e) => { + wrong.push(e); + String::new() + } + }; + if !red_mode() { + let planted = plant(linked); + if planted == *linked { + wrong.push("RED arm: the planted wrong answer changed nothing".to_string()); + } else if let Ok(accepted) = check(&planted) { + wrong.push(format!( + "RED arm: the check accepted a planted wrong answer ({accepted})" + )); + } + } + if wrong.is_empty() { + result( + slice, + true, + what, + &format!("{proved}; linked == dropped; planted wrong answer refused"), + ); + true + } else { + result(slice, false, what, &wrong.join("; ")); + false + } +} + +// ── the lifecycle ─────────────────────────────────────────────────────────────────────────────── + +/// One claim a snapshot publishes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Claimed { + /// `VERB TARGET CARRIER[ exact][ open]`. + pub line: String, + /// The dialect a refusal on it is rendered in. + pub refusal_dialect: u16, +} + +/// What an `open` answered: its outcome, and the snapshot it published. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Opened { + /// The outcome. + pub outcome: Outcome, + /// The published claims. + pub claims: Vec, + /// The audience a caller's token must carry. + pub audience: String, +} + +/// `open` generation 1 over `settings`, under `public_url`. +pub fn open( + p: &Plugin, + settings: &'static [u8], + public_url: Option<&'static str>, +) -> Opened { + let mut i: PlaneOpenIn = z(); + i.open.head = in_head(); + i.open.generation = 1; + i.open.settings = json(settings); + if let Some(u) = public_url { + i.public_url = text(u); + } + let mut o: PlaneOpenOut = z(); + o.open.head = out_head(); + let mut f = Frame::new(i, o); + let c = p.call(life::OPEN, &mut f); + let mut opened = Opened { + outcome: c.outcome, + claims: Vec::new(), + audience: String::new(), + }; + let snap = f.out.snapshot; + if c.outcome != Outcome::Ready || snap.is_null() { + return opened; + } + // SAFETY: a READY open publishes its generation's snapshot, valid until that generation's + // `retire` (never called here). + let s = unsafe { &*snap }; + for n in 0..s.claims_len { + // SAFETY: the snapshot names `claims_len` claims. + let c = unsafe { &*s.claims.add(n) }; + let exact = if c.flags & CLAIM_EXACT != 0 { + " exact" + } else { + "" + }; + let open = if c.flags & CLAIM_OPEN != 0 { + " open" + } else { + "" + }; + opened.claims.push(Claimed { + line: format!( + "{} {} {}{exact}{open}", + read(c.verb), + read(c.target), + read(c.carrier) + ), + refusal_dialect: c.refusal_dialect, + }); + } + opened.audience = read(s.audience); + opened +} + +/// What `arrive` answered for a claim. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Arrived { + /// The outcome. + pub outcome: Outcome, + /// The dialect index. + pub dialect: u32, + /// The op class index. + pub op_class: u32, + /// The principal need. + pub principal: u32, + /// The route class and the entry it names. + pub route: (u8, String), + /// The plane's refusal code and status. + pub refusal: (u32, u32), + /// The units the admission was told to expect. + pub units: u32, +} + +/// `arrive` on `claim`. +pub fn arrive(p: &Plugin, claim: u32) -> Arrived { + let mut units: Vec = zeros(8); + let mut a: Frame = Frame::new(z(), z()); + a.input.head = in_head(); + a.input.claim = claim; + a.input.unit = 900 + u64::from(claim); + a.input.units_buf = units.as_mut_ptr(); + a.input.units_cap = units.len(); + a.out.head = out_head(); + let c = p.call(slot::ARRIVE, &mut a); + Arrived { + outcome: c.outcome, + dialect: a.out.dialect, + op_class: a.out.op_class, + principal: a.out.principal_need, + route: (a.out.route, read(a.out.pool)), + refusal: (a.out.refusal, a.out.refusal_status), + units: a.out.units_written, + } +} + +/// What `refusal` rendered. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Rendered { + /// The outcome. + pub outcome: Outcome, + /// The rendered body. + pub body: String, + /// Its head fields. + pub fields: Vec<(String, String)>, + /// The status it answered (`0` = the kernel's). + pub status: u32, + /// The record writes it made. + pub records: u32, +} + +/// The kernel refuses unit `unit` on `dialect` with `status` and `words`: the door renders it. +pub fn refusal( + p: &Plugin, + unit: u64, + dialect: u32, + status: u32, + words: &'static str, +) -> Rendered { + let mut reply = vec![0u8; 4096]; + let mut fields: Vec = zeros(4); + let mut arena = vec![0u8; 512]; + let mut records: Vec = zeros(4); + let mut i: RefusalIn = z(); + i.head = in_head(); + i.cause = REFUSAL_KERNEL; + i.status = status; + i.dialect = dialect; + i.text = text(words); + i.unit = unit; + (i.reply_buf, i.reply_cap) = (reply.as_mut_ptr(), reply.len()); + (i.fields_buf, i.fields_cap) = (fields.as_mut_ptr(), fields.len()); + (i.arena_buf, i.arena_cap) = (arena.as_mut_ptr(), arena.len()); + (i.records_buf, i.records_cap) = (records.as_mut_ptr(), records.len()); + let mut o: RefusalOut = z(); + o.head = out_head(); + let mut f = Frame::new(i, o); + let c = p.call(slot::REFUSAL, &mut f); + let out = f.out; + let written = (out.reply_written as usize).min(reply.len()); + let n = (out.fields_written as usize).min(fields.len()); + Rendered { + outcome: c.outcome, + body: String::from_utf8_lossy(&reply[..written]).into_owned(), + fields: fields[..n] + .iter() + .map(|f| (at(&arena, f.name), at(&arena, f.value))) + .collect(), + status: out.status, + records: out.records_written, + } +} + +/// `drive`: the streams whose sessions owe output of their own. +pub fn drive(p: &Plugin) -> (Outcome, Vec) { + let mut sessions = [0_u64; 16]; + let mut f: Frame = Frame::new(z(), z()); + f.input.drive.head = in_head(); + (f.input.sessions_buf, f.input.sessions_cap) = (sessions.as_mut_ptr(), sessions.len()); + f.out.head = out_head(); + let c = p.call(life::DRIVE, &mut f); + let n = (f.out.sessions_written as usize).min(sessions.len()); + (c.outcome, sessions[..n].to_vec()) +} + +/// `tick` at `now_ns`: its outcome and the next tick it asks for. +pub fn tick(p: &Plugin, now_ns: u64) -> (Outcome, u64) { + let mut k: Frame = Frame::new(z(), z()); + k.input.head = in_head(); + k.input.now_ns = now_ns; + k.out.head = out_head(); + let c = p.call(life::TICK, &mut k); + (c.outcome, k.out.next_tick_ns) +} + +// ── a piece and its answer ────────────────────────────────────────────────────────────────────── + +/// One piece pushed to a unit. +#[derive(Clone, Copy, Debug)] +pub struct Piece<'a> { + /// The kernel's unit key. + pub unit: u64, + /// A live session's stream (`0` = a request unit). + pub stream: u64, + /// The claim it arrived on. + pub claim: u32, + /// Who pushed it. + pub from: u32, + /// `PIECE_*`. + pub flags: u32, + /// An ATTEMPT's number (`0` = none). + pub attempt_no: u32, + /// Its bytes. + pub bytes: &'a [u8], + /// The far end's status, with `PIECE_HAS_STATUS`. + pub status: u32, + /// The capacity of the units buffer the host lends. + pub units_cap: usize, +} + +/// A piece of request unit `unit` on `claim`. +pub const fn request(unit: u64, claim: u32, from: u32, flags: u32, bytes: &[u8]) -> Piece<'_> { + Piece { + unit, + stream: 0, + claim, + from, + flags, + attempt_no: 0, + bytes, + status: 0, + units_cap: 16, + } +} + +/// The ATTEMPT that opens attempt `n` of request unit `unit` on `claim`. +pub const fn attempt(unit: u64, claim: u32, n: u32) -> Piece<'static> { + Piece { + attempt_no: n, + ..request(unit, claim, FROM_KERNEL, 0, &[]) + } +} + +/// A piece of the live session on stream `stream` (claim `claim`). +pub const fn session(stream: u64, claim: u32, from: u32, flags: u32, bytes: &[u8]) -> Piece<'_> { + Piece { + stream, + ..request(stream, claim, from, flags, bytes) + } +} + +/// What the door answered one piece with. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Answer { + /// The outcome. + pub outcome: Outcome, + /// The bytes it emitted. + pub emitted: Vec, + /// `more`. + pub more: u32, + /// The emission is bound for the far end. + pub to_far: bool, + /// The unit (or session) is done. + pub done: bool, + /// The caller's status. + pub status: u32, + /// The far request's verb and target. + pub verb: String, + /// See `verb`. + pub target: String, + /// The head fields it wrote. + pub fields: Vec<(String, String)>, + /// The units it reported: (class, source, amount). + pub units: Vec<(u32, u32, u64)>, + /// A short answer's units room. + pub units_needed: u32, + /// The declared need a far request rides (`0` = none). + pub need: u32, + /// The record writes it made. + pub records: u32, +} + +impl Answer { + /// The units as `class:source=amount` words. + pub fn units_line(&self) -> String { + if self.units_needed != 0 { + return format!("needed {}", self.units_needed); + } + self.units + .iter() + .map(|(c, s, a)| format!("{c}:{s}={a}")) + .collect::>() + .join(" ") + } +} + +/// Push `x` and read the answer. +pub fn piece(p: &Plugin, x: &Piece<'_>) -> Answer { + let mut reply = vec![0u8; 1 << 18]; + let mut units: Vec = zeros(x.units_cap); + let mut records: Vec = zeros(4); + let mut fields: Vec = zeros(8); + let mut arena = vec![0u8; 4096]; + let mut i: OnPieceIn = z(); + i.head = in_head(); + (i.unit, i.stream, i.claim, i.from, i.flags) = (x.unit, x.stream, x.claim, x.from, x.flags); + (i.attempt_no, i.bytes, i.status_code) = (x.attempt_no, octets(x.bytes), x.status); + i.caller_ref = text(CALLER_REF); + (i.reply_buf, i.reply_cap) = (reply.as_mut_ptr(), reply.len()); + (i.units_buf, i.units_cap) = (units.as_mut_ptr(), units.len()); + (i.records_buf, i.records_cap) = (records.as_mut_ptr(), records.len()); + (i.fields_buf, i.fields_cap) = (fields.as_mut_ptr(), fields.len()); + (i.arena_buf, i.arena_cap) = (arena.as_mut_ptr(), arena.len()); + let mut o: OnPieceOut = z(); + o.head = out_head(); + let mut f = Frame::new(i, o); + let c = p.call(slot::ON_PIECE, &mut f); + let o = f.out; + let emitted = (o.emitted as usize).min(reply.len()); + let nu = (o.units_written as usize).min(units.len()); + let nf = (o.fields_written as usize).min(fields.len()); + Answer { + outcome: c.outcome, + emitted: reply[..emitted].to_vec(), + more: o.more, + to_far: o.flags & EMIT_TO_FAR_END != 0, + done: o.flags & EMIT_DONE != 0, + status: o.reply_status, + verb: at(&arena, o.verb), + target: at(&arena, o.target), + fields: fields[..nf] + .iter() + .map(|f| (at(&arena, f.name), at(&arena, f.value))) + .collect(), + units: units[..nu] + .iter() + .map(|u| (u.class, u.source, u.amount)) + .collect(), + units_needed: o.units_needed, + need: o.need, + records: o.records_written, + } +} + +// ── one live session ──────────────────────────────────────────────────────────────────────────── + +/// One frame the door emitted on a session. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Emission { + /// Bound for the far end (else the caller). + pub to_far: bool, + /// The frame. + pub frame: Vec, + /// The declared need a far frame rides. + pub need: u32, + /// A far frame's verb and target. + pub verb: String, + /// See `verb`. + pub target: String, +} + +/// Everything one session showed: every answer that was not READY (or owed `more`), every frame +/// in the order it was emitted, the last cumulative units, the record writes and how many answers +/// ended it. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Log { + /// `what Outcome` per answer that was not a plain READY. + pub anomalies: Vec, + /// Every frame, in emission order. + pub emissions: Vec, + /// The last READY answer's cumulative units. + pub units: Vec<(u32, u32, u64)>, + /// Record writes across every answer. + pub records: u32, + /// Answers that carried the session's end. + pub done: u32, +} + +/// A live session on one door instance. +pub struct Session<'p> { + p: &'p Plugin, + stream: u64, + claim: u32, + /// What it showed so far. + pub log: Log, +} + +impl<'p> Session<'p> { + /// The session on `stream` (door `claim`), opened by the caller's first (empty) piece. + pub fn open(p: &'p Plugin, stream: u64, claim: u32) -> Self { + let mut s = Session { + p, + stream, + claim, + log: Log::default(), + }; + let a = piece(p, &session(stream, claim, FROM_CALLER, 0, &[])); + s.note("open", &a); + s + } + + fn note(&mut self, what: &str, a: &Answer) { + if a.outcome != Outcome::Ready || a.more != 0 { + self.log + .anomalies + .push(format!("{what} {:?} more={}", a.outcome, a.more)); + } + if !a.emitted.is_empty() { + self.log.emissions.push(Emission { + to_far: a.to_far, + frame: a.emitted.clone(), + need: a.need, + verb: a.verb.clone(), + target: a.target.clone(), + }); + } + if a.outcome == Outcome::Ready { + self.log.units.clone_from(&a.units); + } + self.log.records += a.records; + if a.done { + self.log.done += 1; + } + } + + /// Push `bytes` from `from` (with `flags`), then collect every frame the session still owes. + /// Answers the piece's own answer. + pub fn push(&mut self, from: u32, flags: u32, bytes: &[u8]) -> Answer { + let a = piece( + self.p, + &session(self.stream, self.claim, from, flags, bytes), + ); + self.note("piece", &a); + if a.outcome == Outcome::Ready && !a.done { + self.collect(); + } + a + } + + /// Collect (`FROM_KERNEL`, nothing, no attempt) until the session owes nothing. + pub fn collect(&mut self) { + for _ in 0..64 { + let a = piece( + self.p, + &session(self.stream, self.claim, FROM_KERNEL, 0, &[]), + ); + let quiet = a.emitted.is_empty() && !a.done; + self.note("collect", &a); + if quiet || a.done || a.outcome != Outcome::Ready { + return; + } + } + self.log.anomalies.push("collect never ran dry".to_string()); + } + + /// The frames emitted so far. + pub fn emitted(&self) -> usize { + self.log.emissions.len() + } +} diff --git a/testing/voice-conformance/harness/src/governance.rs b/testing/voice-conformance/harness/src/governance.rs new file mode 100644 index 0000000000..c34b059a8c --- /dev/null +++ b/testing/voice-conformance/harness/src/governance.rs @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE FIVE VISION CHECKPOINTS, AT THE DOOR — observations, never a conformance result (the runner +//! keeps governance out of the conformance tally). Each is probed on both doors and judged like +//! any leg; the process always exits 0. + +use busbar_contract::abi::mechanism::call::Outcome; +use busbar_contract::abi::plane::{FROM_CALLER, FROM_FAR_END, PIECE_LAST}; +use busbar_plane_streaming::session_door::CEILING_TICK_NS; +use busbar_plane_streaming::session_pump::SESSION_CEILING_REASON; +use busbar_plugin_loader::dispatch::kinds::plane::Plane; +use busbar_plugin_loader::dispatch::Plugin; +use serde_json::json; + +use crate::codec_legs::{caller_norms, downscope_bridge, far_norms, frame, Dialect, Norm}; +use crate::composition::{downlink, uplink, usage_units, USAGE_DONE}; +use crate::door::{ + drive, judge, open, piece, refusal, result, session, tick, Emission, Log, Piece, Rendered, Rig, + Session, PUBLIC, SESSION, +}; + +/// `governance `: one observation; always exit 0. +pub fn run(rig: &Rig, checkpoint: &str) -> i32 { + match checkpoint { + "V1-barge-in-preemption" => barge_in(rig, checkpoint), + "V2-turn-budget-enforcement" => ceiling(rig, checkpoint), + "V3-metering-lease-settled" => settled_once(rig, checkpoint), + "V4-dialect-downscope" => downscope(rig, checkpoint), + "D2-hard-close-on-exhaustion" => hard_close(rig, checkpoint), + other => result(other, false, "checkpoint", "unknown checkpoint"), + } + 0 +} + +// ── V1: a barge-in preempts the turn ──────────────────────────────────────────────────────────── + +fn barge_script(p: &Plugin) -> Log { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 100, 2); + // 96 bytes of 24 kHz PCM16: the caller has heard 2 ms of the far end's answer. + s.push(FROM_FAR_END, 0, &downlink(96)); + s.push( + FROM_FAR_END, + 0, + &frame(&json!({"type": "input_audio_buffer.speech_started", + "audio_start_ms": 0, "item_id": "it1"})), + ); + s.log +} + +fn barge_check(log: &Log) -> Result { + let far = far_norms(Dialect::OpenAi, log); + let near = caller_norms(Dialect::OpenAi, log); + if !log.anomalies.is_empty() { + return Err(format!("not every piece READY: {:?}", log.anomalies)); + } + if far != [Norm::ResponseCancel, Norm::Truncate(2)] { + return Err(format!("the barge-in sent the far end {far:?}")); + } + if near != [Norm::AudioDown(vec![0; 96]), Norm::SpeechStart] { + return Err(format!("the caller was sent {near:?}")); + } + Ok( + "the far end's speech_started preempts the turn: the door cancels the response and \ + truncates the item at the 2 ms the caller heard, and tells the caller" + .to_string(), + ) +} + +fn barge_in(rig: &Rig, checkpoint: &str) { + judge( + checkpoint, + "barge-in", + &rig.both(barge_script), + &barge_check, + &|l: &Log| { + let mut planted = l.clone(); + if let Some(i) = planted.emissions.iter().rposition(|e| e.to_far) { + planted.emissions.remove(i); + } + planted + }, + ); +} + +// ── V2: a session is bounded ──────────────────────────────────────────────────────────────────── + +const CEILING: &[u8] = br#"{"session":{"model":"m-cap"},"session_max_secs":1}"#; + +#[derive(Clone, Debug, PartialEq, Eq)] +struct CeilingObs { + next: u64, + owed: Vec, + log: Log, +} + +fn ceiling_script(p: &Plugin) -> CeilingObs { + let _ = open(p, CEILING, Some(PUBLIC)); + let mut s = Session::open(p, 110, 2); + let (_, next) = tick(p, 1_000); + let _ = tick(p, 1_000 + 1_000_000_000); + let (_, owed) = drive(p); + s.collect(); + CeilingObs { + next, + owed, + log: s.log, + } +} + +fn ceiling_check(o: &CeilingObs) -> Result { + if o.next != 1_000 + CEILING_TICK_NS { + return Err(format!( + "a ceilinged generation asked for its next tick at {}", + o.next + )); + } + if o.owed != [110] { + return Err(format!("drive named {:?} once the ceiling passed", o.owed)); + } + let told = caller_norms(Dialect::OpenAi, &o.log); + let said = matches!(told.as_slice(), [Norm::Error(code, _)] if code == SESSION_CEILING_REASON); + if !o.log.anomalies.is_empty() || !said || o.log.done != 1 { + return Err(format!( + "past its ceiling the session told the caller {told:?} and ended {} time(s)", + o.log.done + )); + } + Ok(format!( + "a session past `streams.session_max_secs` is told why ({SESSION_CEILING_REASON}) and \ + ended once, on the kernel's tick clock; a money cut is the kernel's, rendered through \ + `refusal`" + )) +} + +fn ceiling(rig: &Rig, checkpoint: &str) { + judge( + checkpoint, + "the wall-clock ceiling", + &rig.both(ceiling_script), + &ceiling_check, + &|o: &CeilingObs| { + let mut planted = o.clone(); + planted.log.done = 0; + planted + }, + ); +} + +// ── V3: a turn's units settle once ────────────────────────────────────────────────────────────── + +/// The three answers of a turn whose report is re-called once, as lines. +#[derive(Clone, Debug, PartialEq, Eq)] +struct Settled { + lines: Vec, +} + +fn settle_script(p: &Plugin) -> Settled { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 120, 2); + s.push(FROM_CALLER, 0, &uplink(1000)); + let narrow = Piece { + units_cap: 2, + ..session(120, 2, FROM_FAR_END, 0, USAGE_DONE) + }; + let a = piece(p, &narrow); + let b = piece(p, &session(120, 2, FROM_FAR_END, 0, USAGE_DONE)); + let c = piece(p, &session(120, 2, FROM_CALLER, PIECE_LAST, &[])); + Settled { + lines: vec![ + format!("narrow {:?} units=[{}]", a.outcome, a.units_line()), + format!("re-call {:?} units=[{}]", b.outcome, b.units_line()), + format!( + "end {:?} done={} units=[{}]", + c.outcome, + c.done, + c.units_line() + ), + ], + } +} + +fn settle_check(o: &Settled) -> Result { + let lines = &o.lines; + let want = [ + "narrow Failed units=[needed 6]".to_string(), + format!("re-call Ready units=[{}]", usage_units(1)), + format!("end Ready done=true units=[{}]", usage_units(1)), + ]; + if *lines != want { + return Err(format!("the turn settled as {lines:?}; want {want:?}")); + } + Ok( + "a turn's usage settles exactly once: the re-call of a short answer reports the same \ + cumulative units, and the end adds nothing already settled" + .to_string(), + ) +} + +fn settled_once(rig: &Rig, checkpoint: &str) { + judge( + checkpoint, + "units settle once", + &rig.both(settle_script), + &settle_check, + &|o: &Settled| { + let mut planted = o.clone(); + planted.lines[1] = planted.lines[1].replace("0:1=10", "0:1=20"); + planted + }, + ); +} + +// ── V4: crossing a dialect down-scopes ────────────────────────────────────────────────────────── + +fn widened(text: &str) -> bool { + text.contains("semantic") || text.contains("g711") +} + +fn downscope_script(p: &Plugin) -> Log { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 140, 2); + s.push(FROM_CALLER, 0, &frame(&downscope_update())); + s.log +} + +fn downscope_update() -> serde_json::Value { + json!({"type": "session.update", "session": {"instructions": "x", + "turn_detection": {"type": "semantic_vad", "eagerness": "high"}, + "input_audio_format": "g711_ulaw", "output_audio_format": "g711_ulaw"}}) +} + +fn downscope_check(log: &Log) -> Result { + let bridged = downscope_bridge(&downscope_update()); + if !bridged.contains("Config") || widened(&bridged) { + return Err(format!("the bridge toward Gemini widened: {bridged}")); + } + let far: Vec<&Emission> = log.emissions.iter().filter(|e| e.to_far).collect(); + if !log.anomalies.is_empty() + || far.len() != 1 + || far + .iter() + .any(|e| widened(&String::from_utf8_lossy(&e.frame))) + || !far_norms(Dialect::OpenAi, log) + .iter() + .any(|n| matches!(n, Norm::Config(_))) + { + return Err(format!( + "the caller's session.update reached the far end as {far:?}" + )); + } + Ok( + "an OpenAI-only semantic_vad/g711 concept is down-scoped, never widened, toward Gemini; \ + and at the door the caller's session.update never reaches the far end: the locked \ + session params do" + .to_string(), + ) +} + +fn downscope(rig: &Rig, checkpoint: &str) { + judge( + checkpoint, + "down-scope", + &rig.both(downscope_script), + &downscope_check, + &|l: &Log| { + let mut planted = l.clone(); + for e in &mut planted.emissions { + if e.to_far { + e.frame = frame(&downscope_update()); + } + } + planted + }, + ); +} + +// ── D2: an ended session is closed hard ───────────────────────────────────────────────────────── + +#[derive(Clone, Debug, PartialEq, Eq)] +struct CloseObs { + log: Log, + late: (Outcome, usize), + cut: Rendered, +} + +fn close_script(p: &Plugin) -> CloseObs { + let _ = open(p, SESSION, Some(PUBLIC)); + let mut s = Session::open(p, 130, 2); + s.push(FROM_FAR_END, 0, &downlink(96)); + s.push(FROM_CALLER, PIECE_LAST, &[]); + let late = piece(p, &session(130, 2, FROM_FAR_END, 0, &downlink(96))); + CloseObs { + log: s.log, + late: (late.outcome, late.emitted.len()), + cut: refusal(p, 130, 0, 429, "the caller's budget is exhausted"), + } +} + +fn close_check(o: &CloseObs) -> Result { + if o.log.done != 1 || !o.log.anomalies.is_empty() { + return Err(format!("the session did not end once: {:?}", o.log)); + } + if o.late != (Outcome::Refused, 0) { + return Err(format!( + "far-end audio after the end was answered {:?}", + o.late + )); + } + let v: serde_json::Value = serde_json::from_str(&o.cut.body).unwrap_or_default(); + if o.cut.outcome != Outcome::Ready || v["error"]["type"] != "rate_limit_error" { + return Err(format!("the cut rendered as {:?}", o.cut)); + } + Ok( + "once a session has ended no far-end audio reaches the caller (the stream is refused) \ + and the cut renders in the dialect's error shape; reading the chain dry and cutting \ + are the kernel's" + .to_string(), + ) +} + +fn hard_close(rig: &Rig, checkpoint: &str) { + judge( + checkpoint, + "hard close", + &rig.both(close_script), + &close_check, + &|o: &CloseObs| CloseObs { + late: (Outcome::Ready, 128), + ..o.clone() + }, + ); +} diff --git a/testing/voice-conformance/harness/src/main.rs b/testing/voice-conformance/harness/src/main.rs new file mode 100644 index 0000000000..6d3f44ca91 --- /dev/null +++ b/testing/voice-conformance/harness/src/main.rs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2026 Busbar Inc and contributors + +//! THE VOICE CONFORMANCE HARNESS — the driver behind `testing/voice-conformance/`, re-homed onto +//! the streaming plane's DOOR (ARCHITECT Q6: the streaming plane's conformance MUST-set, built +//! against `busbar-plane-streaming`, linked and dlopened, RED arm kept). +//! +//! A dev-only testkit binary: never shipped, no production dependency. Every assertion drives the door +//! through `busbar-plugin-loader` BOTH ways — the LINKED door (`busbar_plane_streaming::door::door`) +//! and the DROPPED door (the `streaming_door` example `cdylib`, dlopened) — and requires both to +//! answer identically, then judges the answer, then requires the leg's planted wrong answer to be +//! refused by the same judgment (`door::judge`). `VOICE_CONFORM_RED=1` makes the planted answers +//! the subject: every leg must then go RED. +//! +//! Output contract (the runner greps `^RESULT `): each assertion prints exactly one line +//! RESULT +//! Non-`RESULT` lines (`SUBITEM`) are ignored by the runner. +//! +//! ```text +//! voice_conform spec +//! voice_conform replay +//! voice_conform cross +//! voice_conform governance +//! voice_conform composition +//! ``` + +use std::path::Path; + +mod codec_legs; +mod composition; +mod door; +mod governance; + +fn usage() -> ! { + eprintln!( + "usage:\n voice_conform spec \n voice_conform replay \ + \n voice_conform cross \ + \n voice_conform governance \n voice_conform composition " + ); + std::process::exit(2); +} + +/// Argument `i`, or the usage and exit 2. +fn nth(args: &[String], i: usize) -> &str { + match args.get(i) { + Some(a) => a.as_str(), + None => usage(), + } +} + +fn main() { + let args: Vec = std::env::args().collect(); + let rig = door::Rig::boot(); + let code = match nth(&args, 1) { + "spec" => codec_legs::spec(&rig, nth(&args, 2), Path::new(nth(&args, 3))), + "replay" => codec_legs::replay(&rig, Path::new(nth(&args, 2))), + "cross" => { + let map: serde_json::Value = match std::fs::read(nth(&args, 5)) + .map_err(|e| e.to_string()) + .and_then(|b| serde_json::from_slice(&b).map_err(|e| e.to_string())) + { + Ok(map) => map, + Err(e) => { + door::result( + nth(&args, 2), + false, + "map", + &format!("{}: {e}", nth(&args, 5)), + ); + std::process::exit(1); + } + }; + codec_legs::cross( + &rig, + nth(&args, 2), + Path::new(nth(&args, 3)), + Path::new(nth(&args, 4)), + &map, + ) + } + "governance" => governance::run(&rig, nth(&args, 2)), + "composition" => composition::run(&rig, nth(&args, 2)), + _ => usage(), + }; + std::process::exit(code); +} diff --git a/testing/voice-conformance/legs/admit-refusal.sh b/testing/voice-conformance/legs/admit-refusal.sh index e34988c91a..4781068ed8 100644 --- a/testing/voice-conformance/legs/admit-refusal.sh +++ b/testing/voice-conformance/legs/admit-refusal.sh @@ -4,27 +4,19 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: admit-refusal — a key whose budget is already spent is refused AT THE DOOR, before any provider -# dial (BUSBAR-1.6.0.md THE DESIGN, §1's teller loop, step 4 ADMIT: a refused reserve costs zero bytes and zero charge). +# LEG: admit-refusal — a refused unit opens nothing and dials nothing, and its refusal is rendered in +# the dialect's error shape (BUSBAR-1.6.0.md THE DESIGN §1, step 4 ADMIT; Part 3 §12 "refused → refusal"). # -# `topology::begin_session` reserves the D2 lease strictly AFTER the destination gate and strictly -# BEFORE any socket is touched (`topology::open_admitted_session`'s doc comment: "the session's own -# charge... fires only AFTER the gate clears"). This leg drives that real function with a REFUSE-ALL -# cap (`Some(0)`, the reserve-then-settle lease's own "already spent" shape) over the substrate's -# `FixtureHost` and asserts: (a) `begin_session` returns `Err(StartError::BudgetRefused)` — the -# plane's own refusal, not a generic one; (b) no cost lease was ever opened host-side -# (`FixtureHost::leases_opened()` does not advance across the refusal); (c) no ledger posting landed -# for the presenting principal (`FixtureHost::ledger_usage`) — voice posts no separate "kernel-floor" -# line at Admit the way BUSBAR-1.6.0.md describes for an ALREADY-DIALED provider push (nothing is ever -# dialed here, so nothing is owed and voice's design posts none); (d) a sanity/negative control — the SAME -# destination with an uncapped budget opens cleanly — so a leg that always failed (or always passed) -# could not hide behind this result. +# THE DOOR'S HALF (judged, linked AND dropped door, `voice_conform composition admit-refusal`): the +# kernel's refusal — budget 429, grant 403, credential 401 — is rendered by the door's `refusal` op in +# the realtime dialects' error envelope (the kernel's status kept, `content-type: application/json`, +# no record row); a refused unit's stream has no session (its far-end piece and its collection are +# refused, `drive` names nothing), while an admitted control on the same door dials (its frame rides +# the realtime socket need). RED arm: a door that renders the budget refusal as a server error fails. # -# WAS RED (this leg's own history): before this leg existed, `governance.sh`'s D2 checkpoint probed -# exhaustion AFTER a session had already opened and spent partway through its cap; nothing proved the -# DOOR itself refuses a caller who arrives already spent, or that the refusal costs zero dial/zero -# ledger. A caller with `Some(0)` remaining could only be judged by mid-session exhaustion, never by an -# admission that never opens at all. +# THE KERNEL'S HALF (not the door's, so not judged here): reading the caller's budget chain dry and +# refusing before any hold or ledger posting — the admit step, proven on the served leg by +# crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_refuses_an_over_budget_caller_before_it_dials. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/audit-record.sh b/testing/voice-conformance/legs/audit-record.sh index 459503f19c..094179cadd 100644 --- a/testing/voice-conformance/legs/audit-record.sh +++ b/testing/voice-conformance/legs/audit-record.sh @@ -4,25 +4,19 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: audit-record — one governed voice session lands EXACTLY ONE new admin-audit entry, carrying the -# plane's own action literal and outcome (BUSBAR-1.6.0.md's audit-chain discipline, applied to the -# voice-plane mutation a session open represents). +# LEG: audit-record — the audit kind and operation the kernel's ONE row per unit is written under, and +# no row of the door's own beside it (BUSBAR-1.6.0.md THE DESIGN §1, the fixed record; Part 3 §12 "audit"). # -# Before this leg, NOTHING in `busbar-voice` ever called `JournalHost::audit_record` — a session could -# open, meter and close without leaving a single row on the admin audit trail. `topology:: -# open_admitted_session` now journals ONE row (`action = "streaming.session.open"`, `outcome = "applied"`, -# `principal` = the session's owner) at its single `Ok` success point, through the live host -# (`VoiceRuntime::audit_session`, a no-op on the pre-host/dev-default runtime with nothing to journal -# through). This leg drives that real code path twice over the substrate's `FixtureHost` — a full -# `EngineHost` double whose `audit_record` now RECORDS (not a no-op) — and asserts: +# THE DOOR'S HALF (judged, linked AND dropped door): the tail states the `streaming_session` audit kind +# and the one `streaming.session.open` operation class; every one of the five doors' units arrives +# as that operation; and the door writes NO record row of its own — it declares no record kind, and +# across two whole sessions, a mint and a refusal its answers carry zero record writes — so the +# kernel's one fixed record is the only row (never doubled). RED arm: a door that writes a row fails. # -# * a clean session open lands EXACTLY ONE row, shaped `("streaming.session.open", "streaming:", -# "applied", )` — the literal action/outcome vocabulary, not a placeholder; -# * a SECOND, independent session adds exactly one MORE row (two sessions -> two rows, never -# doubled, never dropped) — so a leg that always saw "at least one" could not pass by luck. -# -# WAS RED: with no call site at all, `FixtureHost::audit_log()` stayed empty across every session this -# leg opens; the very first assertion (`len() == 1`) is what a truly wired call site must clear. +# NOT THE DOOR'S: the door does not write the session's audit row — the plane is a pure kind and the +# kernel writes the one fixed record at the unit's audit step (served leg: +# crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_audits_each_call_once). The plane's +# `session_unit` keeps a session row internally, but the door surfaces none (no record kind declared). # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/cross-parity.sh b/testing/voice-conformance/legs/cross-parity.sh index ee574c059c..f4cd5cea2a 100644 --- a/testing/voice-conformance/legs/cross-parity.sh +++ b/testing/voice-conformance/legs/cross-parity.sh @@ -4,31 +4,22 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: cross-parity — the 4 ORDERED OpenAI<->Gemini pairs. +# LEG: cross-parity — the 4 ORDERED OpenAI<->Gemini pairs (oo, og, go, gg). # -# The cross-dialect mapping (qa/evidence/voice-cross-dialect-mapping.*, authored by another agent) -# declares which behaviours MUST be equivalent across the two dialects. This leg drives all four -# ORDERED pairs and asserts the mapping holds in each direction: -# -# oo openai -> openai (self-parity: the mapping must be identity within a dialect) -# og openai -> gemini (a session captured on openai, re-derived under gemini, must map) -# go gemini -> openai -# gg gemini -> gemini -# -# Both diagonal pairs (oo, gg) are ordered slices in their own right, not skipped: a mapping that is -# not identity within a dialect is already broken, and only running the cross pairs would never see -# it. That is the cross-parity analogue of the sibling batteries' "a control that exercises a -# different path from the subject proves less than it appears to". -# -# STATUS: ready. Each ordered pair is driven as read(A) → shared IR → write(B) → IR against the -# machine-readable `qa/evidence/voice-cross-dialect-map.json`. For every SHARED concept the map -# declares, the load-bearing fields must survive the bridge (compared on a correlation-collapsed -# fingerprint, so a streamed⟷atomic tool-call reframing counts as agreement, and documented -# non-survivors — text modality, VAD specifics, truncate ms — are excluded per the map). And EVERY -# row of the asymmetry table is exercised as a documented drop+warn in its origin→other direction, so -# a one-dialect-only concept is accounted for, never silently lost. Concepts whose source-dialect -# fixture cannot decode (the one genuine codec gap, or a documented drop) are printed as PENDING -# sub-items, never faked green. +# For every shared concept of the machine-readable `qa/evidence/voice-cross-dialect-map.json`: +# * CODEC: the plane's own codecs (`busbar_plane_streaming::codec`, the module the door's sessions +# run) bridge the concept A → IR → B → IR keeping its load-bearing fields (correlation-collapsed +# fingerprint; documented non-survivors excluded per the map); +# * DOOR: the bridged B wire is pushed through a live session on the door that speaks B, on the +# linked AND the dropped door, and what the door relays must equal the session rules (a caller +# frame reaches the far end as sent, the caller's session config replaced by the locked one; a +# far-end frame reaches the caller as sent, usage consumed into reported units, a barge-in also +# cancelling and truncating upstream). RED arm per concept: a door reporting a token the far end +# never sent fails. +# Every asymmetry row is exercised as a documented drop+warn in its origin→other direction (codec); +# concepts with no decoding source fixture print a SUBITEM PENDING line, never a pass. The diagonal +# pairs (oo, gg) are slices in their own right: a mapping that is not identity within a dialect is +# already broken. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/exit-terminal.sh b/testing/voice-conformance/legs/exit-terminal.sh index 1d881919da..6e6de1f46e 100644 --- a/testing/voice-conformance/legs/exit-terminal.sh +++ b/testing/voice-conformance/legs/exit-terminal.sh @@ -4,31 +4,15 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: exit-terminal — one voice session ends ONCE: exactly one metering-lease settlement and exactly -# one admin-audit row, even when the session is INTERRUPTED (torn down before it runs a single frame, -# and independently closed twice — the shape a parked per-frame handler's stale guard plus the node's -# own sweep produce, BUSBAR-1.6.0.md THE DESIGN, §1's teller loop, the "exit" step — ONE exit path). +# LEG: exit-terminal — one session ends ONCE (BUSBAR-1.6.0.md THE DESIGN §1, the exit step). # -# TWO INDEPENDENT PROOFS, both over the substrate's `FixtureHost` (the same faithful in-memory -# `CostHold`/audit-ring stand-in the admit/route/audit legs use): +# THE DOOR'S HALF (judged, linked AND dropped door): the caller's end settles the open turn's audio +# seconds once and carries the session's end exactly once (cumulative units final); afterwards the +# stream is refused on every side (a collection and a far-end piece) and `drive` names nothing; a +# session torn down before its first frame ends with no units and no fee. RED arm: a second end fails. # -# 1. METERING SETTLES EXACTLY ONCE UNDER A DOUBLE CLOSE. `MeteringHost::cost_close` is the primitive -# `LeaseCloseGuard::drop` calls, and its OWN contract is that a redundant close is a harmless -# `None` -- "no double refund" (see `runtime/metering.rs`'s doc comment on `LeaseCloseGuard`). -# This leg reserves, settles a real increment, then closes the SAME lease id TWICE directly -# against the host: the FIRST close must return the exact settled amount: the SECOND -- the -# "interrupted, closed again" case -- must return nothing, proving there is nothing left to -# double-settle. -# 2. THE SESSION'S ONE AUDIT ROW SURVIVES AN INTERRUPTION. A session opened through the real -# `topology::begin_session` (lands its one `audit-record` row, see the `audit-record` leg), then -# torn down immediately -- the core dropped and the D2 close guard dropped -- without ever -# running a frame. Exactly one audit row must remain: not zero (the interruption did not erase -# the row the open already wrote) and not two (nothing re-fires on teardown). -# -# WAS RED: before `open_admitted_session` wrote its one audit row (see `audit-record`), there was no -# row to prove survives anything at all; and before this leg existed, nothing exercised the SAME -# double-close shape production's own `LeaseCloseGuard`/parked-handler race notes as the reason a -# by-value guard (not a refcount-gated `Drop`) is required. +# THE KERNEL'S HALF: sealing the session's one line and evicting its durable row (served leg: +# crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_ends_each_call_once). # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/gemini-live-route.sh b/testing/voice-conformance/legs/gemini-live-route.sh index f1c83a52ee..0672ead8fb 100644 --- a/testing/voice-conformance/legs/gemini-live-route.sh +++ b/testing/voice-conformance/legs/gemini-live-route.sh @@ -4,23 +4,14 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: gemini-live-route (the second-dialect route) — the Gemini Live dialect has a MOUNTED route, not just a codec. +# LEG: gemini-live-route — the Gemini Live dialect has a door, not just a codec. # -# The spec-per-dialect and cross-parity legs already prove the Gemini Live codec is correct in -# isolation: wire<->IR round trips, and agreement with OpenAI Realtime where the cross-dialect map -# says the two must agree. Neither leg drives the MOUNT: `PLANE_DECL.wire_format_names` named -# `gemini_live` as a dialect the plane speaks, but `voice_claims` / `voice_ws_arrivals` named only the -# OpenAI base, so a caller had no ingress path to reach it at all — a real second dialect the plane -# could not actually serve. -# -# This leg judges the route, on the plane's own PUBLIC functions (the same ones the composition root -# and the core router call): the dispatch slot claims a Gemini-labelled base distinct from the OpenAI -# one, the plane still admits exactly one audience for both, a Gemini WS-accept arrival is declared -# and keyed to this plane's own slot, and — the wire handshake itself — a provider's `setupComplete` -# relays to the client verbatim through the EXACT `SessionCore` type the mounted -# route's `WsArrivalSpec` closure closes over. -# -# WAS RED: no ingress route spoke Gemini Live at all. +# THE DOOR'S HALF (judged, linked AND dropped door): the snapshot claims `GET +# /v1/realtime/gemini/{call_id}` (an upgrade line whose refusals render in Gemini's dialect) beside the +# OpenAI doors under the ONE audience; `arrive` on it answers the gemini_live dialect for a principal +# on the session model's DIRECT route; and the handshake crosses the door — the caller's `setup` +# reaches the far end as a Gemini setup on the Live socket need, the far end's `setupComplete` reaches +# the caller verbatim. RED arm: a Gemini claim refused in the OpenAI dialect fails. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/governance.sh b/testing/voice-conformance/legs/governance.sh index 76bad60cf2..5528f27578 100644 --- a/testing/voice-conformance/legs/governance.sh +++ b/testing/voice-conformance/legs/governance.sh @@ -4,32 +4,22 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: governance — the 5 vision checkpoints. NOT A CONFORMANCE RESULT. +# LEG: governance — the 5 vision checkpoints, observed at the streaming plane's DOOR (linked AND +# dropped). NOT A CONFORMANCE RESULT: the runner keeps a governance FAIL out of the conformance verdict +# (proven by `--selftest`), exactly as testing/a2a-governance/ can never move the A2A verdict. # -# This leg observes busbar's voice PRODUCT POLICY, not the voice PROTOCOL. Exactly as -# testing/a2a-governance/ can never contribute to the A2A conformance verdict — a perfectly -# conformant agent that ignores every budget scores 100% on conformance — this leg's findings are -# OBSERVATIONS. The runner enforces the separation in code: a governance leg that FAILs cannot move -# the conformance verdict, and `--selftest` proves that separation bites. -# -# THE 5 VISION CHECKPOINTS (scaffolded; the real assertions and their fixtures are authored -# elsewhere — this file only fixes their names and order): -# -# V1-barge-in-preemption a caller barge-in must preempt the in-flight turn within budget. -# V2-turn-budget-enforcement a turn that overruns its token/time budget must be bounded. -# V3-metering-lease-settled every turn's metering lease (cost_reserve/cost_settle — the D2 host -# seam this battery's base commit wired) must settle; none may leak. -# V4-dialect-downscope crossing the OpenAI<->Gemini boundary must down-scope, never widen, -# what the far dialect is permitted to see. -# D2-hard-close-on-exhaustion the D2 checkpoint: on budget/lease exhaustion the session must HARD -# CLOSE, not degrade open. A soft-degrade here is the exact failure D2 -# exists to forbid. -# -# STATUS: ready. Each checkpoint is probed over the REAL T2 runtime (feature `runtime`) and recorded -# as an OBSERVATION — the runner records these but never lets a governance finding move the -# conformance verdict (proven by `--selftest`). The load-bearing one is D2-hard-close-on-exhaustion: -# it settles a real `LocalLease` past its cap and asserts the carrier HARD-closes (response.cancel -# upstream, no post-close audio reaches the client), reusing the runtime's actual exhaustion path. +# V1-barge-in-preemption a far-end speech_started cancels the response and truncates the item at +# the ms the caller heard, upstream, and tells the caller. +# V2-turn-budget-enforcement a session past `streams.session_max_secs` is told why (session_expired) +# and ended once, on the kernel's tick clock. +# V3-metering-lease-settled a turn's usage settles exactly once (a short answer's re-call reports +# the same cumulative units; the end adds nothing settled). +# V4-dialect-downscope an OpenAI-only semantic_vad/g711 concept is down-scoped toward Gemini +# (codec), and the caller's session.update never reaches the far end: +# the locked session params do (door). +# D2-hard-close-on-exhaustion once a session has ended no far-end audio reaches the caller and the cut +# renders in the dialect's error shape; reading the chain dry and cutting +# are the kernel's. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/metering-lease.sh b/testing/voice-conformance/legs/metering-lease.sh index 6ee2c932ee..fb0f7cc52a 100644 --- a/testing/voice-conformance/legs/metering-lease.sh +++ b/testing/voice-conformance/legs/metering-lease.sh @@ -4,21 +4,19 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: metering-lease — a served session's money hop is the HOST's, and its ceiling is the CALLER's. +# LEG: metering-lease — a session's units, reported per declared class (BUSBAR-1.6.0.md THE DESIGN +# §1 step "meter"; Part 3 §12: "only far-end-reported units bill"). # -# The governance leg proves the D2 lease hard-closes at whatever cap it is handed. This leg judges the -# question one step earlier, which fails independently: WHERE the lease lives and WHOSE budget sets -# its cap. A served session must reserve on the host's own reserve-then-settle lease — the one the -# rest of the deployment's spend flows through — with the ceiling read off the presenting principal's -# real budget chain: the tightest remaining bucket, widened from the budget projection's micro-units -# into the lease's nanodollars. +# THE DOOR'S HALF (judged, linked AND dropped door): the tail declares the six counted classes plus the +# per-session fee unit; `arrive` admits on no estimate; every session answer reports cumulative +# REPORTED units — a turn's far-end tokens and its audio seconds once per turn (a short answer's re-call +# does not count its audio twice), the open turn settled once at the end, and the fee only once the +# far end answered (a session whose far end never answered reports none). RED arm: a door reporting one +# token more than the far end sent fails. # -# The two boundaries are judged too: a caller with nothing capped anywhere in its chain has no ceiling -# to impose and stays uncapped (exactly as an unbudgeted model call does), and a caller whose budget -# is already spent is denied at the reserve, so it never opens a session at all. -# -# WAS RED: every session reserved an uncapped in-process cell, so no caller's budget could reach a -# live session and the host never saw the lease. +# THE KERNEL'S HALF: the session account — reserving against the caller's budget chain, capping at its +# tightest remaining bucket, ledgering the counts (served leg: +# crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_meters_each_turn_per_declared_class). # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/provider-credential.sh b/testing/voice-conformance/legs/provider-credential.sh index c07869800e..e43cd9c2e1 100644 --- a/testing/voice-conformance/legs/provider-credential.sh +++ b/testing/voice-conformance/legs/provider-credential.sh @@ -4,21 +4,17 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: provider-credential — a MOUNTED voice door can actually reach a realtime provider. +# LEG: provider-credential — the plane never holds the realtime provider's credential. # -# The plane's two one-shot HTTP passes (the browser `ek_` mint and the SDP broker) are governed and -# audience-checked whether or not a provider is composed — so "governed" is not the same claim as -# "serves". This leg judges the second: the composition root hands the plane the provider ORIGIN and -# the secret REFERENCE the deployment's own provider catalog declares for it, the plane resolves that -# reference through the deployment's ordinary secret resolver, and the mint / SDP passes serve under -# the resulting credential instead of reporting that there is nothing to dial. +# THE DOOR'S HALF (judged, linked AND dropped door): every far request the door answers — the mint, +# the SDP offer, the sideband socket and the Gemini socket — names the declared OUTBOUND need it rides, +# under the auth style the kernel binds the provider credential to (`bearer` for OpenAI Realtime, +# `x-goog-api-key` for Gemini Live) and with the member's address, never a plane config path; no +# request carries a credential field, and the caller is named only by the kernel's opaque reference. +# RED arm: a mint that carries the caller's `Authorization` fails. # -# It also judges the two ways this must fail: a reference that does not resolve composes NOTHING (an -# unresolvable credential must never become an empty one), and the endpoint is set-once, so nothing -# later in the process can silently swap a deployment's realtime credential. -# -# WAS RED: nothing composed a provider on any deployment, so both passes answered "governed, but no -# provider credential composed" and the plane's real key had no way in. +# THE COMPOSITION ROOT'S HALF (not the door's): resolving the catalog's secret reference through the +# deployment's resolver, set-once, an unresolvable reference composing nothing. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/provider-dial.sh b/testing/voice-conformance/legs/provider-dial.sh index e9fddce9f8..1856996e53 100644 --- a/testing/voice-conformance/legs/provider-dial.sh +++ b/testing/voice-conformance/legs/provider-dial.sh @@ -4,24 +4,15 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: provider-dial (the provider-dial leg) — a session actually DIALS the composed provider; the WS legs' upstream dial -# is no longer uncomposed. +# LEG: provider-dial — a session's far end is reached through the HOST CONNECTOR, never by the plane +# (Law 3; ARCHITECT Q128). # -# `topology::dial_provider` has existed since K1: breaker-admitted, net-guarded, counted on the shared -# upstream-attempt family. But nothing in the mounted routes called it — the telephony and Gemini WS -# accepts served the client socket only, discarding the uplink into a channel with no receiver, so no -# conformance leg (and no real deployment) ever drove a live provider socket end to end. -# -# This leg proves the wiring, not just the library function: it stands up a tiny loopback WS -# "provider" (no network, no vendor credential — a stand-in exactly as the shadow-oracle's mock -# upstream stands in for a real one), dials it through `dial_provider`'s own net-guarded path, feeds -# the frame it sends through the SAME `SessionCore` type the mounted routes open, and asserts the -# session's D2 metering lease actually settles the usage that arrived over that live socket — not a -# fixture, a socket. -# -# WAS RED: `topology::dial_provider` existed and passed its own unit tests in isolation, but no leg (and -# no mounted route) ever called it, so "the dial exists" and "a session dials it" were different, unproven -# claims. +# THE DOOR'S HALF (judged, linked AND dropped door): a session's far frames name the dialect's socket +# under the member's base URL (`GET /v1/realtime`, the Gemini BidiGenerateContent path) on the declared +# need they ride (door::RIDES_REALTIME_SOCKET / RIDES_LIVE_SOCKET), and the usage the far end sends over +# that socket is reported as units. The plane opens no socket and holds no guard (its closure is +# contract + codecs: tests/purity.rs); the net guard, TLS and the upgrade are the connector's. RED arm: +# a socket frame that names the one-shot pass need fails. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/replay.sh b/testing/voice-conformance/legs/replay.sh index a62ce16795..5bd67be7c3 100644 --- a/testing/voice-conformance/legs/replay.sh +++ b/testing/voice-conformance/legs/replay.sh @@ -6,19 +6,15 @@ # # LEG: replay — captured-transcript replay. # -# A recorded voice session (a captured transcript under -# testing/voice-conformance/fixtures/{openai,gemini}/) must re-derive IDENTICALLY when replayed -# through busbar's voice plane: same turn boundaries, same tool invocations, same barge-in points, -# same settlement. Replay is the leg that catches a change which passes the live spec but silently -# alters behaviour a real caller already depended on — the regression a spec-only battery cannot see. -# -# STATUS: ready. Each dialect's captured `transcript.jsonl` golden is driven through the real codec — -# threading ONE session `DecodeState`, honoring each line's `dir` (client → uplink, server → -# downlink) — and the decoded IR must re-derive the expected ordered concept skeleton -# (config → connect → audio → tool call → tool result → audio → barge-in → close/complete) with no -# load-bearing drop, and every decoded frame must re-encode to valid wire JSON. The Gemini uplink -# `realtimeInput.audio{}` frames the shipped codec does not read are recorded as a documented replay -# sub-item (the openai→gemini bridge still exercises that concept in cross-parity). +# Each dialect's captured `transcript.jsonl` (testing/voice-conformance/fixtures/{openai,gemini}/): +# * CODEC: decoded through ONE session state in order, every event re-framed to valid wire JSON, and +# the expected concept skeleton (config → connect → audio → tool call → tool result → audio → +# barge-in → close/complete) re-derived in order; +# * DOOR: the whole transcript driven through ONE live session on the door (client lines as caller +# pieces, server lines as far-end pieces), linked AND dropped; what the door relays must equal the +# session rules, and the same skeleton must appear in the order the door emitted it (less the far +# end's usage, which the session consumes into reported units). RED arm: a door reporting a token +# the transcript never sent fails. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/route-failover.sh b/testing/voice-conformance/legs/route-failover.sh index 512b7861d3..806cfa504a 100644 --- a/testing/voice-conformance/legs/route-failover.sh +++ b/testing/voice-conformance/legs/route-failover.sh @@ -4,28 +4,17 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: route-failover — a down provider trips the breaker on its first hard-down strike, and a tripped -# breaker refuses EVERY further dial before any socket/URL work — the documented terminal outcome -# (BUSBAR-1.6.0.md THE DESIGN, §1's teller loop, step 5 ROUTE) with no repeated egress once the cell is open. +# LEG: route-failover — the door's half of the route walk (BUSBAR-1.6.0.md THE DESIGN §1 step 5 ROUTE; +# Part 3 §12 "The route pump": the walk — pool, member pick, breaker, failover, exhaustion — is the +# kernel's, and the driver builds no second one). # -# `topology::dial_provider` probes the `(pool, lane)` breaker cell through the host seam FIRST — before -# any DNS/guard/socket work — and folds a real dial's outcome back into the SAME cell -# (`host.breaker_record_signal`). This leg drives that real function twice over the substrate's -# `FixtureHost` (a real in-memory breaker, not a stand-in that always admits): -# -# 1. breaker CLOSED, a real dial to a target the default fail-closed `GuardPolicy` refuses (a -# plaintext `ws://` loopback address) — a genuine `DialProviderError::Dial(_)`, and the guard -# refusal's canonical signal (Auth-class) trips the cell HARD DOWN on this first strike. -# 2. breaker now OPEN — a SECOND dial, to a syntactically GARBAGE target a real dial would fail -# differently on (`DialProviderError::Dial(Url(_))`), must instead come back -# `DialProviderError::BreakerOpen` with a positive `Retry-After` — proving the breaker check runs -# STRICTLY BEFORE any dial attempt, not just that a dial eventually fails again. -# -# WAS RED (this leg's own history): `dial_provider`'s breaker-admit-first order and its fold-back onto -# the cell existed and were unit-tested in isolation, but no conformance leg judged the OBSERVABLE -# outcome from the plane's own public seam — that a real hard-down dial actually opens the SAME cell a -# second dial is judged against, and that the terminal refusal it then returns names ZERO further dial -# attempt (as opposed to, say, a retried dial that merely fails again for a different reason). +# THE DOOR'S HALF (judged, linked AND dropped door, the mint door): it names the session model's DIRECT +# route (and none with no model configured); each ATTEMPT is answered afresh with the same request on +# the declared pass need (door::RIDES_REALTIME_PASS) — the host connector dials it; a failing far end +# reaches the caller with nothing before its last piece, so the walk may still fail over; the next +# attempt's answer is the caller's whole answer, free of the failed one; and a last attempt that fails +# is the served plane's 502. The plane opens no socket and holds no breaker or guard. RED arm: a retry +# that sends a different request fails. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/session-scope.sh b/testing/voice-conformance/legs/session-scope.sh index f46b2d6504..ee9dd5cf96 100644 --- a/testing/voice-conformance/legs/session-scope.sh +++ b/testing/voice-conformance/legs/session-scope.sh @@ -4,21 +4,16 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: session-scope — the plane's one declared scope kind is a gate, not vocabulary. +# LEG: session-scope — the plane's `session` grant kind, at the door. # -# Holding a key that is valid for the voice door's AUDIENCE is not the same as being GRANTED a session -# on it: the audience check answers "is this token for this door", the grant answers "may this caller -# walk through it". The plane declares a `session` scope kind, which is what an operator's -# `allowed_scopes: [{ kind: session, value: … }]` entry validates against, and this leg judges that -# the door actually asks the question — the way MCP double-gates a tool and A2A gates an agent. +# THE DOOR'S HALF (judged, linked AND dropped door): the tail declares the `session` grant kind; every +# one of the five doors asks for a principal (none admits anonymously) under the one session-open +# operation; an unpublished claim is refused 404 with the plane's own code; a grant refusal renders as +# the dialect's permission_error. RED arm: a door that admits anonymously fails. # -# The whole grant semantic is judged, not just the happy path: a key with no scope list at all is the -# store's wildcard and is granted every kind; a key with an explicit list must carry the session grant -# for this voice pool, so a model-plane key, a session grant aimed at another pool, and an empty list -# are all refused. -# -# WAS RED: any key valid for the plane's audience opened a session, and the declared scope kind was -# vocabulary nothing consulted. +# THE KERNEL'S HALF: matching the presenting key's grants (wildcard, an explicit session grant, one aimed +# at another pool, an empty list) — the approve step (served leg: +# crates/busbar/src/root/tests/gauntlet_kernel.rs::served_rider_refuses_a_grant_short_of_the_operation). # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/spec-per-dialect.sh b/testing/voice-conformance/legs/spec-per-dialect.sh index 4be828fcad..dc1b3626ff 100644 --- a/testing/voice-conformance/legs/spec-per-dialect.sh +++ b/testing/voice-conformance/legs/spec-per-dialect.sh @@ -4,21 +4,17 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: spec-per-dialect — the voice conformance spec, run once PER DIALECT. +# LEG: spec-per-dialect — the voice conformance spec, run once PER DIALECT (openai, gemini). # -# The matrix is the two dialects busbar's voice plane will speak: openai and gemini. Each slice runs -# the full spec battery against that dialect's peer and holds it to the dialect's own required -# scenario set (set equality, never a floor — see the sibling MCP `assert_covered`). -# -# STATUS: ready. Wired against the `busbar-voice` plane. Each slice drives EVERY captured fixture in -# `testing/voice-conformance/fixtures/$dialect/` through the dialect's real codec -# (`OpenAiRealtimeCodec` / `GeminiLiveCodec`) as wire JSON → IR → wire JSON, and asserts round-trip -# stability at the level the codec guarantees (IR-fixpoint; some families are also byte-stable, and -# an atomic Gemini `toolCall` that decodes to a streamed triple is held to the correlation -# fingerprint, since the stateless writer re-frames per event). A fixture that decodes to NOTHING is -# only accepted when it is a documented drop+warn; anything else fails RED. One fixture — the Gemini -# `realtimeInput.audio{}` uplink shape the shipped codec does not read — stays an HONEST PENDING -# sub-item (printed, never dressed as a pass), not a reason to weaken the whole leg. +# Every captured fixture in `testing/voice-conformance/fixtures/$dialect/`: +# * CODEC: wire JSON → IR → wire JSON → IR through the dialect's codec in the plane crate +# (`OpenAiRealtimeCodec` / `GeminiLiveCodec`), stable at the level the codec guarantees +# (IR-fixpoint, or the correlation fingerprint for an atomic Gemini toolCall); a fixture that +# decodes to nothing passes only as a documented drop; +# * DOOR: pushed through a live session on the dialect's door (the caller's frames as caller pieces, +# the far end's as far-end pieces), linked AND dropped; the door must relay it by the session rules +# (a documented drop on no wire at all), every far frame on the dialect's socket need, the far end's +# tokens reported as units. RED arm per fixture: a door reporting a token never sent fails. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/legs/tool-reply.sh b/testing/voice-conformance/legs/tool-reply.sh index d85951e6b8..d8ae22fad9 100755 --- a/testing/voice-conformance/legs/tool-reply.sh +++ b/testing/voice-conformance/legs/tool-reply.sh @@ -4,38 +4,17 @@ # shellcheck shell=bash # shellcheck disable=SC2034 # LEG_KIND/LEG_STATUS/LEG_SLICES are read by voice-conformance.sh on source # -# LEG: tool-reply — a call the node does not serve is answered by the client, through the governed wait. +# LEG: tool-reply — a tool call is RELAYED, never answered (Law 11; QUESTIONS Q98; ARCHITECT Q128). # -# The tool moat has two halves and only one of them was reachable from a served session. A call for a -# tool the node itself serves executes in-process and the client can neither see nor forge its result; -# that half is unchanged and is not what this leg judges. The other half is a call the node does NOT -# serve: the answer can only come from the client, so the call's leg is a WAIT the composition root -# enters when it plans the leg, and something on the socket has to be able to end it. +# THE DOOR'S HALF (judged on both duplex dialects, OpenAI Realtime and Gemini Live, linked AND dropped +# door): the model's tool call reaches the caller as it was made (dialect translation only); the door +# sends the far end NOTHING for it (busbar authors no tool result); the call is counted once; and the +# caller's own result reaches the far end unchanged and alone (no door-authored response). RED arm: a +# door that answers the call upstream fails. # -# Three facts, driven through a REAL session core on EVERY duplex dialect the plane serves (OpenAI -# Realtime and Gemini Live — one runtime, two codecs, which is the point of the neutral IR): -# -# * the wake — the client's `function_call_output` (Gemini: `toolResponse.functionResponses`) goes to -# the node's table FIRST and reaches the model only because the wait was woken; -# * the refusal — a reply naming a call nobody is waiting on is refused by identifier and carried on -# no wire at all, rather than paid out against whichever call happens to be standing; -# * the sweep — the tick beside the pump ends a call nobody answered AT THE DEADLINE THE PLANE -# DECLARES, so its unit exits under that deadline rather than settling as though the answer had -# arrived. -# -# The deadline is `busbar_plane_streaming::plane::TOOL_REPLY_DEADLINE_SECS`, read from the plane crate the -# composition root builds its own reply leg out of — one declaration and two readers, so a change to -# the figure moves this leg with it. Both edges are judged: nothing is ended one millisecond before -# that wall, and the unanswered call IS ended at it. What the ending then does to a unit — the exit -# under `Failed(Route, DeadlineExceeded)` — is the composition root's own identity cell, which drives -# this same wall through this same tick over the node's real table. -# -# WAS RED: the runtime executed every tool call in-process and carried a client-authored result -# upstream verbatim, so on the served path the wait was entered and then nothing ever woke it and -# nothing ever swept it. A client that never replied held the call's hold open forever, and a forged -# reply rode upstream under whatever call was open. Then the wake and the sweep landed and the -# DEADLINE still did not: the tick ended whatever it found, at whatever wall it was handed, so a -# call could be settled before the client it was delivered to had run out of time to answer it. +# NOT PORTED, BY RULING: the old leg certified in-process tool execution and busbar waking, refusing +# and sweeping client tool replies through an open-call table; those assertions violate Law 11 and are +# not carried in any form. # shellcheck source=../lib/conform-bin.sh disable=SC1091 . "$(dirname "${BASH_SOURCE[0]}")/../lib/conform-bin.sh" diff --git a/testing/voice-conformance/lib/conform-bin.sh b/testing/voice-conformance/lib/conform-bin.sh index 2e82e7f267..58e759894d 100644 --- a/testing/voice-conformance/lib/conform-bin.sh +++ b/testing/voice-conformance/lib/conform-bin.sh @@ -6,8 +6,10 @@ # # SHARED HELPER for the voice-conformance legs — NOT a leg (it lives outside legs/, so the runner's # `legs/*.sh` glob never discovers it). Sourced by each `legs/.sh` to locate the REAL Rust -# conformance harness (`busbar-voice`'s `voice-conform` bin) the legs shell out to. The legs reuse the -# crate's production codecs + runtime through this bin; they never reimplement a codec in shell. +# conformance harness the legs shell out to: the `voice_conform` binary of the testkit crate +# `testing/voice-conformance/harness`, which drives the streaming plane's DOOR both ways through the loader — the linked door and +# the `streaming_door` example cdylib, dlopened (ARCHITECT Q6). The legs reuse the plane's own door +# and codecs through this harness; they never reimplement a codec — or a door — in shell. # Resolved from THIS file's location (testing/voice-conformance/lib/): the battery dir, repo root, # fixtures, and the cross-dialect map. Computed once at source time. @@ -17,15 +19,18 @@ VC_ROOT="$(cd "$VC_DIR/../.." && pwd)" # repo root VC_FIXTURES="$VC_DIR/fixtures" VC_MAP="$VC_ROOT/qa/evidence/voice-cross-dialect-map.json" -# The trees the harness IS. A `voice-conform` older than any file under these is a binary that would +# The trees the harness IS. A `voice_conform` older than any file under these is a binary that would # judge source it was not built from — which is not an error at run time, it is a PASS about code # nobody is running. The legs are in the list for the reason the note below already gives: the bin # answers a `composition ` subcommand per leg, so a leg added after the binary was built is a # slice the binary has never heard of. _vc_harness_trees() { printf '%s\n' \ - "$VC_ROOT/crates/busbar-voice/src" \ - "$VC_ROOT/crates/busbar-plane-streaming/src/codec" \ + "$VC_ROOT/crates/busbar-plane-streaming/src" \ + "$VC_ROOT/crates/busbar-plane-streaming/examples" \ + "$VC_DIR/harness/src" \ + "$VC_ROOT/crates/plugin-loader/src" \ + "$VC_ROOT/crates/busbar-contract/src" \ "$VC_DIR/legs" \ "$VC_DIR/lib" } @@ -46,39 +51,39 @@ _vc_newer_than() { return 1 } -# Echo the path to the built `voice-conform` binary, building it once if necessary. -# * $VOICE_CONFORM_BIN, if set, names the binary the workflow built once and exported — and it is -# CHECKED before it is used. It was taken verbatim, which meant a path to nothing, or to a -# binary built before the source beside it changed, was believed: a stale harness runs, passes, -# and reports conformance about code that is not in the tree. That is the same false green the -# runner's own anti-vacuity rule exists to refuse, one level further in, and unlike a missing -# binary it leaves no trace at all — every leg reports PASS. -# * else the harness is built (cargo is incremental, so an up-to-date binary costs a lock and a -# stat; a binary older than the legs it serves once reported "unknown composition slice" for -# four legs that existed only in source, and this is what stops that recurring) — features -# `runtime,test-support` — the D2 governance probe needs the -# async session engine, and the admit/route/audit/exit composition legs drive the substrate's -# `FixtureHost` test double over the real `EngineHost` seam) — cargo noise goes to stderr so it -# never pollutes the RESULT lines the runner parses on stdout. +# Echo the path to the built `voice_conform` harness, building it (and the dropped-in door beside it) +# once if necessary. +# * $VOICE_CONFORM_BIN, if set, names the harness the workflow built once and exported — and it is +# CHECKED before it is used: a path to nothing, or to a binary built before the source beside it +# changed, would run, pass, and report conformance about code that is not in the tree (the same +# false green the runner's anti-vacuity rule refuses, one level further in). The dropped-in door +# it loads is found beside it (or at $VOICE_CONFORM_DOOR); a door that is not there is a FAIL in +# every assertion, never a skip. +# * else both examples are built (cargo is incremental, so an up-to-date pair costs a lock and a +# stat; a harness older than the legs it serves would answer "unknown composition slice" for a leg +# that exists only in source) — cargo noise goes to stderr so it never pollutes the RESULT lines +# the runner parses on stdout. voice_conform_bin() { if [ -n "${VOICE_CONFORM_BIN:-}" ]; then if [ ! -x "$VOICE_CONFORM_BIN" ]; then - printf 'voice-conform: VOICE_CONFORM_BIN=%s is not an executable file\n' \ + printf 'voice_conform: VOICE_CONFORM_BIN=%s is not an executable file\n' \ "$VOICE_CONFORM_BIN" >&2 return 1 fi local newer if newer="$(_vc_newer_than "$VOICE_CONFORM_BIN")"; then - printf 'voice-conform: VOICE_CONFORM_BIN=%s is older than %s — it would report conformance about source it was not built from\n' \ + printf 'voice_conform: VOICE_CONFORM_BIN=%s is older than %s — it would report conformance about source it was not built from\n' \ "$VOICE_CONFORM_BIN" "$newer" >&2 return 1 fi printf '%s' "$VOICE_CONFORM_BIN" return 0 fi - local bin="${CARGO_TARGET_DIR:-$VC_ROOT/target}/debug/voice-conform" - cargo build -q --manifest-path "$VC_ROOT/Cargo.toml" \ - -p busbar-voice --features runtime,test-support --bin voice-conform >&2 || return 1 + local bin="${CARGO_TARGET_DIR:-$VC_ROOT/target}/debug/voice_conform" + cargo build -q --manifest-path "$VC_ROOT/Cargo.toml" -p voice-conformance-harness \ + --bin voice_conform >&2 || return 1 + cargo build -q --manifest-path "$VC_ROOT/Cargo.toml" -p busbar-plane-streaming \ + --example streaming_door >&2 || return 1 [ -x "$bin" ] || return 1 printf '%s' "$bin" } diff --git a/testing/voice-conformance/voice-conformance.sh b/testing/voice-conformance/voice-conformance.sh index 4e6d61445e..7f486746a3 100755 --- a/testing/voice-conformance/voice-conformance.sh +++ b/testing/voice-conformance/voice-conformance.sh @@ -34,34 +34,29 @@ # work before the first real leg depends on it. # # THE DECLARED LEGS (discovered from `legs/*.sh`, never enumerated here — the same rule -# `verdict-covers-every-leg.py` applies to the workflow, one level in): +# `verdict-covers-every-leg.py` applies to the workflow, one level in). Every leg drives the streaming +# plane's DOOR through the dev-only `voice_conform` harness (testing/voice-conformance/harness), BOTH ways — the +# linked door and the `streaming_door` cdylib, dlopened through the loader — requires the two to answer +# identically, and carries a RED arm per assertion (the leg's planted wrong answer must be refused; +# `VOICE_CONFORM_RED=1` makes the planted answers the subject, and every leg must then go RED). Each +# leg judges the DOOR'S half of its seam and names the half that is the kernel's (lib/conform-bin.sh): # -# spec-per-dialect the voice spec battery, per dialect. MATRIX: openai, gemini. -# replay captured-transcript replay: a recorded session must re-derive identically. -# cross-parity the 4 ORDERED OpenAI<->Gemini pairs (oo, og, go, gg) must agree where the -# cross-dialect mapping says they must. -# provider-credential the realtime provider credential the mint / SDP passes dial under, composed -# from the deployment's own provider catalog + secret resolver. -# metering-lease a session's money hop is the HOST's reserve-then-settle lease, capped by the -# presenting principal's own remaining budget. -# session-scope the plane's declared `session` scope kind, enforced at session open. -# gemini-live-route (the second-dialect route) the Gemini Live dialect has a MOUNTED WS-accept route (claim, admission, -# arrival, and the wire handshake itself), not just a codec the spec/cross-parity -# legs exercise off to the side. -# provider-dial (the provider-dial leg) a session actually DIALS the composed provider through a real (loopback) -# socket via `topology::dial_provider`, and its D2 metering lease settles the -# usage that arrived over it — the WS legs' upstream dial is no longer uncomposed. -# admit-refusal a key whose budget is already spent is refused AT THE DOOR -# (`StartError::BudgetRefused`) before any host-side lease is opened and before -# any ledger posting — no provider dial has anything left to reach. -# route-failover a hard-down provider dial trips the breaker cell on its first strike, and the -# tripped cell refuses every FURTHER dial before any socket/URL work — the -# documented terminal outcome, with no repeated egress once the cell is open. -# audit-record one governed session lands EXACTLY ONE new admin-audit entry, carrying the -# plane's own action literal (`streaming.session.open`) and outcome (`applied`). -# exit-terminal one session ends ONCE: a metering lease settles exactly once under a double -# close, and a session's one admin-audit row survives being torn down before it -# ever runs a frame. +# spec-per-dialect every fixture, per dialect (MATRIX: openai, gemini): codec round trip, and the +# door's relay of it by the session rules. +# replay each captured transcript through one session: codec skeleton, and the door's +# relay and skeleton in emitted order. +# cross-parity the 4 ORDERED OpenAI<->Gemini pairs (oo, og, go, gg): the codec bridge per the +# cross-dialect map, and the bridged wire through the destination dialect's door. +# provider-credential every far request rides a declared outbound need; none carries a credential. +# metering-lease a session's units, reported per declared class, once per turn; fee once answered. +# session-scope the `session` grant kind; every door asks for a principal; 404 off the guest list. +# gemini-live-route the Gemini Live door: its claim, its arrival, the handshake across it. +# provider-dial a session's far frames ride the dialect's socket need (the host connector dials). +# admit-refusal a refusal is rendered in the dialect's shape and opens nothing, dials nothing. +# route-failover each ATTEMPT answered afresh on the pass need; nothing reaches the caller early. +# audit-record the audit kind and operation the kernel's one row is written under; no door row. +# exit-terminal one session, one end; a stream that ended is refused on every side. +# tool-reply a tool call is relayed to the caller and never answered by busbar (Law 11). # governance the 5 vision checkpoints (incl. D2 hard-close-on-exhaustion). GOVERNANCE IS # NOT A CONFORMANCE RESULT — it can never move the conformance verdict, exactly # as `testing/a2a-governance/` can never contribute to the A2A verdict. @@ -100,9 +95,9 @@ VOICE_LEGS_DIR="${VOICE_LEGS_DIR:-$HERE/legs}" # A FLOOR on the declared-leg count, for the same reason the python verdict linter has one: every # equality below would hold for a battery that had been gutted to a single leg, so the count is -# checked first. Thirteen legs ship today (spec-per-dialect, replay, cross-parity, +# checked first. Fourteen legs ship today (spec-per-dialect, replay, cross-parity, # provider-credential, metering-lease, session-scope, gemini-live-route, provider-dial, -# admit-refusal, route-failover, audit-record, exit-terminal, plus governance). +# admit-refusal, route-failover, audit-record, exit-terminal, tool-reply, plus governance). MIN_LEGS="${VOICE_MIN_LEGS:-3}" say() { printf '%s\n' "$*"; } diff --git a/xtask/fixtures/reachability-green/crates/busbar/Cargo.toml b/xtask/fixtures/reachability-green/crates/busbar/Cargo.toml index bff75e737d..732c06482c 100644 --- a/xtask/fixtures/reachability-green/crates/busbar/Cargo.toml +++ b/xtask/fixtures/reachability-green/crates/busbar/Cargo.toml @@ -14,7 +14,7 @@ busbar-llm = { path = "../busbar-llm", optional = true } # and fold mechanics on a sample plane, and the door arm is exercised on the decisions plane. busbar-plane-mcp = { path = "../busbar-plane-mcp", optional = true } busbar-a2a = { path = "../busbar-a2a", optional = true } -busbar-voice = { path = "../busbar-voice", optional = true } +busbar-plane-streaming = { path = "../busbar-plane-streaming", optional = true } busbar-plane-decisions = { path = "../busbar-plane-decisions", optional = true } # The linked table the real root's build script turns into the `LINKED` table `main.rs` includes. @@ -22,7 +22,7 @@ busbar-plane-decisions = { path = "../busbar-plane-decisions", optional = true } proto-llm = "busbar-llm" plane-mcp = "busbar-plane-mcp" plane-a2a = "busbar-a2a" -plane-streaming = "busbar-voice" +plane-streaming = "busbar-plane-streaming" plane-decisions = "busbar-plane-decisions" # The axes each linked crate registers on; the plane axis is what `register_planes()` folds. @@ -30,7 +30,7 @@ plane-decisions = "busbar-plane-decisions" proto-llm = "plane protocols" plane-mcp = "plane protocols diagnostics" plane-a2a = "plane diagnostics" -plane-streaming = "plane diagnostics" +plane-streaming = "plane-door" plane-decisions = "plane" # The decisions plane's entry module is in the composition root, as on the real tree. diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/main.rs b/xtask/fixtures/reachability-green/crates/busbar/src/main.rs index 9564632e59..5e83c12b97 100644 --- a/xtask/fixtures/reachability-green/crates/busbar/src/main.rs +++ b/xtask/fixtures/reachability-green/crates/busbar/src/main.rs @@ -16,7 +16,7 @@ fn serve() -> u64 { root::plane_node::answer() + root::units_mcp::answer() + root::units_a2a::answer() - + root::units_voice::answer() + + root::serve::serve(&root::serve::compose_planes()) + root::units_decision::answer() } diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/gauntlet_install.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/gauntlet_install.rs index 0a8cd0ae8d..df579db006 100644 --- a/xtask/fixtures/reachability-green/crates/busbar/src/root/gauntlet_install.rs +++ b/xtask/fixtures/reachability-green/crates/busbar/src/root/gauntlet_install.rs @@ -19,5 +19,4 @@ pub fn install() { flip_one_shot_to_kernel(busbar_llm::PLANE_DECLARATION.key); flip_one_shot_to_kernel(busbar_plane_mcp::PLANE_KEY); flip_one_shot_to_kernel(busbar_a2a::PLANE_KEY); - flip_session_to_kernel(busbar_voice::PLANE_KEY); } diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/mod.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/mod.rs index 203ed684cc..9f0aadcc92 100644 --- a/xtask/fixtures/reachability-green/crates/busbar/src/root/mod.rs +++ b/xtask/fixtures/reachability-green/crates/busbar/src/root/mod.rs @@ -3,7 +3,7 @@ pub mod gauntlet_kernel; pub mod plane_decisions; pub mod plane_node; pub mod registry; +pub mod serve; pub mod units_a2a; pub mod units_decision; pub mod units_mcp; -pub mod units_voice; diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/registry.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/registry.rs index 98055308c4..77f446885e 100644 --- a/xtask/fixtures/reachability-green/crates/busbar/src/root/registry.rs +++ b/xtask/fixtures/reachability-green/crates/busbar/src/root/registry.rs @@ -2,14 +2,12 @@ use busbar_plane_a2a::A2aPlane; use busbar_plane_decisions::DecisionPlane; use busbar_plane_llm::LlmPlane; use busbar_plane_mcp::McpPlane; -use busbar_plane_streaming::StreamingPlane; pub fn plane_claims() -> Vec<&'static str> { let mut claims: Vec<&'static str> = Vec::new(); claims.push(LlmPlane::KEY); claims.push(McpPlane::KEY); claims.push(A2aPlane::KEY); - claims.push(StreamingPlane::KEY); claims.push(DecisionPlane::KEY); claims } diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/serve.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/serve.rs new file mode 100644 index 0000000000..0577ebed72 --- /dev/null +++ b/xtask/fixtures/reachability-green/crates/busbar/src/root/serve.rs @@ -0,0 +1,15 @@ +// The door planes' one composition, in miniature: exactly one function reached from `fn main()` +// builds a `PlaneDriver`, and a reached line in this module drives a unit through it (the served data +// route). A plane whose crate is linked on the `plane-door` axis is served here. +pub struct ServedPlane { + driver: PlaneDriver, +} + +pub fn compose_planes() -> Vec { + let driver = PlaneDriver::new(1); + vec![ServedPlane { driver }] +} + +pub fn serve(planes: &[ServedPlane]) -> u64 { + planes.iter().map(|p| p.driver.unit(1)).sum() +} diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/tests/units_voice.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/tests/units_voice.rs deleted file mode 100644 index 5d08fb00eb..0000000000 --- a/xtask/fixtures/reachability-green/crates/busbar/src/root/tests/units_voice.rs +++ /dev/null @@ -1,7 +0,0 @@ -use super::VoiceUnit; - -#[test] -fn the_unit_drives() { - let unit = VoiceUnit::new(1); - assert_eq!(unit.n, 1); -} diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/units_mcp.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/units_mcp.rs index c117448369..223fd631b8 100644 --- a/xtask/fixtures/reachability-green/crates/busbar/src/root/units_mcp.rs +++ b/xtask/fixtures/reachability-green/crates/busbar/src/root/units_mcp.rs @@ -24,3 +24,7 @@ pub fn answer() -> u64 { #[cfg(test)] #[path = "tests/units_mcp.rs"] mod tests; + +/// The root unit the generated table reaches when the manifest lists this module under +/// `[package.metadata.busbar.root-units]` — the self-test's generated-table cases plant that row. +pub const ROOT_UNIT: fn() -> u64 = answer; diff --git a/xtask/fixtures/reachability-green/crates/busbar/src/root/units_voice.rs b/xtask/fixtures/reachability-green/crates/busbar/src/root/units_voice.rs deleted file mode 100644 index a59c8dee12..0000000000 --- a/xtask/fixtures/reachability-green/crates/busbar/src/root/units_voice.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub struct VoiceUnit { - n: u64, -} - -impl VoiceUnit { - pub fn new(n: u64) -> Self { - VoiceUnit { n } - } -} - -impl busbar_kernel::teller::Units for VoiceUnit { - fn drive(&self) -> u64 { - self.n - } -} - -/// The reached caller: `main` -> `serve` -> here. It returns something OTHER than the unit, so -/// the construction below is a CALL of the constructor rather than the constructor's own body. -pub fn answer() -> u64 { - let unit = VoiceUnit::new(1); - unit.drive() -} - -/// The root unit the generated table reaches when the manifest lists this module under -/// `[package.metadata.busbar.root-units]` — the self-test's generated-table cases plant that row. -pub const ROOT_UNIT: fn() -> u64 = answer; - -#[cfg(test)] -#[path = "tests/units_voice.rs"] -mod tests; diff --git a/xtask/src/conformance_record/rigs.rs b/xtask/src/conformance_record/rigs.rs index 84897045b5..0f5c78d15d 100644 --- a/xtask/src/conformance_record/rigs.rs +++ b/xtask/src/conformance_record/rigs.rs @@ -940,14 +940,15 @@ impl Runner { ][..], ), ( + // The `streams:` section's grammar, now the streaming plane's own (`config::` + // in busbar-plane-streaming; the legacy busbar-voice crate is deleted). "boot-validate-config", &[ "cargo", "test", "-p", - "busbar-voice", - "--features", - "runtime", + "busbar-plane-streaming", + "--lib", "config::", ][..], ), diff --git a/xtask/src/gates/config_schema/declared.rs b/xtask/src/gates/config_schema/declared.rs index f7dff389ee..220b45a84f 100644 --- a/xtask/src/gates/config_schema/declared.rs +++ b/xtask/src/gates/config_schema/declared.rs @@ -64,6 +64,9 @@ fn spec() -> WalkSpec { #[derive(Clone, Default)] struct Facts { consts: Vec<(String, String)>, + /// String constants written as another constant's path (`const A: &str = config::SECTION;`), + /// resolved through the same reader once every file's literals are known. + aliases: Vec<(String, String)>, decls: Vec), String>>, core_owned: Option>, } @@ -95,16 +98,23 @@ fn facts(path: &str, raw: &str) -> Facts { } else if ty.contains("str") && !ty.contains('[') && value.len() >= 2 { if let Some(lit) = value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) { out.consts.push((name, lit.to_string())); + } else if value + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':') + { + out.aliases.push((name, value.to_string())); } } } - // A PLANE DOOR's Statement declares its sections as `Section { name: abi_str("…"), flags: … }` + // A PLANE DOOR's Statement declares its sections as `Section { name: abi_str(…), flags: … }` // rows: the one flagged `SECTION_DECLARING` is its declaring section, and a row flagged neither // declaring nor consumed is a section it owns beside it (the loader's own reading, // `registration().owns`). A door-only plane has no `PLANE_DECLARATION`; this is its declaration. - // Read only when EVERY row names its section as a string LITERAL: a door that spells them - // through other items still has its `PLANE_DECLARATION` row, which this census reads instead; - // a door-only plane states literals (its own test holds them equal to its grammar's constants). + // A door owns the grammar of the section it declares (the kernel folds it into the declared- + // section map carrier, DECL-FOLD), exactly as a PlaneDecl whose `owned_config_sections` names its + // `config_section`. A row's name is a string literal or a path to a string constant, resolved + // as a PlaneDecl's is; a row that only CONSUMES a section declares nothing and is not read. A + // door with a declaring or owned row spelled any other way is not read here. let mut from = 0; while let Some(at) = text[from..].find(DOOR_SECTIONS) { let start = from + at + DOOR_SECTIONS.len(); @@ -117,26 +127,35 @@ fn facts(path: &str, raw: &str) -> Facts { }; let (mut declaring, mut owned) = (None, Vec::new()); let mut rows = &text[open..close]; - let mut literal = true; + let mut readable = true; while let Some(row) = rows.find("Section {") { let Some(body) = block(rows, row) else { break }; rows = &rows[row + body.len() + 2..]; + let flags = field(body, "flags").map(str::trim).unwrap_or_default(); + if flags.contains("SECTION_CONSUMED") { + continue; + } let name = body .find("abi_str(") .map(|i| &body[i + "abi_str(".len()..]) .and_then(|n| n.find(')').map(|e| n[..e].trim().to_string())); - let flags = field(body, "flags").map(str::trim).unwrap_or_default(); - let Some(name) = name.filter(|n| n.starts_with('"') && n.ends_with('"')) else { - literal = false; + let Some(name) = name.filter(|n| { + (n.len() >= 2 && n.starts_with('"') && n.ends_with('"')) + || (!n.is_empty() + && n.chars() + .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':')) + }) else { + readable = false; continue; }; if flags.contains("SECTION_DECLARING") { declaring = Some(name); - } else if !flags.contains("SECTION_CONSUMED") { + } else { owned.push(name); } } - if let (true, Some(declaring)) = (literal, declaring) { + if let (true, Some(declaring)) = (readable, declaring) { + owned.insert(0, declaring.clone()); out.decls.push(Ok((declaring, owned))); } } @@ -190,7 +209,9 @@ fn base(cx: &Ctx) -> Result, String> { for f in files { let rel = f.rel_str(); let facts = facts(&rel, &f.text); - if facts.consts.len() + facts.decls.len() > 0 || facts.core_owned.is_some() { + if facts.consts.len() + facts.aliases.len() + facts.decls.len() > 0 + || facts.core_owned.is_some() + { out.insert(rel, facts); } } @@ -252,6 +273,24 @@ fn resolve( if let Some(v) = found { return Ok(v); } + // A path through a module of the declaring file's own crate (`config::SECTION`): the crate's + // own definitions, when they agree. + if segs.len() > 1 { + if let Some(krate) = file + .strip_prefix(&format!("{ROOT}/")) + .and_then(|r| r.split_once('/')) + .map(|(k, _)| format!("{ROOT}/{k}/")) + { + let own: BTreeSet<&String> = defs + .iter() + .filter(|(p, _)| p.starts_with(&krate)) + .map(|(_, v)| v) + .collect(); + if own.len() == 1 { + return Ok(own.into_iter().next().cloned().unwrap_or_default()); + } + } + } let values: BTreeSet<&String> = defs.iter().map(|(_, v)| v).collect(); match values.len() { 1 => Ok(values.into_iter().next().cloned().unwrap_or_default()), @@ -318,6 +357,19 @@ pub fn read(cx: &Ctx) -> Result { .push((path.clone(), lit.clone())); } } + // An alias resolves to the literal its path names; one pass, an alias of a literal (an alias of + // an alias the reader cannot follow is left out, and a declaration naming it is then refused). + let mut resolved: Vec<(String, String, String)> = Vec::new(); + for (path, f) in &files { + for (name, target) in &f.aliases { + if let Ok(lit) = resolve(target, path, &consts) { + resolved.push((name.clone(), path.clone(), lit)); + } + } + } + for (name, path, lit) in resolved { + consts.entry(name).or_default().push((path, lit)); + } let core_owned: BTreeSet = files .values() .find_map(|f| f.core_owned.clone()) diff --git a/xtask/src/gates/config_schema/selftest.rs b/xtask/src/gates/config_schema/selftest.rs index fa723e1a77..382c35e4a7 100644 --- a/xtask/src/gates/config_schema/selftest.rs +++ b/xtask/src/gates/config_schema/selftest.rs @@ -450,6 +450,47 @@ pub fn run<'a>(gate: &'a dyn Gate, cx: &'a Ctx) -> Report<'a> { &["no struct in the tracked"], )); + // A DOOR plane's Statement declares its section in a `Section` row flagged SECTION_DECLARING + // (no PlaneDecl), and the reader reads it there. A door OWNS the section it declares (the + // kernel folds it into the pre-pass's map carrier, DECL-FOLD), so, like an owned PlaneDecl + // section below, it is an orphan when no carrier holds it: the case takes the tree's map carrier + // away. A row that only CONSUMES a section declares nothing. + let door_row = |flags: &str| { + format!( + "const ZZ_SECTION: &str = \"zz_door_no_such_carrier\";\nconst SECTIONS: &[Section] = \ + &[Section {{\n name: abi_str(ZZ_SECTION),\n flags: {flags},\n _reserved: 0,\n}}];\n" + ) + }; + let prepass = "crates/busbar-kernel/src/config/prepass.rs"; + let no_tree_map_carrier = |ov: &mut Overlay| { + ov.set( + prepass, + cx.read(prepass) + .unwrap_or_default() + .replace("= Declared::Any;", "= Declared::ByField;"), + ); + }; + let mut ov = Overlay::new(); + ov.set(plane_fixture, door_row("SECTION_DECLARING")); + no_tree_map_carrier(&mut ov); + report.push(prove_rows_red( + cx, + gate, + "a section a plane DOOR declares that no struct carries is REFUSED", + &[ROW_TRACKED_SOURCES], + ov, + &["no struct in the tracked"], + )); + let mut ov = Overlay::new(); + ov.set(plane_fixture, door_row("SECTION_CONSUMED")); + report.push(prove_rows_green( + cx, + gate, + "a section a plane door only CONSUMES is not read as declared", + &[ROW_TRACKED_SOURCES], + ov, + )); + // A declared section this reader cannot resolve to a string is a section it cannot freeze. let mut ov = Overlay::new(); ov.set(plane_fixture, declaration("ZZ_NO_SUCH_CONST", "")); @@ -483,15 +524,6 @@ pub fn run<'a>(gate: &'a dyn Gate, cx: &'a Ctx) -> Report<'a> { // with no map carrier the owned section is an orphan (RED); with a fixture map carrier it is // grammar (GREEN). A plane that does NOT own its declaring section stays an orphan either way // (the "no struct carries" case above). - let prepass = "crates/busbar-kernel/src/config/prepass.rs"; - let no_tree_map_carrier = |ov: &mut Overlay| { - ov.set( - prepass, - cx.read(prepass) - .unwrap_or_default() - .replace("= Declared::Any;", "= Declared::ByField;"), - ); - }; let mut ov = Overlay::new(); ov.set(plane_fixture, declaration("\"zz_owned\"", "\"zz_owned\"")); no_tree_map_carrier(&mut ov); diff --git a/xtask/src/gates/construction/census.rs b/xtask/src/gates/construction/census.rs index 893bda4fb6..90241ac7fb 100644 --- a/xtask/src/gates/construction/census.rs +++ b/xtask/src/gates/construction/census.rs @@ -175,7 +175,11 @@ pub fn ceiling_census(cx: &Ctx, cfg: &Cfg) -> Vec { cx.abs(d).exists() })); let mut moved = moved_out_by_kind(cx, cfg, &doc, &base.sha, &now_deleted, &renamed); - if let Some(n) = deleted_plane_crates(cx, cfg, &doc, &now_deleted) { + let base_planes = doc.table_or_empty("gate").list_of("plane_crates"); + let now_planes = cfg.plane_crates().unwrap_or_default(); + if let Some(n) = deleted_plane_crates(&base_planes, &now_planes, |dir| { + now_deleted.contains(dir) && !cx.abs(dir).exists() + }) { moved.insert("plane_crates".to_string(), n as i64); } bad.extend(lowered_floors(&cfg.doc, &doc, base.short(), &moved)); @@ -231,7 +235,8 @@ pub fn ceiling_census(cx: &Ctx, cfg: &Cfg) -> Vec { /// `plugin_kinds.` floor may drop by at most `moved[]`, the number of that kind's crate /// directories that left the tree as MOVED-OUT plugins or ruled deletions ([`excused_out`]), and /// `plane_crates` by the legacy engines deleted under `[gate.census.deleted]` -/// ([`deleted_plane_crates`], P3 DEL-MCP, ARCHITECT 2026-10-05). Every other drop is RED. +/// ([`deleted_plane_crates`], P3 DEL-MCP, ARCHITECT 2026-10-05; and `busbar-voice`, FLIP-STREAMING). +/// Every other drop is RED. /// A crate whose package name changed with the move counts only through a rename row ([`renames`], /// ARCHITECT 2026-10-03 Q-L7B2-CENSUS). fn lowered_floors( @@ -307,29 +312,21 @@ fn moved_out_by_kind( /// THE SAME RULED WAY DOWN FOR `gate.plane_crates` (P3 DEL-MCP, ARCHITECT 2026-10-05: "delete /// busbar-mcp, add `crates/busbar-mcp` to [gate.census.deleted]"). The legacy-engine list may lose /// an entry only when its crate was DELETED under the ledger: the entries the base listed and this -/// tree does not, `Some(n)` when EVERY one names a `crates/` that `[gate.census.deleted]` -/// lists and that is absent from this tree ([`excused_out`], no move-out arm — a legacy engine is -/// retired, never pinned back). `None` when nothing left the list or any one left any other way, so -/// the `plane_crates` floor drop stays RED. +/// tree does not, `Some(n)` when EVERY one names a `crates/` that is a ruled deletion +/// (`is_deleted`: `[gate.census.deleted]` lists it and it is absent from this tree; [`excused_out`], +/// no move-out arm — a legacy engine is retired, never pinned back). `None` when nothing left the +/// list or any one left any other way, so the `plane_crates` floor drop stays RED. fn deleted_plane_crates( - cx: &Ctx, - cfg: &Cfg, - base_doc: &Document, - deleted: &BTreeSet, + base: &[String], + now: &[String], + is_deleted: impl Fn(&str) -> bool, ) -> Option { - let now = cfg.plane_crates().unwrap_or_default(); - let gone: Vec = base_doc - .table_or_empty("gate") - .list_of("plane_crates") - .into_iter() + let gone: Vec = base + .iter() .filter(|c| !now.contains(c)) .map(|c| format!("crates/{c}")) .collect(); - excused_out( - &gone, - |_| false, - |dir| deleted.contains(dir) && !cx.abs(dir).exists(), - ) + excused_out(&gone, |_| false, is_deleted) } /// A plugin crate MOVED OUT (TODO PATH TO DEV-GREEN P5: filter-repo into its own repo, pinned back diff --git a/xtask/src/gates/construction/census_moveout_tests.rs b/xtask/src/gates/construction/census_moveout_tests.rs index 04a2bf3923..4db4204a11 100644 --- a/xtask/src/gates/construction/census_moveout_tests.rs +++ b/xtask/src/gates/construction/census_moveout_tests.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; -use super::{glob_matches, lowered_floors, moved_out}; +use super::{deleted_plane_crates, glob_matches, lowered_floors, moved_out}; const ROOT: &str = "[workspace.dependencies]\n\ busbar-x-moved = { git = \"https://example.invalid/busbar-x-moved\", rev = \"0123abc\" }\n\ @@ -111,6 +111,44 @@ fn a_kind_glob_matches_one_directory_level() { assert!(!glob_matches("crates/store-*", "crates/busbar-store-x")); } +/// The `plane_crates` floor's way down (ARCHITECT 2026-10-05): a legacy plane crate the base listed +/// and this tree does not counts only when it was deleted under `[gate.census.deleted]`. +#[test] +fn a_plane_crate_deleted_under_the_ledger_excuses_its_floor_drop() { + let base: Vec = ["busbar-llm", "busbar-voice"].map(String::from).to_vec(); + let now: Vec = vec!["busbar-llm".into()]; + assert_eq!( + deleted_plane_crates(&base, &now, |d| d == "crates/busbar-voice"), + Some(1) + ); + let mut moved = BTreeMap::new(); + moved.insert("plane_crates".to_string(), 1); + assert!(lowered_floors( + &doc("[gate.census]\nplane_crates = 3\n"), + &doc("[gate.census]\nplane_crates = 4\n"), + "base", + &moved + ) + .is_empty()); +} + +/// RED: a plane crate that left the list without a ledger row excuses nothing, and neither does a +/// list that lost nothing. +#[test] +fn an_unlisted_plane_crate_drop_stays_red() { + let base: Vec = ["busbar-llm", "busbar-voice"].map(String::from).to_vec(); + let now: Vec = vec!["busbar-llm".into()]; + assert_eq!(deleted_plane_crates(&base, &now, |_| false), None); + assert_eq!(deleted_plane_crates(&base, &base, |_| true), None); + assert!(!lowered_floors( + &doc("[gate.census]\nplane_crates = 3\n"), + &doc("[gate.census]\nplane_crates = 4\n"), + "base", + &BTreeMap::new() + ) + .is_empty()); +} + /// RED (ARCHITECT 2026-10-03, Q-L7B2-CENSUS): a crate whose package name changed with the move is /// NOT a move-out on the pin alone. `crates/busbar-x-old` left the tree, and the root pins /// `busbar-x-moved` at a rev, but with no rename row nothing says the new crate is the old one. diff --git a/xtask/src/gates/dep_wall.rs b/xtask/src/gates/dep_wall.rs index 5e475061a7..a047336f70 100644 --- a/xtask/src/gates/dep_wall.rs +++ b/xtask/src/gates/dep_wall.rs @@ -58,11 +58,7 @@ const BANNED_PATHS: &[&str] = &["std::net", "tokio::net"]; /// `busbar-mcp` is not here either (P3 DEL-MCP, ARCHITECT 2026-10-05): the engine is deleted, and the /// mcp plane is its door crate `busbar-plane-mcp`, a `gate.plugin_kinds.plane` row this wall already /// scans. Its eight pins in the ledger drained with it and are struck there. -const LEGACY: &[&str] = &[ - "crates/busbar-llm", - "crates/busbar-a2a", - "crates/busbar-voice", -]; +const LEGACY: &[&str] = &["crates/busbar-llm", "crates/busbar-a2a"]; /// The external source a plugin repository resolves from. const EXTERNAL_SOURCE: &str = "git+https://github.com/GetBusbar/"; diff --git a/xtask/src/gates/kind_isolation.rs b/xtask/src/gates/kind_isolation.rs index e6bf30eddb..383a57fb05 100644 --- a/xtask/src/gates/kind_isolation.rs +++ b/xtask/src/gates/kind_isolation.rs @@ -365,7 +365,7 @@ static KINDS: &[KindDef] = &[ KindDef { kind: "legacy", family: Family::Plane, - matchers: &["=busbar-llm", "=busbar-a2a", "=busbar-voice"], + matchers: &["=busbar-llm", "=busbar-a2a"], }, ]; @@ -427,8 +427,9 @@ const PENDING_EDGES: &[(&str, &str)] = &[ ]; /// The retiring 1.5.x crates, named so the ratchet can check they still exist. `busbar-mcp` retired -/// at P3 DEL-MCP (ARCHITECT 2026-10-05) and is struck, as `legacy-retired` requires. -const LEGACY_CRATES: &[&str] = &["busbar-llm", "busbar-a2a", "busbar-voice"]; +/// at P3 DEL-MCP (ARCHITECT 2026-10-05) and `busbar-voice` at FLIP-STREAMING (the streaming plane's +/// door serves every route); both are struck, as `legacy-retired` requires. +const LEGACY_CRATES: &[&str] = &["busbar-llm", "busbar-a2a"]; /// THE MANIFESTS IN THIS REPOSITORY THAT ARE NOT CRATES OF THE TREE, each with the sentence that /// says why, and each on the expiry ratchet every allowance in this file lives under: an entry that @@ -478,6 +479,17 @@ const OFF_TREE_MANIFESTS: &[(&str, &str)] = &[ never builds it, and it carries no kind and no plane or transport instance of its own. It \ is an input to the conformance leg, never a crate a rule here is written about.", ), + ( + "testing/voice-conformance/harness/Cargo.toml", + "the voice (streaming-plane) conformance HARNESS: a testkit binary the \ + `testing/voice-conformance/` legs shell out to, which loads the streaming plane's door both \ + ways through the loader (ARCHITECT Q6). It is a workspace member so \ + `testing/voice-conformance/lib/conform-bin.sh` can build it with \ + `-p voice-conformance-harness`, and it is not a crate of the product tree: it ships in no \ + artifact, `cargo build --bin busbar` never builds it, and it carries no kind. It lives \ + outside the plane crate so the plane's one user of the loader is its own both-ways witness. \ + It is an input to the conformance leg, never a crate a rule here is written about.", + ), ]; /// `qa/construction.toml`'s `[gate.plugin_kinds]` keys, each mapped onto the kind it names here. @@ -10222,15 +10234,17 @@ impl Gate for KindIsolationGate { // table, so the finding names the step the loop expected. The subject is a plane with the // legacy face only: a plane that also serves through its door (the flipped ones) is judged // by its door's slots instead, so removing its legacy file leaves the door face standing. + // The a2a plane is the one left with the legacy face only (FLIP-STREAMING deleted the + // streaming plane's `plane.rs`; it serves through its door alone). let mut ov = Overlay::new(); - ov.remove("crates/busbar-plane-streaming/src/plane.rs"); + ov.remove("crates/busbar-plane-a2a/src/plane.rs"); report.push(prove_rows_red( cx, subject, "a data plane that implements none of the strict step list", &[ROW_STEPS], ov, - &["missing-step", "busbar-plane-streaming"], + &["missing-step", "busbar-plane-a2a"], )); // A PLANE THAT RUNS THE KERNEL'S DECISION, both ways the ruling names: an `approve` of its @@ -10664,7 +10678,7 @@ impl Gate for KindIsolationGate { // // THE SHIPPED GATE, NOT THE DEBT-FREE SUBJECT: this case already brings its own green base // (the empty table), and the subject's debt — measured over the REAL table, whose - // `busbar-voice` row is exactly this plant's row — would take the planted finding out of + // `busbar-a2a` row is exactly this plant's row — would take the planted finding out of // view as if it were the tree's. The pair below is the same transition read the other way. report.push(prove_rows_red( &cx.with_overlay(registry_plant("")), @@ -11431,13 +11445,13 @@ fn announced_reaching_drain_target(cx: &Ctx) -> Overlay { /// THE LEGACY CRATE THE DRAIN PAIR IS ABOUT. It must be a `legacy` crate that is really on disk, /// or the red case plants a row for nothing and the green case removes a manifest that is not there. -const DRAIN_PLANT_FROM: &str = "busbar-voice"; +const DRAIN_PLANT_FROM: &str = "busbar-a2a"; /// ONE `[[transitional]]` row for [`DRAIN_PLANT_FROM`], and no other row — the red case's plant /// and, with the crate removed, the green case's. fn drain_row_plant() -> Overlay { registry_plant(&format!( - "[[transitional]]\nfrom = \"{DRAIN_PLANT_FROM}\"\nto = \"busbar-plane-streaming\"\nreason = \ + "[[transitional]]\nfrom = \"{DRAIN_PLANT_FROM}\"\nto = \"busbar-plane-a2a\"\nreason = \ \"legacy drain\"\n" )) } diff --git a/xtask/src/gates/no_float_money.rs b/xtask/src/gates/no_float_money.rs index 2f42784931..d865c75b37 100644 --- a/xtask/src/gates/no_float_money.rs +++ b/xtask/src/gates/no_float_money.rs @@ -545,10 +545,8 @@ pub const COUNT_READ_ROOTS: &[CountRoot] = &[ // `crates/busbar-voice-codec/src` home is struck. CountRoot { area: "the audio codecs, engine and plane", - homes: &[ - "crates/busbar-voice/src", - "crates/busbar-plane-streaming/src", - ], + // `crates/busbar-voice/src` is struck: FLIP-STREAMING deleted the legacy streams crate. + homes: &["crates/busbar-plane-streaming/src"], floor: 28, }, // THE ONE FOLD THIS GROUP WAS BUILT FOR ACTUALLY HAPPENED, AND THE GROUP DID NOT MOVE WITH IT. @@ -806,7 +804,8 @@ pub const PENDING_CEILING: usize = 0; /// reads are gone FAILS the row until it is deleted: a standing permission nobody uses is a /// permission the next read takes. RETIRED AT THE RE-KEY, each measured matching nothing on predev /// `5e672d125d` (the row's own "now match nothing" note): `openai_chat/handler.rs` `get(keys::END)` -/// and the two `get("channels")` rows (`topology/twilio.rs`, `twilio.rs`) — their reads were already +/// and the `get("channels")` row (`topology/twilio.rs`; the legacy `twilio.rs` copy went with +/// FLIP-STREAMING's deletion of `plane.rs` and friends) — their reads were already /// inside the window of the `get(keys::START)` / `get("sampleRate")` rows, and they are counted by /// those items' rows now. `audio::Segment`, `get(keys::START)` and `get(keys::END)` fold into the one /// `read_transcription_response` row. @@ -869,13 +868,6 @@ pub const ALLOWED_COUNT_READS: &[Allow] = &[ class: AllowClass::NotACount, why: "a media format's sample rate and channel count, not metered quantities", }, - Allow { - file: "crates/busbar-plane-streaming/src/twilio.rs", - item: "decode", - reads: 2, - class: AllowClass::NotACount, - why: "a media format's sample rate and channel count, not metered quantities", - }, Allow { file: "crates/busbar-kernel/src/config/migrate.rs", item: "migrate_governance", @@ -2842,10 +2834,10 @@ impl Gate for NoFloatMoneyGate { // // 1. A NEW defaulted read placed right after two allowed media-format reads, in the same // item, is a finding: it used to sit inside the allowance's 200-byte window and ride it. - let twilio = "crates/busbar-plane-streaming/src/twilio.rs"; + let twilio = "crates/busbar-plane-streaming/src/codec/topology/twilio.rs"; match cx.read(twilio) { Ok(text) => { - let at = " if encoding != ASSUMED_ENCODING"; + let at = " let media_format = MediaFormat {"; if text.contains(at) { let mut ov = Overlay::new(); ov.set( @@ -2853,7 +2845,7 @@ impl Gate for NoFloatMoneyGate { text.replacen( at, &format!( - " let _planted_billed = mf.get(\"usage\").and_then(serde_json::Value::as_u64).unwrap_or(0);\n{at}" + " let _planted_billed = mf.get(\"usage\").and_then(serde_json::Value::as_u64).unwrap_or(0);\n{at}" ), 1, ), diff --git a/xtask/src/gates/plane_pricing_blindness.rs b/xtask/src/gates/plane_pricing_blindness.rs index da57ddb4b5..f48d784ecf 100644 --- a/xtask/src/gates/plane_pricing_blindness.rs +++ b/xtask/src/gates/plane_pricing_blindness.rs @@ -215,10 +215,8 @@ pub const PLANE_CRATES: &[PlaneCrate] = &[ dir: "busbar-a2a", placement: "#83 SPLIT -> def 16-20", }, - PlaneCrate { - dir: "busbar-voice", - placement: "#83 SPLIT -> def 16-20", - }, + // `busbar-voice` is struck: FLIP-STREAMING deleted the legacy streams crate, and the streaming + // plane is `busbar-plane-streaming`, scanned as a plane crate. ]; /// The five CONTRACT planes that must each still declare at least one meter class ([`ROW_DECLARATION`]). diff --git a/xtask/src/gates/plane_purity/mod.rs b/xtask/src/gates/plane_purity/mod.rs index fc116d9139..3b1a8cf1b9 100644 --- a/xtask/src/gates/plane_purity/mod.rs +++ b/xtask/src/gates/plane_purity/mod.rs @@ -485,7 +485,7 @@ impl Gate for PlanePurityGate { "an include! dual-compile, carrying a pragma that must not launder it", &[ROW_PATH_INCLUDE], &plant_at, - "include!(\"../../../busbar-voice/src/witness.rs\"); // plane-purity: frozen-wire abuse\n", + "include!(\"../../../busbar-a2a/src/witness.rs\"); // plane-purity: frozen-wire abuse\n", &[ROW_PATH_INCLUDE, "planted_plane_purity.rs:1"], )); diff --git a/xtask/src/gates/plane_secret_blindness.rs b/xtask/src/gates/plane_secret_blindness.rs index cdf665c96c..0c7e79ab98 100644 --- a/xtask/src/gates/plane_secret_blindness.rs +++ b/xtask/src/gates/plane_secret_blindness.rs @@ -33,7 +33,8 @@ pub const NEEDLES: &[&str] = &[ ]; /// The crates that still carry plane code beside the `busbar-plane-*` crates. -pub const PLANE_HOMES: &[&str] = &["busbar-llm", "busbar-mcp", "busbar-a2a", "busbar-voice"]; +/// `busbar-voice` is struck: FLIP-STREAMING deleted the legacy streams crate. +pub const PLANE_HOMES: &[&str] = &["busbar-llm", "busbar-mcp", "busbar-a2a"]; pub const SPEC: Spec = Spec { gate: "plane-secret-blindness", diff --git a/xtask/src/gates/reachability.rs b/xtask/src/gates/reachability.rs index 62cf0265aa..1186bae750 100644 --- a/xtask/src/gates/reachability.rs +++ b/xtask/src/gates/reachability.rs @@ -311,12 +311,13 @@ const ROSTER: &[Plane] = &[ }, Plane { key: "streaming", - on_disk: "voice", - module: "units_voice", - linked_crate: "busbar-voice", + on_disk: "plane-streaming", + module: "units_streaming", + linked_crate: "busbar-plane-streaming", register_tokens: &["StreamingPlane"], - note: "#18: streaming is the PLANE, voice is one dialect inside it. The rename has not \ - landed, so the module and the crate are still spelled `voice`", + note: "#18: streaming is the PLANE (voice is one dialect inside it), a DOOR plane since \ + FLIP-STREAMING: its crate is `busbar-plane-streaming`, registered by its `plane-door` \ + row (Part 2 #2) and served through the one composition's driver (Q-SO10)", }, Plane { key: "decision", @@ -1774,7 +1775,6 @@ const AXIS_ITEMS: &[(&str, &[&str])] = &[ ("body-ingress", &["BODY_INGRESS"]), ("protocol-seams", &["install_protocol_seams"]), ("diagnostics", &["DIAGNOSTICS"]), - ("ws-arrivals", &["install_ws_arrivals"]), ("on-host", &["on_host"]), ("compose", &["compose"]), ]; @@ -2538,8 +2538,8 @@ impl Gate for ReachabilityGate { let f = &findings[p.key]; // THE ON-DISK SPELLING IS IN THE TITLE WHENEVER IT DIFFERS. A reader who finds // `reachability:unit-path:streaming` red has to know, without opening this file, that - // the module it is about is `units_voice.rs` — the #18 rename has not landed and a row - // naming a file that is not there is a row nobody can act on. + // the crate it is about — a row naming a file that is not there is a row nobody can act + // on. let who = if p.key == p.on_disk { format!("plane `{}`", p.key) } else { @@ -2820,15 +2820,13 @@ impl Gate for ReachabilityGate { cx, FIX_GREEN, "a key flipped only under #[cfg(test)] reds its unit-path and root-reach rows", - &[row_unit_path("streaming"), row_root_reach("streaming")], + &[row_unit_path("a2a"), row_root_reach("a2a")], { let mut ov = Overlay::new(); ov.set( INSTALL_RS, - FIXTURE_GREEN_INSTALL.replace( - VOICE_FLIP_LINE, - &format!(" #[cfg(test)]\n{VOICE_FLIP_LINE}"), - ), + FIXTURE_GREEN_INSTALL + .replace(A2A_FLIP_LINE, &format!(" #[cfg(test)]\n{A2A_FLIP_LINE}")), ); ov }, @@ -2885,9 +2883,9 @@ impl Gate for ReachabilityGate { ov.set( INSTALL_RS, FIXTURE_GREEN_INSTALL.replace( - VOICE_FLIP_LINE, + A2A_FLIP_LINE, &format!( - "{VOICE_FLIP_LINE} flip_one_shot_to_kernel(crate::root::plane_decisions::PLANE_DECLARATION.key);\n" + "{A2A_FLIP_LINE} flip_one_shot_to_kernel(crate::root::plane_decisions::PLANE_DECLARATION.key);\n" ), ), ); @@ -3004,7 +3002,7 @@ impl Gate for ReachabilityGate { cx, FIX_GREEN, "a plane whose linked-axes row carries no gauntlet axis is not flipped by the fold", - &[row_unit_path("streaming"), row_root_reach("streaming")], + &[row_unit_path("a2a"), row_root_reach("a2a")], folded_install(false), "no fold over `LINKED` flips it", )); @@ -3093,7 +3091,8 @@ impl Gate for ReachabilityGate { }), "the plane-crate population is empty", )); - // THE ON-DISK SPELLING MAPS: a plane crate declaring `voice` is the streaming plane's. + // THE ON-DISK SPELLING MAPS: a plane crate declaring `plane-streaming` is the streaming + // plane's. report.push(green_over( self, cx, @@ -3108,7 +3107,7 @@ impl Gate for ReachabilityGate { ov.set( "crates/busbar-plane-voice/src/lib.rs", "pub struct VoicePlane;\n\nimpl PlaneMeta for VoicePlane {\n \ - const KEY: &'static str = \"voice\";\n}\n", + const KEY: &'static str = \"plane-streaming\";\n}\n", ); ov }), @@ -3262,17 +3261,19 @@ impl Gate for ReachabilityGate { cx, FIX_GREEN, "a crate linked but not on the plane axis, and no plane_claims token, reds its registration row", - &[row_registered("streaming")], + &[row_registered("mcp")], { let mut ov = claims_name_no_plane(); ov.set( CRATE_MANIFEST, - FIXTURE_LINKED_MANIFEST - .replace("plane-streaming = \"plane diagnostics\"", "plane-streaming = \"diagnostics\""), + FIXTURE_LINKED_MANIFEST.replace( + "plane-mcp = \"plane protocols diagnostics\"", + "plane-mcp = \"protocols diagnostics\"", + ), ); ov }, - "no linked-table row for `busbar-voice` on the `plane` or `plane-door` axis", + "no linked-table row for `busbar-plane-mcp` on the `plane` or `plane-door` axis", )); // A DOOR ROW IS REGISTRATION (Part 2 #2, compiled-in = dropped-in): a door-shaped plane — // its declaration row on no `plane` axis, its crate linked again on `plane-door`, no @@ -3315,7 +3316,7 @@ impl Gate for ReachabilityGate { cx, FIX_GREEN, "a module `main.rs` reaches only through a root-units row of the generated table is reached", - evidenced(voice_reached_only_through_root_units(true)), + evidenced(mcp_reached_only_through_root_units(true)), )); report.push(red_over( self, @@ -3323,8 +3324,8 @@ impl Gate for ReachabilityGate { FIX_GREEN, "the same module with no root-units row reds the root-module row", &[ROW_ROOT_MODULE.to_string()], - voice_reached_only_through_root_units(false), - "units_voice", + mcp_reached_only_through_root_units(false), + "units_mcp", )); report @@ -3366,19 +3367,19 @@ fn claims_name_no_plane() -> Overlay { ov } -/// The green fixture with `main.rs` no longer calling into `units_voice` — reached, if at all, only +/// The green fixture with `main.rs` no longer calling into `units_mcp` — reached, if at all, only /// through a root-units row of the manifest (planted when `listed`). -fn voice_reached_only_through_root_units(listed: bool) -> Overlay { +fn mcp_reached_only_through_root_units(listed: bool) -> Overlay { let mut ov = Overlay::new(); ov.set( MAIN_RS, - FIXTURE_GREEN_MAIN.replace(" + root::units_voice::answer()\n", ""), + FIXTURE_GREEN_MAIN.replace(" + root::units_mcp::answer()\n", ""), ); if listed { ov.set( CRATE_MANIFEST, format!( - "{FIXTURE_LINKED_MANIFEST}\n[package.metadata.busbar.root-units]\nroot-voice = \"units_voice\"\n" + "{FIXTURE_LINKED_MANIFEST}\n[package.metadata.busbar.root-units]\nroot-mcp = \"units_mcp\"\n" ), ); } @@ -3406,7 +3407,6 @@ const FIXTURE_GREEN_DECISION: &str = include_str!("../../fixtures/reachability-green/crates/busbar/src/root/plane_decisions.rs"); const A2A_FLIP_LINE: &str = " flip_one_shot_to_kernel(busbar_a2a::PLANE_KEY);\n"; const MCP_FLIP_LINE: &str = " flip_one_shot_to_kernel(busbar_plane_mcp::PLANE_KEY);\n"; -const VOICE_FLIP_LINE: &str = " flip_session_to_kernel(busbar_voice::PLANE_KEY);\n"; const INSTALL_CALL_LINE: &str = " root::gauntlet_install::install();\n"; /// The decisions plane's entry as it is on the real tree: a declaration and hooks, no `Units` impl — @@ -3456,14 +3456,9 @@ fn decision_door(door_row: bool, driven: bool, twice: bool) -> Overlay { ); calls.push_str(" let _ = root::serve::compose_again();\n"); } + // The green fixture's root already carries `root/serve.rs` (the streaming door plane's one + // composition); this plants its decisions-shaped variant over it. ov.set(SERVE_RS, serve); - ov.set( - "crates/busbar/src/root/mod.rs", - format!( - "{}pub mod serve;\n", - include_str!("../../fixtures/reachability-green/crates/busbar/src/root/mod.rs") - ), - ); ov.set( MAIN_RS, FIXTURE_GREEN_MAIN.replace(INSTALL_CALL_LINE, &format!("{INSTALL_CALL_LINE}{calls}")), @@ -3600,15 +3595,11 @@ fn folded_install(all: bool) -> Overlay { .replace( "plane-mcp = \"plane protocols diagnostics\"", "plane-mcp = \"plane protocols diagnostics gauntlet-one-shot\"", - ) - .replace( - "plane-a2a = \"plane diagnostics\"", - "plane-a2a = \"plane diagnostics gauntlet-one-shot\"", ); if all { manifest = manifest.replace( - "plane-streaming = \"plane diagnostics\"", - "plane-streaming = \"plane diagnostics gauntlet-session\"", + "plane-a2a = \"plane diagnostics\"", + "plane-a2a = \"plane diagnostics gauntlet-one-shot\"", ); } let mut ov = Overlay::new(); diff --git a/xtask/src/gates/structure_lint/exit_tests.rs b/xtask/src/gates/structure_lint/exit_tests.rs index 714a81308a..8e792cc1f2 100644 --- a/xtask/src/gates/structure_lint/exit_tests.rs +++ b/xtask/src/gates/structure_lint/exit_tests.rs @@ -79,66 +79,72 @@ fn item_183_the_axis_ban_judges_the_money_path_and_the_contract() { ); } -/// Item 184: a concern duplicated between two planes that are not mcp+a2a is seen. +/// Item 184: a concern duplicated between two planes that are not mcp+a2a is seen. (The pair was +/// llm+voice until FLIP-STREAMING deleted `busbar-voice`; llm+a2a is the same "not mcp+a2a" pair.) #[test] -fn item_184_a_duplicate_between_llm_and_voice_is_seen() { +fn item_184_a_duplicate_between_llm_and_a2a_is_seen() { let mut ov = Overlay::new(); ov.set( - "crates/busbar-llm/src/planted_llm_voice_copy.rs", - "pub fn planted_llm_voice_copy() {}\n", + "crates/busbar-llm/src/planted_llm_a2a_copy.rs", + "pub fn planted_llm_a2a_copy() {}\n", ); ov.set( - "crates/busbar-voice/src/planted_llm_voice_copy.rs", - "pub fn planted_llm_voice_copy() {}\n", + "crates/busbar-a2a/src/a2a/planted_llm_a2a_copy.rs", + "pub fn planted_llm_a2a_copy() {}\n", ); names( &run(ov), UNLEDGERED, &[ - "`planted_llm_voice_copy`", - "llm:crates/busbar-llm/src/planted_llm_voice_copy.rs:1", - "voice:crates/busbar-voice/src/planted_llm_voice_copy.rs:1", + "`planted_llm_a2a_copy`", + "llm:crates/busbar-llm/src/planted_llm_a2a_copy.rs:1", + "a2a:crates/busbar-a2a/src/a2a/planted_llm_a2a_copy.rs:1", ], ); } -/// Item 223: a plane-local copy of another plane's helper (voice beside a2a) is seen, and a name the -/// ledger signed for mcp+a2a is NOT excused when a third plane grows a copy of it. +/// Item 223: a plane-local copy of another plane's helper (llm beside a2a; voice until FLIP-STREAMING +/// deleted `busbar-voice`) is seen, and a name the ledger signed for mcp+a2a is NOT excused when a +/// third plane grows a copy of it. /// /// The signed name is `config.rs`, the ledger's one surviving row: its fn-name siblings (`judge` -/// among them) were struck at P3 DEL-MCP with the engine whose half they signed for. The tree -/// already carries the stretch case — voice's own `config.rs` beside a2a's — so the refusal is read -/// off the real files. +/// among them) were struck at P3 DEL-MCP with the engine whose half they signed for. The stretch +/// case (a third plane's `config.rs` beside a2a's) is no longer on disk now that voice is deleted, +/// so llm's `config.rs` is planted beside a2a's real one. #[test] -fn item_223_a_voice_copy_of_a_plane_helper_is_seen_and_a_signed_claim_does_not_stretch() { +fn item_223_a_third_plane_copy_of_a_plane_helper_is_seen_and_a_signed_claim_does_not_stretch() { let signed = plane_dups::ledger() .into_iter() .find(|r| r.name == "config.rs") .expect("the ledger signs `config.rs` for mcp and a2a"); let mut ov = Overlay::new(); ov.set( - "crates/busbar-voice/src/planted_voice_a2a_copy.rs", - "pub fn planted_voice_a2a_copy() {}\n", + "crates/busbar-llm/src/planted_llm_a2a_helper.rs", + "pub fn planted_llm_a2a_helper() {}\n", ); ov.set( - "crates/busbar-a2a/src/a2a/planted_voice_a2a_copy.rs", - "pub fn planted_voice_a2a_copy() {}\n", + "crates/busbar-llm/src/config.rs", + "pub fn planted_llm_config() {}\n", + ); + ov.set( + "crates/busbar-a2a/src/a2a/planted_llm_a2a_helper.rs", + "pub fn planted_llm_a2a_helper() {}\n", ); names( &run(ov), UNLEDGERED, &[ - "`planted_voice_a2a_copy`", - "voice:crates/busbar-voice/src/planted_voice_a2a_copy.rs:1", + "`planted_llm_a2a_helper`", + "llm:crates/busbar-llm/src/planted_llm_a2a_helper.rs:1", &format!("`{}`", signed.name), - "voice:crates/busbar-voice/src/config.rs", + "llm:crates/busbar-llm/src/config.rs", "the ledger row signs for a2a+mcp only", ], ); } /// Item 224: the plane set is derived — a plane on no constant is compared, and a plane the old -/// constant never named (voice) is judged by the plane-roots row. +/// two-plane constant never named (llm) is judged by the plane-roots row. #[test] fn item_224_the_plane_set_is_derived_not_spelled() { let mut ov = Overlay::new(); @@ -152,7 +158,7 @@ fn item_224_the_plane_set_is_derived_not_spelled() { "pub fn planted_derived_plane_helper() {}\n", ); ov.set( - "crates/busbar-planted-twin/src/voice/mod.rs", + "crates/busbar-planted-twin/src/llm/mod.rs", "pub const PLANE_DECL: busbar_kernel::plane::registry::PlaneDecl = PLANTED;\n", ); let v = run(ov); @@ -164,7 +170,7 @@ fn item_224_the_plane_set_is_derived_not_spelled() { "planted:crates/busbar-planted/src/lib.rs:2", ], ); - names(&v, PLANE_ROOTS, &["PLANE-ROOT-AMBIGUOUS", "`voice`"]); + names(&v, PLANE_ROOTS, &["PLANE-ROOT-AMBIGUOUS", "`llm`"]); } /// Item 236: the DEBT half is reachable — a DEBT row naming an undeclared concern is refused even diff --git a/xtask/src/gates/structure_lint/plane_dups.rs b/xtask/src/gates/structure_lint/plane_dups.rs index 630c6fbd45..b64b9383c1 100644 --- a/xtask/src/gates/structure_lint/plane_dups.rs +++ b/xtask/src/gates/structure_lint/plane_dups.rs @@ -114,7 +114,41 @@ pub fn ledger() -> Vec { note: note.to_string(), planes: vec!["mcp".to_string(), "a2a".to_string()], }; + // FLIP-STREAMING: the streaming plane's home moved from `busbar-voice` to its door crate + // `busbar-plane-streaming`, and every row below was argued for exactly the planes it names + // against that crate. NOT signed, because they are copies and not distinct concerns: `settle` + // (decisions' `plane_door` and streaming's `piece`), `absolute` (a2a's `serve` and streaming's + // `door`) and `claim` (mcp's `tool_claims` and streaming's `claims`) — those stay findings. + let s = |name: &str, planes: &[&str], note: &str| LedgerRow { + name: name.to_string(), + class: Class::Distinct, + concern: String::new(), + note: note.to_string(), + planes: planes.iter().map(|p| (*p).to_string()).collect(), + }; vec![ + s("claims.rs", &["decisions", "streaming"], "Each plane's own claim table: decisions declares two exact-path claims under its decision-inbound scheme, streaming five dialect-tagged claims over ws and http under its streaming-key scheme. The table is per plane by construction; the claim grammar they both build is the contract's."), + s("diagnostics.rs", &["mcp", "streaming"], "Each plane's own diagnostic catalogue handed to the root's install_diagnostics: the MCP_* entries and STREAMING_SESSION_LEASE_EXHAUSTED. They share no entry; the catalogue type is the contract's."), + s("door.rs", &["mcp", "streaming"], "Each plane's door declaration: its Statement tail, its ROUTES table, its snapshot and settings. A door is per plane by definition; the ABI it fills is the contract's."), + s("driven.rs", &["decisions", "streaming"], "Each plane's answers to the kernel plane driver's crossings (arrive, on_piece, refusal): jev's systemone operation for decisions, the mint and SDP-offer doors for streaming. The slots are the ABI's; the answers are the plane's."), + s("meta.rs", &["decisions", "streaming"], "Each plane's PlaneMeta with its own meter classes: decisions' single decision class, streaming's audio token classes. The classes are what makes each plane's meter its own."), + s("usage.rs", &["llm", "streaming"], "Unrelated: llm's engine file bills a non-streaming token response to a key's budget (record_resp_usage); streaming's codec file is the Realtime and Gemini duplex usage IR and its float-tolerant count reader."), + s("Answer", &["mcp", "streaming"], "Unrelated: mcp's Answer is the reply to one MCP arrival (here with a status and body, or far); streaming's is a RequestUnit's per-piece answer (nothing, attempt, to the far end, to the caller, refused)."), + s("Arrival", &["mcp", "streaming"], "Unrelated: mcp's Arrival is one carrier line read (a LineUnit and an optional dispatch body); streaming's is its arrive slot's answer (door, op class, dialect)."), + s("CredentialPlacement", &["a2a", "streaming"], "Unrelated: a2a's places a leased credential in an HTTP header (Authorization Bearer or a named header); streaming's places a provider key on a WebSocket dial per dialect (a query parameter or a prefixed header)."), + s("Dialect", &["a2a", "streaming"], "Unrelated: a2a's Dialect is the push-config verb spelling (V03 or V10); streaming's is its voice dialect roster (OpenAI Realtime, Gemini Live, Twilio Media Streams, one-shot transcribe and tts)."), + s("Held", &["mcp", "streaming"], "Unrelated: mcp's Held is one generation's catalogue, tools section and pools; streaming's is a one-request door unit's state (its RequestUnit, what it owes, a short answer)."), + s("Outbound", &["a2a", "streaming"], "Unrelated: a2a's Outbound is the caller's request in three forms for the outbound hop; streaming's is a session frame plan (upstream and downlink wire events, close, a refused reply)."), + s("Reply", &["mcp", "streaming"], "Unrelated: mcp's Reply is a caller's answer to one of busbar's own asks (answered or failed); streaming's is the caller's HTTP answer (status, fields, body, the rtc call id)."), + s("Route", &["mcp", "streaming"], "Each plane's own row type for its door's ROUTES table: mcp's carries verb, target, carrier and open; streaming's adds upgrade, auth, dialect and refusal dialect. The row is shaped by what the plane routes on."), + s("Session", &["mcp", "streaming"], "Unrelated: mcp's Session is the carrier session's state (streams, buffered events, revision, owner); streaming's is the Realtime or Gemini enum over a codec's SessionUnit."), + s("Unit", &["decisions", "streaming"], "Unrelated: decisions' Unit is one unit's attempt, reading and owed bytes in its plane door; streaming's is the Realtime or Gemini codec enum its session door dispatches over."), + s("arrive", &["mcp", "decisions", "streaming"], "Each plane's body for the arrive slot: mcp reads a carrier line, decisions classifies verb and target through its ops table, streaming maps a claim index to a door. The slot is the ABI's; what an arrival means is the plane's."), + s("open", &["mcp", "streaming"], "Unrelated: mcp's door_listen open starts a subscriptions/listen subscription and its tool_claims open builds a claim with no credential scheme; streaming's constructs a live Session for a door."), + s("outbound", &["mcp", "streaming"], "Unrelated: mcp's builds the OutboundCall request (POST, path, body) for one tool-call round; streaming's is a const naming a connection need on the provider egress class."), + s("parse", &["a2a", "streaming"], "Unrelated: a2a's reads an agent card value into an AgentCard; streaming's reads wire bytes into a JSON value in its codec."), + s("read_settings", &["decisions", "streaming"], "Each plane judges its own settings blob by its own grammar and answers its own section for an empty one: DecisionsSection for decisions, StreamsCfg for streaming. The grammar is the plane's."), + s("refusal_body", &["decisions", "streaming"], "Each plane renders the kernel's refusal in its own wire's error shape: jev's error code and message for decisions, the OpenAI REST error envelope for streaming. The status and text are the kernel's; the shape is the dialect's."), d("config.rs", "The parse ORDER these two files shared is gone: plane::config::split_section owns the reserved-key refusals, the two typed lifts and the sequence they run in, and all THREE plane sections are read through it. What is left in each file is that plane's GRAMMAR and nothing else — they share no type, no field, no value rule, no sentence and no caller. What they share is a filename, which on every plane says the same true and un-actionable thing: this is where that section's grammar is written."), // STRUCK at P3 DEL-MCP (ARCHITECT 2026-10-05: busbar-mcp deleted, the mcp plane is served // through its door crate alone): `transport.rs`, `judge`, `revalidate`, `observed_pin`, diff --git a/xtask/src/gates/structure_lint/selftest.rs b/xtask/src/gates/structure_lint/selftest.rs index 98f7c3dbea..2b640a10f6 100644 --- a/xtask/src/gates/structure_lint/selftest.rs +++ b/xtask/src/gates/structure_lint/selftest.rs @@ -326,13 +326,39 @@ pub fn run<'a>(gate: &'a StructureLintGate, cx: &'a Ctx) -> Report<'a> { ), } + // The same for STREAMING since FLIP-STREAMING: `voice` left the legacy roster with + // `crates/busbar-voice`, and the plane's door row is all that locates it. Without `streaming` on + // the kind-only roster, taking that row's `plane-door` axis off would drop the plane from the + // roster in silence. + match cx.read(&manifest_rel) { + Ok(text) if text.contains("plane-streaming = \"plane-door\"") => { + let mut ov = Overlay::new(); + ov.set( + &manifest_rel, + text.replace("plane-streaming = \"plane-door\"", "plane-streaming = \"\""), + ); + report.push(tree_case( + cx, + gate, + "the streaming plane whose door row left the manifest is refused, not silently unscanned", + &[roots::ROW_PLANE_ROOTS], + ov, + &["PLANE-ROOT-MISSING", "streaming"], + )); + } + _ => report.note_infra_failure( + "structure-lint selftest: the manifest carries no `plane-streaming = \"plane-door\"` row, \ + so the streaming plane's plane-root rule has nothing to plant against", + ), + } + // A PLANE THE OLD CONSTANT NEVER NAMED (item 224). `PLANES` was `["mcp", "a2a"]`, so a second // home for voice could not be reported: the row "every plane resolves to exactly one home" was // true of the two planes it had been given. A second declaration of voice, in a directory named // for it, is two homes for one plane. let mut ov = Overlay::new(); ov.set( - "crates/busbar-planted/src/voice/mod.rs", + "crates/busbar-planted/src/llm/mod.rs", format!( "{} : busbar_kernel::plane::registry::PlaneDecl = PLANTED;\n", crate::planes::PLANE_GRAMMAR @@ -344,7 +370,7 @@ pub fn run<'a>(gate: &'a StructureLintGate, cx: &'a Ctx) -> Report<'a> { "a plane outside the old two-plane constant with two homes is refused", &[roots::ROW_PLANE_ROOTS], ov, - &["PLANE-ROOT-AMBIGUOUS", "voice"], + &["PLANE-ROOT-AMBIGUOUS", "llm"], )); // ── the denominator ────────────────────────────────────────────────────────────────────────── @@ -648,7 +674,9 @@ pub fn run<'a>(gate: &'a StructureLintGate, cx: &'a Ctx) -> Report<'a> { // * A THIRD COPY OF A LEDGERED NAME — the ledger's rows were argued for mcp and a2a; a copy in // a plane the claim was never signed for is a file nobody read, and matching the row by name // alone absorbed it. - let voice = addresses.plane("voice"); + // The third plane was voice until FLIP-STREAMING deleted `busbar-voice`; llm is the same + // "never loaded beside a2a" pair. + let voice = addresses.plane("llm"); let mut ov = Overlay::new(); ov.set( format!("{voice}/planted_shared_concern.rs"), @@ -669,11 +697,11 @@ pub fn run<'a>(gate: &'a StructureLintGate, cx: &'a Ctx) -> Report<'a> { let signed = t .plane_ledger .iter() - .find(|r| r.planes.iter().all(|p| p != "voice") && !r.name.ends_with(".rs")) + .find(|r| r.planes.iter().all(|p| p != "llm") && !r.name.ends_with(".rs")) .map(|r| r.name.clone()); let mut naming: Vec = vec![ "PLANE-DUPLICATE (symbol): `planted_shared_concern`".to_string(), - format!("voice:{voice}/planted_shared_concern.rs"), + format!("llm:{voice}/planted_shared_concern.rs"), "PLANE-DUPLICATE (symbol): `planted_derived_plane_helper`".to_string(), "planted:crates/busbar-planted/src/lib.rs".to_string(), ]; diff --git a/xtask/src/planes.rs b/xtask/src/planes.rs index 0b1b37e89f..9c0737a50f 100644 --- a/xtask/src/planes.rs +++ b/xtask/src/planes.rs @@ -32,21 +32,25 @@ use std::collections::BTreeMap; use std::fmt; use std::path::{Path, PathBuf}; -/// The canonical order is the doctrine order: the three original protocols, then voice (Plane 4). +/// The canonical order is the doctrine order: the legacy-engine protocols. `voice` (Plane 4's legacy +/// crate, `crates/busbar-voice`) is struck: FLIP-STREAMING deleted it, and the streaming plane is +/// `busbar-plane-streaming`, scanned by the PLANE-KIND regime like every `busbar-plane-*` crate. /// /// THE LEGACY-ENGINE ROSTER: every consumer reads a key as a `crates/busbar-` directory (see /// [`plane_src_roots`]). `mcp` IS STRUCK (P3 DEL-MCP, ARCHITECT 2026-10-05; its shell twin /// `scripts/plane-keys.sh` says the same in the same commit): `crates/busbar-mcp` is deleted and the /// mcp plane is plane-kind only, its door crate `busbar-plane-mcp` — scanned by the plane-kind regime /// and located by its door row ([`door_planes`]) wherever a gate resolves the plane's home. -pub const PLANE_KEYS: [&str; 3] = ["llm", "a2a", "voice"]; +pub const PLANE_KEYS: [&str; 2] = ["llm", "a2a"]; /// Plane keys with NO legacy-engine crate: the plane lives only in its plane-kind crate /// `busbar-plane-` and is located by its door row ([`door_planes`]). Twin of /// `scripts/plane-keys.sh`'s `PLANE_KEYS_KIND_ONLY`. A roster that must notice a plane VANISHING /// (structure-lint's plane-roots row) reads these beside [`PLANE_KEYS`]: leaving the legacy roster -/// must not take a plane off the roster that refuses an unlocatable plane. -pub const PLANE_KEYS_KIND_ONLY: [&str; 1] = ["mcp"]; +/// must not take a plane off the roster that refuses an unlocatable plane. `streaming` joined when +/// FLIP-STREAMING took `voice` off [`PLANE_KEYS`]: `crates/busbar-voice` is deleted and the plane is +/// `busbar-plane-streaming`, located by its `plane-streaming` door row. +pub const PLANE_KEYS_KIND_ONLY: [&str; 2] = ["mcp", "streaming"]; /// The default ownership grammar. Overridable for a fixture tree, the way /// `PLANE_ROOTS_GRAMMAR` is in the shell. @@ -173,7 +177,7 @@ pub fn plane_src_roots() -> Vec { /// /// * the five `busbar-plane-*` crates — the plane kind itself, scanned by the REVERSE side of /// `plane-purity` through `kind_isolation::plane_kind_src_roots`; -/// * `busbar-llm`, `busbar-mcp`, `busbar-a2a`, `busbar-voice` and the two surviving `-codec` halves +/// * `busbar-llm`, `busbar-a2a` (`busbar-mcp` and `busbar-voice` are deleted) and the two surviving `-codec` halves /// — the legacy engines: naming a protocol is what they are for, and [`plane_src_roots`] is the /// list that scans them; /// * the plugin INSTANCES (`busbar-transport-*`, `store-*`, `secret-*`, `auth-*`, `hook*`, diff --git a/xtask/src/toml_doc.rs b/xtask/src/toml_doc.rs index 2aa6fa8cc4..2cbfc777e7 100644 --- a/xtask/src/toml_doc.rs +++ b/xtask/src/toml_doc.rs @@ -611,8 +611,8 @@ forbidden = [ doc.table("gate") .expect("gate table") .list_of("plane_crates"), - // busbar-mcp left at P3 DEL-MCP (ARCHITECT 2026-10-05; [gate.census.deleted]). - vec!["busbar-llm", "busbar-a2a", "busbar-voice"] + // busbar-mcp left at P3 DEL-MCP and busbar-voice at FLIP-STREAMING ([gate.census.deleted]). + vec!["busbar-llm", "busbar-a2a"] ); assert_eq!( doc.children("rules.legacy-reach.prefixes") diff --git a/xtask/tests/declared_classes.rs b/xtask/tests/declared_classes.rs index 0c2fb30f61..51dad4cb59 100644 --- a/xtask/tests/declared_classes.rs +++ b/xtask/tests/declared_classes.rs @@ -39,7 +39,6 @@ const SCANNED: &[&str] = &[ // `crates/busbar-mcp/src` STRUCK (P3 DEL-MCP, ARCHITECT 2026-10-05): the engine is deleted; the // mcp plane is `crates/busbar-plane-mcp/src`, scanned above. "crates/busbar-a2a/src", - "crates/busbar-voice/src", ]; /// The shapes a class key enters a usage report through. @@ -49,13 +48,17 @@ const SHAPES: &[&str] = &[ "=> Billing::Counted {", "usage_units: std::collections::BTreeMap::from(", "usage_units: class_counts(", + // The streaming door's session sink folds each closed turn's counts into its unit tail. + "in class_counts(", ]; /// Where a plane's `PlaneDeclaration` (and so its `billable_classes`) is written. const DECLARATIONS: &[(&str, &str)] = &[ ("llm", "crates/busbar-llm/src/lib.rs"), ("a2a", "crates/busbar-a2a/src/a2a/mod.rs"), - ("streaming", "crates/busbar-voice/src/lib.rs"), + // FLIP-STREAMING: the streaming plane is a door; its classes are its Statement's + // `BILLABLE_CLASSES` table (`class(, )` rows). + ("streaming", "crates/busbar-plane-streaming/src/door.rs"), ]; /// One report site: its plane, its file, its shape and how many times the shape occurs there, and @@ -116,11 +119,12 @@ const SITES: &[Site] = &[ emits: &["busbar_plane_a2a::meta::CLASS_BYTES"], dead: None, }, - // `class_counts` (busbar-plane-streaming's session.rs) names these six. + // `class_counts` (busbar-plane-streaming's session.rs) names these six; the door's session + // sink reports each closed turn's counts under them. Site { plane: "streaming", - file: "crates/busbar-voice/src/runtime/metering.rs", - shape: "usage_units: class_counts(", + file: "crates/busbar-plane-streaming/src/session_unit.rs", + shape: "in class_counts(", occurrences: 1, emits: &[ "busbar_plane_streaming::meta::CLASS_AUDIO_TOKENS_IN", @@ -212,6 +216,61 @@ fn literal_of(src: &str, name: &str) -> Option { Some(def[open + 1..open + 1 + close].to_string()) } +/// A plane DOOR's classes: its Statement's `const BILLABLE_CLASSES: &[BillableClass] = &[ … ]` +/// table of `class(, )` rows, each class a crate-relative constant path +/// (`meta::CLASS_X.as_str()`) or a name the file imports (`use busbar_contract::plane::{PER_SESSION, +/// …}`). `None` when the file holds no such table (a `PlaneDeclaration` file). +fn door_classes(root: &Path, file: &str, src: &str) -> Result>, String> { + let Some(at) = src.find("const BILLABLE_CLASSES: &[BillableClass] = &[") else { + return Ok(None); + }; + let body = &src[at..]; + let end = body + .find("\n];") + .ok_or_else(|| format!("{file}: an unterminated BILLABLE_CLASSES"))?; + let krate = file + .strip_prefix("crates/") + .and_then(|r| r.split('/').next()) + .ok_or_else(|| format!("{file}: not under crates/"))? + .replace('-', "_"); + let mut classes = BTreeSet::new(); + for line in body[..end].lines() { + let Some(row) = line.trim().strip_prefix("class(") else { + continue; + }; + let expr = row.split(',').next().unwrap_or("").trim(); + let expr = expr.trim_end_matches(".as_str()"); + let path = if let Some(lit) = expr.strip_prefix('"') { + classes.insert(lit.trim_end_matches('"').to_string()); + continue; + } else if expr.contains("::") { + format!("{krate}::{expr}") + } else { + let import = src + .lines() + .filter_map(|l| l.trim().strip_prefix("use ")) + .find_map(|l| { + let l = l.trim_end_matches(';'); + match l.split_once("::{") { + Some((module, names)) => names + .trim_end_matches('}') + .split(',') + .any(|n| n.trim() == expr) + .then(|| format!("{module}::{expr}")), + None => (l.rsplit("::").next() == Some(expr)).then(|| l.to_string()), + } + }) + .ok_or_else(|| format!("{file}: class `{expr}` is neither a path nor imported"))?; + import + }; + classes.insert(resolve(root, &path)?); + } + if classes.is_empty() { + return Err(format!("{file}: a BILLABLE_CLASSES table with no rows")); + } + Ok(Some(classes)) +} + /// The classes a plane's `PlaneDeclaration::billable_classes` names, resolved. fn declared(root: &Path, plane: &str) -> Result, String> { let (_, file) = DECLARATIONS @@ -219,6 +278,9 @@ fn declared(root: &Path, plane: &str) -> Result, String> { .find(|(p, _)| *p == plane) .ok_or_else(|| format!("no declaration file for plane {plane}"))?; let src = std::fs::read_to_string(root.join(file)).map_err(|e| format!("{file}: {e}"))?; + if let Some(classes) = door_classes(root, file, &src)? { + return Ok(classes); + } let start = src .find("billable_classes: &") .ok_or_else(|| format!("{file}: no billable_classes"))?; diff --git a/xtask/tests/infra.rs b/xtask/tests/infra.rs index 61f9dee334..820040a962 100644 --- a/xtask/tests/infra.rs +++ b/xtask/tests/infra.rs @@ -494,9 +494,10 @@ fn strip_comment_line_keeps_string_literals_intact() { #[test] fn the_plane_key_contract_matches_plane_keys_sh() { - // `mcp` left the legacy-engine roster at P3 DEL-MCP (ARCHITECT 2026-10-05). - assert_eq!(planes::PLANE_KEYS, ["llm", "a2a", "voice"]); - assert_eq!(planes::plane_keys_protocol(), vec!["a2a", "voice"]); + // `mcp` left the legacy-engine roster at P3 DEL-MCP (ARCHITECT 2026-10-05) and `voice` at + // FLIP-STREAMING (crates/busbar-voice deleted). + assert_eq!(planes::PLANE_KEYS, ["llm", "a2a"]); + assert_eq!(planes::plane_keys_protocol(), vec!["a2a"]); // READ THE SHELL'S KEYS TOO: the two rosters are one list. let keys_line = std::fs::read_to_string(repo_root().join("scripts/plane-keys.sh")) .expect("scripts/plane-keys.sh is readable") diff --git a/xtask/tests/p_item_refusal_reason_collapse.rs b/xtask/tests/p_item_refusal_reason_collapse.rs index 61f17d1df6..3b92202f95 100644 --- a/xtask/tests/p_item_refusal_reason_collapse.rs +++ b/xtask/tests/p_item_refusal_reason_collapse.rs @@ -41,8 +41,8 @@ const RENDERERS: &[(&str, &[&str])] = &[ ), ("crates/busbar-plane-a2a/src/plane.rs", &["refusal_render"]), ( - "crates/busbar-plane-streaming/src/plane.rs", - &["refusal_render"], + "crates/busbar-plane-streaming/src/door.rs", + &["refusal_body", "refusal_render"], ), ( "crates/busbar/src/root/units_admin/admin_mount.rs", @@ -215,7 +215,7 @@ fn p_item_refusal_reason_collapse_the_classifier_names_every_reason() { #[test] fn p_item_refusal_reason_collapse_the_scan_catches_a_reason_match() { let reasons = vocabulary(); - let old = r#"fn refusal_render(reason: busbar_contract::unit::RefusalReason) -> (&'static str, &'static str) { + let old = r#"fn refusal_body(reason: busbar_contract::unit::RefusalReason) -> (&'static str, &'static str) { use busbar_contract::unit::RefusalReason as R; match reason { R::RateLimited | R::InFlightCap => ("rate_limited", "too many"), @@ -223,18 +223,26 @@ fn p_item_refusal_reason_collapse_the_scan_catches_a_reason_match() { } } "#; + // The plant puts `refusal_body` back the old way inside the door's real source, so every other + // renderer the scan names in that file (`refusal_render`) is still read, and still clean. let planted = findings(&reasons, &|f| { - if f == "crates/busbar-plane-streaming/src/plane.rs" { - old.to_string() + let real = read(f); + if f == "crates/busbar-plane-streaming/src/door.rs" { + let start = real + .find("fn refusal_body(") + .expect("the door's refusal_body"); + let body = body_of(&real[start..], "refusal_body").expect("its body"); + let end = start + real[start..].find(body).expect("the body in place") + body.len(); + format!("{}{old}{}", &real[..start], &real[end..]) } else { - read(f) + real } }); assert_eq!( planted, [ - "crates/busbar-plane-streaming/src/plane.rs fn refusal_render names the reason R::InFlightCap", - "crates/busbar-plane-streaming/src/plane.rs fn refusal_render names the reason R::RateLimited", + "crates/busbar-plane-streaming/src/door.rs fn refusal_body names the reason R::InFlightCap", + "crates/busbar-plane-streaming/src/door.rs fn refusal_body names the reason R::RateLimited", ] ); let spelled = r#"fn kind_of(reason: &str, status: u16) -> &'static str {